Created
July 25, 2019 22:41
-
-
Save bmeck/afb5e03f6e80a655dd93b829b50da53b to your computer and use it in GitHub Desktop.
This file contains 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
#! /usr/bin/env node | |
// get file slim.code.js.lz4 from https://github.com/nodejs/Gzemnid/blob/master/doc/Using_pre-built_datasets.md | |
// lz4cat < slim.code.js.lz4 | node check.js | |
'use strict'; | |
;(async () => { | |
const rl=require("readline").createInterface({ | |
input: process.stdin, | |
crlfDelay: Infinity | |
}); | |
let current_file = null; | |
let body = ''; | |
function flush(current_file, body) { | |
if (current_file === null) return; | |
if (body.startsWith('#!')) body = `//${body.slice(2)}`; | |
let ast = null; | |
const errors = []; | |
try { | |
ast = require('esprima').parseScript(body, { | |
loc: true, | |
jsx: true | |
}); | |
} catch (e) { | |
errors.push(e); | |
} | |
if (!ast) try { | |
ast = require('esprima').parseModule(body, { | |
loc: true, | |
jsx: true, | |
tolerant: true, | |
}); | |
} catch (e) { | |
errors.push(e); | |
} | |
if (!ast) { | |
// console.error( | |
// current_file, 'did not parse' | |
// ); | |
} | |
else { | |
const result = checkAST(current_file, ast); | |
if (result && result.length) console.log(JSON.stringify({ | |
file: current_file, | |
result: result.map(r => { | |
const lines = body.split(/\r?\n|\r/g).slice(r.start.line - 1, r.end.line); | |
if (r.start.line === r.end.line) { | |
lines[0] = lines[0].slice(r.start.col, r.end.col); | |
} else { | |
lines[0] = lines[0].slice(r.start.col); | |
lines[lines.length - 1] = lines[lines.length - 1].slice(0, r.end.col); | |
} | |
r.text = lines.join('\n'); | |
return r; | |
}), | |
})); | |
} | |
} | |
for await (let line of rl) { | |
let match = /^([^:]+):([^:]+)?:([\s\S]*)/.exec(line); | |
if (!match) { | |
console.error({line}, 'failed to parse'); | |
continue; | |
} | |
let [ , file, line_num, txt ] = match; | |
if (current_file === file) { | |
body += `\n${txt}`; | |
} else { | |
flush(current_file, body); | |
body = txt; | |
current_file = file; | |
} | |
} | |
flush(current_file, body); | |
})(); | |
function checkAST(file, ast) { | |
const updates = []; | |
function isFalseyConstant(node) { | |
if (node.type !== 'Literal') return false; | |
if (node.value) return false; | |
return true; | |
} | |
function isTruthyConstant(node) { | |
if (node.type !== 'Literal') return false; | |
if (node.value) return true; | |
return false; | |
} | |
function isTrueLiteral(node) { | |
return node.type === 'Literal' && node.value === true; | |
} | |
function isFalseLiteral(node) { | |
return node.type === 'Literal' && node.value === false; | |
} | |
function isSame(a, b) { | |
if (a.type !== b.type) return false; | |
if (a.type === 'Literal') return a.value === b.value; | |
if (a.type === 'Identifier') return a.name === b.name; | |
if (a.type === 'MemberExpression') { | |
return a.computed === b.computed && | |
isSame(a.object, b.object) && | |
isSame(a.property, b.property); | |
} | |
return false; | |
} | |
function unwrapNested(expr) { | |
const double = getFalseTestingSubExpression(expr); | |
if (!double) return expr; | |
const triple = getFalseTestingSubExpression(double); | |
if (triple) return triple; | |
return expr; | |
} | |
function getFalseTestingSubExpression(expr) { | |
if (expr.type === 'UnaryExpression') { | |
if (expr.operator === '!') { | |
return unwrapNested(expr.argument); | |
} | |
} | |
if (expr.type === 'BinaryExpression') { | |
if (expr.operator === '==') { | |
if (isFalseyConstant(expr.left)) { | |
return unwrapNested(expr.right); | |
} else if (isFalseyConstant(expr.right)) { | |
return unwrapNested(expr.left); | |
} | |
} | |
if (expr.operator === '===') { | |
if (isFalseLiteral(expr.left)) { | |
return unwrapNested(expr.right); | |
} else if (isFalseLiteral(expr.right)) { | |
return unwrapNested(expr.left); | |
} | |
} | |
if (expr.operator === '!=') { | |
if (isTruthyConstant(expr.left)) { | |
return unwrapNested(expr.right); | |
} else if (isTruthyConstant(expr.right)) { | |
return unwrapNested(expr.left); | |
} | |
} | |
if (expr.operator === '!==') { | |
if (isTrueLiteral(expr.left)) { | |
return unwrapNested(expr.right); | |
} else if (isTrueLiteral(expr.right)) { | |
return unwrapNested(expr.left); | |
} | |
} | |
} | |
return null; | |
} | |
function isSet(node) { | |
if (node.type !== 'CallExpression') return false; | |
if (node.callee.type !== 'MemberExpression') return false; | |
if (node.callee.computed) return false; | |
if (node.callee.property.name !== 'set') return false; | |
return true; | |
} | |
function isUpdate(test, consequent) { | |
const call = getFalseTestingSubExpression(test); | |
if (!call) return false; | |
if (call.type !== 'CallExpression') return false; | |
if (call.callee.type !== 'MemberExpression') return false; | |
if (call.callee.computed) return false; | |
if (call.callee.property.name !== 'has') return false; | |
let obj = call.callee.object; | |
let key = call.arguments[0]; | |
if (consequent.type === 'ExpressionStatement') consequent = consequent.expression; | |
let sets = []; | |
if (consequent.type === 'BlockStatement') { | |
if (consequent.body.length !== 1) return false; | |
for (const node of consequent.body) { | |
if (isSet(node)) { | |
sets.push(node); | |
} | |
} | |
} else { | |
if (isSet(consequent)) sets.push(consequent); | |
} | |
for (const set of sets) { | |
if (!isSame(obj, set.callee.object)) continue; | |
if (isSame(key, set.arguments[0])) return true; | |
} | |
return false; | |
} | |
function walk(node) { | |
if (typeof node !== 'object' || node === null) return; | |
if (Array.isArray(node)) { | |
for (const _ of node) walk(_); | |
return; | |
} | |
if (typeof node.type !== 'string') return; | |
function push(node) { | |
updates.push({ | |
start: { | |
line: node.loc.start.line, | |
col: node.loc.start.column, | |
}, | |
end: { | |
line: node.loc.end.line, | |
col: node.loc.end.column, | |
}, | |
check_id: 'map_update', | |
path: file, | |
}); | |
} | |
if (node.type === 'IfStatement') { | |
if (isUpdate(node.test, node.consequent)) { | |
push(node); | |
} | |
} | |
if (node.type === 'ConditionalExpression') { | |
if (isUpdate(node.test, node.consequent)) { | |
push(node); | |
} | |
} | |
if (node.type === 'LogicalExpression' && node.operator === '||') { | |
if (isUpdate(node.left, node.right)) { | |
push(node); | |
} | |
} | |
for (const [k, v] of Object.entries(node)) { | |
if (typeof v === 'object' && v !== null) { | |
walk(v); | |
} | |
} | |
} | |
walk(ast); | |
return updates; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment