Created
November 23, 2018 06:53
-
-
Save stevenhao/8d8885f903c740b4edfad256bf048d40 to your computer and use it in GitHub Desktop.
presto lint
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 | |
// @namespace http://tampermonkey.net/ | |
// @version 0.1 | |
// @description modeanalytics.com userscript to autolint sql code | |
// @author Steven Hao | |
// @match https://modeanalytics.com/editor/* | |
// @grant none | |
// ==/UserScript== | |
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 Q = (...args) => document.querySelector(...args); | |
const QA = (...args) => document.querySelectorAll(...args); | |
const analyze = () => { | |
const textLayer = Q('.ace_text-layer'); | |
const rawSource = Array.from(textLayer.childNodes).map((x, i) => ({ | |
line: i + 1, | |
val: x.textContent, | |
})); | |
const errors = []; | |
const tokens = flatMap(rawSource, ({ line, val }) => { | |
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 = () => { | |
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; | |
} | |
.toolTip:hover, .toolTip.active { | |
opacity: 1; | |
} | |
.errorIndicator { | |
border-radius: 8px; | |
position: absolute; | |
top: 4px; | |
left: 4px; | |
background-color: red; | |
width: 10px; | |
height: 10px; | |
border-radius: 5px; | |
} | |
`; | |
const gutters = QA('.ace_gutter-cell'); | |
const errors = 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'); | |
gutters[error.line - 1].appendChild(errorIndicator); | |
errorIndicator.className = 'errorIndicator'; | |
const rect = errorIndicator.getBoundingClientRect(); | |
const toolTip = document.createElement('div'); | |
document.body.appendChild(toolTip); | |
toolTip.className = 'toolTip'; | |
toolTip.innerHTML = 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 Q = (...args) => document.querySelector(...args); | |
const textLayer = Q('.ace_text-layer'); | |
}; | |
const autoRunOnEdit = () => { | |
let previousSource = ''; | |
const go = () => { | |
const textLayer = Q('.ace_text-layer'); | |
if (!textLayer) return; | |
const rawSource = Array.from(textLayer.childNodes).map(x => x.textContent).join('\n'); | |
if (previousSource !== rawSource) { | |
previousSource = rawSource; | |
console.debug('linting...'); | |
run(); | |
} else { | |
console.debug('no edits since last check'); | |
} | |
}; | |
setInterval(go, 500); | |
go(); | |
} | |
autoRunOnEdit(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment