UNPKG

indentmon

Version:

Use to detect changes in indentation over several lines using Pythonic rules

303 lines (234 loc) 8.01 kB
// 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); }