UNPKG

ecmarkup

Version:

Custom element definitions and core utilities for markup that specifies ECMAScript and related technologies.

343 lines (342 loc) 15.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.checkVariableUsage = checkVariableUsage; const expr_parser_1 = require("../../expr-parser"); const utils_1 = require("../../utils"); class Scope { constructor(report) { this.vars = new Map(); this.strictScopes = [new Set()]; this.report = report; // TODO remove this when regex state objects become less dumb for (const name of ['captures', 'input', 'startIndex', 'endIndex']) { this.declare(name, null, undefined, true); } } declared(name) { return this.vars.has(name); } // only call this for variables previously checked to be declared used(name) { return this.vars.get(name).used; } declare(name, nameNode, kind = 'variable', mayBeShadowed = false) { if (this.declared(name)) { for (const scope of this.strictScopes) { if (scope.has(name)) { this.report({ ruleId: 're-declaration', message: `${JSON.stringify(name)} is already declared`, line: nameNode.location.start.line, column: nameNode.location.start.column, }); return; } } } else { this.vars.set(name, { kind, used: false, node: nameNode }); } if (!mayBeShadowed) { this.strictScopes[this.strictScopes.length - 1].add(name); } } undeclare(name) { this.vars.delete(name); } use(use) { const name = use.contents; if (this.declared(name)) { this.vars.get(name).used = true; } else { this.report({ ruleId: 'use-before-def', message: `could not find a preceding declaration for ${JSON.stringify(name)}`, line: use.location.start.line, column: use.location.start.column, }); } } } function checkVariableUsage(algorithmSource, containingAlgorithm, steps, parsed, report) { if (containingAlgorithm.hasAttribute('replaces-step')) { // TODO someday lint these by doing the rewrite (conceptually) return; } const scope = new Scope(report); let parentClause = containingAlgorithm.parentElement; while (parentClause != null) { if (parentClause.nodeName === 'EMU-CLAUSE') { break; } if (parentClause.nodeName === 'EMU-ANNEX') { // Annex B adds algorithms in a way which makes it hard to track the original // TODO someday lint Annex B return; } parentClause = parentClause.parentElement; } // we assume any name introduced earlier in the clause is fair game // this is a little permissive, but it's hard to find a precise rule, and that's better than being too restrictive let preceding = previousOrParent(containingAlgorithm, parentClause); while (preceding != null) { if (preceding.tagName !== 'EMU-ALG' && preceding.querySelector('emu-alg') == null && preceding.textContent != null) { // `__` is for <del>_x_</del><ins>_y_</ins>, which has textContent `_x__y_` for (const name of preceding.textContent.matchAll(/(?<=\b|_)_([a-zA-Z0-9]+)_(?=\b|_)/g)) { scope.declare(name[1], null, undefined, true); } } preceding = previousOrParent(preceding, parentClause); } walkAlgorithm(algorithmSource, steps, parsed, scope, report); for (const [name, { kind, used, node }] of scope.vars) { if (!used && node != null && kind !== 'parameter' && kind !== 'abstract closure parameter') { const message = `${JSON.stringify(name)} is declared here, but never referred to`; report({ ruleId: 'unused-declaration', message, line: node.location.start.line, column: node.location.start.column, }); } } } function walkAlgorithm(algorithmSource, steps, parsed, scope, report) { if (steps.name === 'ul') { // unordered lists can refer to variables, but only that for (const step of steps.contents) { const parts = step.contents; for (let i = 0; i < parts.length; ++i) { const part = parts[i]; if (part.name === 'underscore') { scope.use(part); } } if (step.sublist != null) { walkAlgorithm(algorithmSource, step.sublist, parsed, scope, report); } } return; } scope.strictScopes.push(new Set()); stepLoop: for (const step of steps.contents) { const loopVars = new Set(); const declaredThisLine = new Set(); const expr = parsed.get(step); const first = expr.items[0]; const last = expr.items[expr.items.length - 1]; // handle [declared="foo"] attributes const extraDeclarations = step.attrs.find(d => d.key === 'declared'); if (extraDeclarations != null) { for (let name of extraDeclarations.value.split(',')) { name = name.trim(); const line = extraDeclarations.location.start.line; const column = extraDeclarations.location.start.column + extraDeclarations.key.length + 2 + // '="' findDeclaredAttrOffset(extraDeclarations.value, name); if (scope.declared(name)) { const message = `${JSON.stringify(name)} is already declared and does not need an explict annotation`; report({ ruleId: 'unnecessary-declared-var', message, line, column, }); } else { scope.declare(name, { location: { start: { line, column } } }, 'attribute declaration', true); } } } // handle loops if ((first === null || first === void 0 ? void 0 : first.name) === 'text' && first.contents.startsWith('For each ')) { let loopVar = expr.items[1]; if ((loopVar === null || loopVar === void 0 ? void 0 : loopVar.name) === 'pipe' || (loopVar === null || loopVar === void 0 ? void 0 : loopVar.name) === 'record-spec') { loopVar = expr.items[3]; // 2 is the space } if (isVariable(loopVar)) { loopVars.add(loopVar); scope.declare(loopVar.contents, loopVar, 'loop variable'); } } // handle abstract closures if ((last === null || last === void 0 ? void 0 : last.name) === 'text' && / performs the following steps (atomically )?when called:$/.test(last.contents)) { if (first.name === 'text' && first.contents === 'Let ' && isVariable(expr.items[1])) { const closureName = expr.items[1]; scope.declare(closureName.contents, closureName); } // everything in an AC needs to be captured explicitly const acScope = new Scope(report); const paramsIndex = expr.items.findIndex(p => p.name === 'text' && p.contents.endsWith(' with parameters ')); if (paramsIndex !== -1 && paramsIndex < expr.items.length - 1) { const paramList = expr.items[paramsIndex + 1]; if (paramList.name !== 'paren') { report({ ruleId: 'bad-ac', message: `expected to find a parenthesized list of parameter names here`, ...(0, utils_1.offsetToLineAndColumn)(algorithmSource, paramList.location.start.offset), }); continue; } // TODO this kind of parsing should really be factored out let varName = true; for (let i = 0; i < paramList.items.length; ++i) { const item = paramList.items[i]; if (varName) { if (item.name === 'text' && item.contents === '...') { if (i < paramList.items.length - 2) { report({ ruleId: 'bad-ac', message: `expected rest param to come last`, ...(0, utils_1.offsetToLineAndColumn)(algorithmSource, item.location.start.offset), }); continue stepLoop; } continue; } if (item.name !== 'underscore') { report({ ruleId: 'bad-ac', message: `expected to find a parameter name here`, ...(0, utils_1.offsetToLineAndColumn)(algorithmSource, item.location.start.offset), }); continue stepLoop; } acScope.declare(item.contents, item, 'abstract closure parameter'); } else { if (item.name !== 'text' || item.contents !== ', ') { report({ ruleId: 'bad-ac', message: `expected to find ", " here`, ...(0, utils_1.offsetToLineAndColumn)(algorithmSource, item.location.start.offset), }); continue stepLoop; } } varName = !varName; } } let capturesIndex = expr.items.findIndex(p => p.name === 'text' && p.contents.endsWith(' that captures ')); if (capturesIndex !== -1) { for (; capturesIndex < expr.items.length; ++capturesIndex) { const v = expr.items[capturesIndex]; if (v.name === 'text' && v.contents.includes(' and performs ')) { break; } if (isVariable(v)) { const name = v.contents; scope.use(v); acScope.declare(name, v, 'abstract closure capture'); } } } // we have a lint rule elsewhere which checks there are substeps for closures, but we can't guarantee that rule hasn't tripped this run, so we still need to guard if (step.sublist != null && step.sublist.name === 'ol') { walkAlgorithm(algorithmSource, step.sublist, parsed, acScope, report); for (const [name, { node, kind, used }] of acScope.vars) { if (kind === 'abstract closure capture' && !used) { report({ ruleId: 'unused-capture', message: `closure captures ${JSON.stringify(name)}, but never uses it`, line: node.location.start.line, column: node.location.start.column, }); } } } continue; } // handle let/such that/there exists declarations for (let i = 1; i < expr.items.length; ++i) { const part = expr.items[i]; if (isVariable(part) && !loopVars.has(part)) { const varName = part.contents; // check for "there exists" const prev = expr.items[i - 1]; if (prev.name === 'text' && // prettier-ignore /\b(?:for any |for some |there exists |there is |there does not exist )((?!of |in )(\w+ ))*$/.test(prev.contents)) { scope.declare(varName, part); declaredThisLine.add(part); continue; } // check for "Let _x_ be" / "_x_ and _y_ such that" if (i < expr.items.length - 1) { const next = expr.items[i + 1]; const isSuchThat = next.name === 'text' && next.contents.startsWith(' such that '); const isBe = next.name === 'text' && next.contents.startsWith(' be '); if (isSuchThat || isBe) { const varsDeclaredHere = [part]; let varIndex = i - 1; // walk backwards collecting this list of variables separated by comma/'and' for (; varIndex >= 1; varIndex -= 2) { if (expr.items[varIndex].name !== 'text') { break; } const sep = expr.items[varIndex].contents; if (![', ', ', and ', ' and '].includes(sep)) { break; } const prev = expr.items[varIndex - 1]; if (!isVariable(prev)) { break; } varsDeclaredHere.push(prev); } const cur = expr.items[varIndex]; if ( // "of"/"in" guard is to distinguish "an integer X such that" from "an integer X in Y such that" - latter should not declare Y (isSuchThat && cur.name === 'text' && !/(?: of | in )/.test(cur.contents)) || (isBe && cur.name === 'text' && /\blet (?:each of )?$/i.test(cur.contents))) { const conditional = expr.items[0].name === 'text' && /^(If|Else|Otherwise)\b/.test(expr.items[0].contents); for (const v of varsDeclaredHere) { scope.declare(v.contents, v, 'variable', conditional); declaredThisLine.add(v); } } continue; } } } } // handle uses (0, expr_parser_1.walk)(v => { if (v.name === 'underscore' && !loopVars.has(v) && !declaredThisLine.has(v)) { scope.use(v); } }, expr); if (step.sublist != null) { walkAlgorithm(algorithmSource, step.sublist, parsed, scope, report); } for (const decl of loopVars) { scope.undeclare(decl.contents); } } scope.strictScopes.pop(); } function isVariable(node) { return (node === null || node === void 0 ? void 0 : node.name) === 'underscore'; } function previousOrParent(element, stopAt) { if (element === stopAt) { return null; } if (element.previousElementSibling != null) { return element.previousElementSibling; } if (element.parentElement == null) { return null; } return previousOrParent(element.parentElement, stopAt); } function findDeclaredAttrOffset(attrSource, name) { const matcher = new RegExp('\\b' + name.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + '\\b'); // regexp.escape when return attrSource.match(matcher).index; }