Last active
March 28, 2020 04:38
-
-
Save stevenhao/9e2f8d511de18e8478178f3ea6745546 to your computer and use it in GitHub Desktop.
Lint prestodb sql in mode analytic's web editor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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