Skip to content

Instantly share code, notes, and snippets.

@stevenhao
Last active March 28, 2020 04:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stevenhao/9e2f8d511de18e8478178f3ea6745546 to your computer and use it in GitHub Desktop.
Save stevenhao/9e2f8d511de18e8478178f3ea6745546 to your computer and use it in GitHub Desktop.
Lint prestodb sql in mode analytic's web editor
// ==UserScript==
// @name PrestoDB Linter v0.1.3
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author Steven Hao
// @match https://modeanalytics.com/editor/*
// @grant none
// ==/UserScript==
// make sure to get the following dependencies (all tampermonkey scripts):
// 1) Ace Editor Exposer (https://gist.github.com/stevenhao/0c5356ce0fbfcd57f81cb601256a34fd)
// 2) Presto Parser (https://gist.github.com/stevenhao/94d989155bb768a48c23dedd19ada221)
// Promise.delay modified https://gist.github.com/joepie91/2664c85a744e6bd0629c
Promise.delay = function(duration) {
return new Promise(function(resolve, reject){
setTimeout(function(){
resolve();
}, duration)
});
};
if (typeof window.flatMap === 'undefined') {
window.flatMap = (ar, fn) => {
const result = [];
for (const a of ar) {
result.push(...fn(a));
}
return result;
};
}
(function() {
'use strict';
const QA = (...args) => document.querySelectorAll(...args);
const Q = (...args) => document.querySelector(...args);
const getSource = () => {
if (window.aceEditors && window.aceEditors.length) {
const editor = window.aceEditors[window.aceEditors.length - 1].editor;
return editor.getValue();
} else {
console.warn('ace editors not found');
const textLayer = Q('.ace_text-layer');
const source = Array.from(textLayer.childNodes).map(x => x.textContent).join('\n');
return source;
}
};
const analyze = async () => {
const errors = [];
if (window.prestoParser) {
const rawSource = getSource();
try {
await window.prestoParser.parse(rawSource.toUpperCase());
} catch (e) {
const {
line, column, msg,
} = e;
errors.push({
line,
i: column,
message: msg,
});
}
} else {
console.warn('presto parser not found');
const rawLines = getSource().split('\n');
const tokens = flatMap(rawLines, (val, line) => {
const commentIdx = val.indexOf('--');
if (commentIdx !== -1) {
const commentedOutText = val.substring(commentIdx);
val = val.substring(0, commentIdx);
if (commentedOutText.indexOf(';') !== -1) {
errors.push({
line,
i: commentIdx,
message: `Don't use semicolons in comments!`,
});
}
}
const result = [];
let canExtend = false;
for (let i = 0; i < val.length; i += 1) {
const ch = val[i];
if (ch.match(/\s/)) {
canExtend = false;
continue;
}
if (!canExtend || !(ch.match(/\w/))) {
result.push({
line,
i,
val: '',
});
}
result[result.length - 1].val += ch;
canExtend = ch.match(/\w/);
}
return result;
});
console.debug(tokens);
for (let i = 0; i < tokens.length; i += 1) {
const cur = tokens[i].val, next = (tokens[i + 1] && tokens[i + 1].val) || '';
if (cur === ',') {
const forbidden = ['FROM', 'SELECT', 'ORDER', 'WHERE', 'GROUP'];
if (forbidden.indexOf(next.toUpperCase()) !== -1) {
errors.push({
line: tokens[i].line,
i: tokens[i].i,
message: `Comma followed by ${next}`,
});
}
if (!next) {
errors.push({
line: tokens[i].line,
i: tokens[i].i,
message: `Trailing Comma at end of program`,
});
}
}
}
}
return errors;
// Your code here...
};
const run = async () => {
QA('#lint-style').forEach(x => x.remove());
QA('.errorIndicator').forEach(x => x.remove());
QA('.toolTip').forEach(x => x.remove());
const lintStyle = document.createElement('style');
document.head.append(lintStyle);
lintStyle.id = 'lint-style';
lintStyle.innerHTML = `
.toolTip {
z-index: 12;
border: 1px solid red;
color: black;
border-radius: 5px;
background-color: #eee;
position: absolute;
padding: 8px;
opacity: 0;
transition: opacity .3s ease-in;
max-width: 200px;
max-height: 300px;
overflow-y: auto;
pointer-events: none;
}
.toolTip.active {
opacity: 1;
}
.errorIndicator {
border-radius: 8px;
position: absolute;
left: 8px;
background-color: red;
width: 10px;
height: 10px;
border-radius: 5px;
}
`;
const getGutterContainer = () => {
return Q('.ace_gutter-layer');
};
const getGutterHeight = () => {
const s = Q('.ace_gutter-cell').style.height;
return parseFloat(s.substring(0, s.length - 2));
};
const errors = await analyze();
console.error(`Found ${errors.length} errors`);
for (const error of errors) {
console.error(`${error.message} at ${error.line}:${error.i}`);
const errorIndicator = document.createElement('div');
const gutterContainer = getGutterContainer();
if (!gutterContainer) continue;
const height = getGutterHeight();
errorIndicator.style.top = `${(error.line - 1) * height + 4}px`;
gutterContainer.appendChild(errorIndicator);
errorIndicator.className = 'errorIndicator';
const rect = errorIndicator.getBoundingClientRect();
const toolTip = document.createElement('div');
document.body.appendChild(toolTip);
toolTip.className = 'toolTip';
toolTip.textContent = error.message;
errorIndicator.addEventListener('mouseenter', () => {
toolTip.classList.add('active');
const selfRect = toolTip.getBoundingClientRect();
toolTip.style.left = `${rect.left - selfRect.width - 5}px`;
toolTip.style.top = `${rect.top + rect.height / 2 - selfRect.height / 2}px`;
});
errorIndicator.addEventListener('mouseleave', () => {
setTimeout(() => {
toolTip.classList.remove('active');
}, 200);
});
}
const textLayer = Q('.ace_text-layer');
};
const autoRunOnEdit = () => {
let previousSource = '';
const go = async () => {
const textLayer = Q('.ace_text-layer');
if (!textLayer) return;
const rawSource = getSource();
if (previousSource !== rawSource) {
previousSource = rawSource;
console.debug('linting...');
await run();
} else {
console.debug('no edits since last check');
}
};
const loop = async () => {
const timePromise = Promise.delay(1000);
await go();
await timePromise;
loop();
};
loop();
}
autoRunOnEdit();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment