Skip to content

Instantly share code, notes, and snippets.

@bmeck
Created July 25, 2019 22:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bmeck/afb5e03f6e80a655dd93b829b50da53b to your computer and use it in GitHub Desktop.
Save bmeck/afb5e03f6e80a655dd93b829b50da53b to your computer and use it in GitHub Desktop.
#! /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