indentmon
Version:
Use to detect changes in indentation over several lines using Pythonic rules
303 lines (234 loc) • 8.01 kB
JavaScript
// Run `npm test` to run comprehensive tests (O(N^4))
// Use Node REPL (.load ./test.js) to test relevant subsets.
const {indentmonitor, IndentError} = require('.');
const LEADING = /^\s/;
// Create Array of N zeros
const narray = n => new Array(n).fill(0);
// Create array containing increasing integers in [s, n)
const range = (s,e) => narray(e-s).map((_,i) => s+i);
// Select integer in [0, max)
const rand = (max) => Math.floor(Math.random() * max);
const coinflip = () => Math.random() > 0.5;
// Select element in array
const select = A => A[rand(A.length)];
// Select an integer in [min, max]
const pickanumber = (min,max) => select(range(min, max + 1));
// Repeat function N times
const repeat = (n,fn) => narray(n).map(fn);
// Repeat string N times.
const repeatString = (n,str) => repeat(n, () => str).join('');
// Select element in array
const last = A => A[A.length - 1];
// Shuffle array in place.
function shuffle(a) {
for (let i = a.length; i; i--) {
const j = Math.floor(Math.random() * i);
[a[i - 1], a[j]] = [a[j], a[i - 1]];
}
return a;
}
// Generate string used to indent a line of code.
function indentString([nt, ns]) {
return shuffle(
repeat(nt, () => '\t').concat(
repeat(ns, () => ' '))).join('');
}
/*
A mechanism for indenting generated code. Pass one of the below
indenters to change the indent style.
*/
function indenter(indentStyleFn) {
const stack = [[0,0]];
return function(lvl) {
const bound = stack.length - 1;
if (lvl > bound) {
stack.push(indentStyleFn(last(stack)));
} else if (lvl < bound) {
stack.splice(lvl + 1);
}
return (stack.length > 0)
? indentString(last(stack))
: '';
};
}
/*
Indenters indent lines of code with a number or tabs or spaces on the
fly. Idealized indents go two spaces at a time, compliant indents
indent to the next level using a number of tabs OR spaces each level,
and non-compliant indents use tabs AND spaces on each level.
*/
function idealizedIndent([nt, ns]) {
return [nt, ns + 2];
}
function compliantIndent([nt, ns]) {
const amt = pickanumber(1, 8);
return (coinflip()) ? [nt + amt, ns] : [nt, ns + amt];
}
function nonCompliantIndent([nt, ns]) {
return [nt + 1, ns + 1];
}
/*
DSL interpreter to generate lines of text with various indendation
Use 'render' in the REPL to visualize the effects of this generator.
See README.
*/
function* gen(dsl, indentStyleFn) {
let code = '';
let lineno = 1;
let lvl = 0;
const indent = indenter(indentStyleFn);
for (const instruction of dsl) {
switch (instruction) {
case '>':
++lvl;
break;
case '<':
--lvl;
if (lvl < 0) {
throw new Error('Test DSL created negative indent');
}
break;
case '-':
break;
case '0': case '1': case '2':
case '3': case '4': case '5':
case '6': case '7': case '8': case '9':
const it = parseInt(instruction);
if (it > lvl && it-lvl > 1) {
throw new Error(
`DSL level ${it} far exceeds level ${lvl}`);
}
lvl = it;
break;
default:
throw new Error(
`Unknown test DSL instruction: ${instruction}`);
}
yield {
lineno,
expected_level: lvl,
code: `${indent(lvl)}${lineno}`
};
++lineno;
}
return code;
}
/*
DSL Production generators.
*/
function createProduction(len, fn) {
return '-' + narray(len).map(fn).join('');
}
function createConstantProduction(len) {
return createProduction(len, () => '-');
}
function createIncreasingProduction(len) {
return createProduction(len, () => '>');
}
function createNonDecreasingProduction(len) {
return createProduction(len, () => select('>-'));
}
function createAnchoredProduction(ndlen) {
const nondec = createNonDecreasingProduction(ndlen);
return nondec + nondec.replace(/>/g, '<');
}
function createNonMonotonicProduction(ndlen) {
const nondec = createNonDecreasingProduction(ndlen);
const nindents = (nondec.match(/>/g) || []).length;
const noninc = (nindents > 0)
? createProduction(nindents, ()=>select('<-'))
: '';
return nondec + noninc;
}
function createDropOffProduction(ilen) {
const inc = createIncreasingProduction(ilen);
let tail = '';
let nindents = ilen;
while (nindents > 0) {
nindents = Math.min(9, Math.floor(nindents / 2));
tail += nindents;
}
return inc + tail;
}
/*
Visualize productions. Useful in Node REPL.
*/
function render(prod) {
console.log(
Array
.from(gen(prod, idealizedIndent))
.map(({code}) => code)
.join('\n'));
}
/*
Run every test case.
*/
function bruteforce(nfamilytrials = 10, minlen = 20) {
const start = Date.now();
try {
for (const [prodfactory, label] of [
[createConstantProduction, 'CT'],
[createIncreasingProduction, 'IN'],
[createNonDecreasingProduction, 'ND'],
[createNonMonotonicProduction, 'NM'],
[createAnchoredProduction, 'AN'],
[createDropOffProduction, 'DO'],
]) {
for (let ftrial = 0; ftrial < nfamilytrials; ++ftrial) {
const production = prodfactory(minlen);
for (const indentStyleFn of [idealizedIndent, compliantIndent]) {
const M = indentmonitor();
for (const spec of gen(production, indentStyleFn)) {
const {lineno, code, expected_level:el} = spec;
const [level, trimmed] = M(code);
if (trimmed.match(LEADING)) {
throw new Error(
`Leading space found in ${trimmed}`);
}
const num = parseInt(trimmed);
if (num !== lineno || isNaN(num)) {
throw new Error(
`Code "${num}" should be "${lineno}"`);
}
if (level !== el) {
throw new Error(
`Level ${level} should be ${el}`);
}
}
}
// Make sure the monitor catches non-compliant indents.
try {
const M = indentmonitor();
const iter = gen(production, nonCompliantIndent);
for (const {code} of iter) {
M(code);
}
} catch (e) {
if (!(e instanceof IndentError)) {
throw e;
}
}
const display = `${label}${ftrial}: ${production}`;
console.log((display.length) > 72
? display.substring(0, 67) + '[...]'
: display);
}
}
} catch (e) {
console.error(e);
}
console.log(`Done in ${(Date.now() - start)/1000}s.`)
}
function getarg(indexFromRear, def) {
const ordinal = process.argv.length - indexFromRear;
if (ordinal >= process.argv.length) {
return def;
}
const attempt = parseInt(process.argv[ordinal]);
return isNaN(attempt) ? def : attempt;
}
if (require.main === module) {
const minlen = getarg(1, 50);
const ntrial = getarg(2, 20);
bruteforce(minlen, ntrial);
}