UNPKG

@bazel/typescript

Version:
166 lines (165 loc) 7.61 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const ast_tools_1 = require("./ast_tools"); const PATH_NAME_FORMAT = '[/\\.\\w\\d_-]+'; const JS_IDENTIFIER_FORMAT = '[\\w\\d_-]+'; const FQN_FORMAT = `(${JS_IDENTIFIER_FORMAT}\.)*${JS_IDENTIFIER_FORMAT}`; const GLOBAL = 'GLOBAL'; const ANY_SYMBOL = 'ANY_SYMBOL'; const CLOSURE = 'CLOSURE'; /** A fqn made out of a dot-separated chain of JS identifiers. */ const ABSOLUTE_RE = new RegExp(`^${PATH_NAME_FORMAT}\\|${FQN_FORMAT}$`); /** * Clutz glues js symbols to ts namespace by prepending "ಠ_ಠ.clutz.". * We need to include this prefix when the banned name is from Closure. */ const CLUTZ_SYM_PREFIX = 'ಠ_ಠ.clutz.'; /** * This class matches symbols given a "foo.bar.baz" name, where none of the * steps are instances of classes. * * Note that this isn't smart about subclasses and types: to write a check, we * strongly suggest finding the expected symbol in externs to find the object * name on which the symbol was initially defined. * * This matcher requires a scope for the symbol, which may be `GLOBAL`, * `ANY_SYMBOL`, `CLOSURE` or a file path filter. `CLOSURE` indicates that the * symbol is from the JS Closure library processed by clutz. The matcher begins * with this scope, then the separator "|", followed by the symbol name. For * example, "GLOBAL|eval". * * The file filter specifies * (part of) the path of the file in which the symbol of interest is defined. * For example, "path/to/file.ts|foo.bar.baz". * With this filter, only symbols named "foo.bar.baz" that are defined in a path * that contains "path/to/file.ts" are matched. * * This filter is useful when mutiple symbols have the same name but * you want to match with a specific one. For example, assume that there are * two classes named "Foo" defined in /path/to/file0 and /path/to/file1. * // in /path/to/file0 * export class Foo { static bar() {return "Foo.bar in file0";} } * * // in /path/to/file1 * export class Foo { static bar() {return "Foo.bar in file1";} } * * Suppose that these two classes are referenced in two other files. * // in /path/to/file2 * import {Foo} from /path/to/file0; * Foo.bar(); * * // in /path/to/file3 * import {Foo} from /path/to/file1; * Foo.bar(); * * An absolute matcher "Foo.bar" without a file filter will match with both * references to "Foo.bar" in /path/to/file2 and /path/to/file3. * An absolute matcher "/path/to/file1|Foo.bar", however, only matches with the * "Foo.bar()" in /path/to/file3 because that references the "Foo.bar" defined * in /path/to/file1. * * Note that an absolute matcher will match with any reference to the symbol * defined in the file(s) specified by the file filter. For example, assume that * Foo from file1 is extended in file4. * * // in /path/to/file4 * import {Foo} from /path/to/file1; * class Moo { static tar() {return "Moo.tar in file4";} } * Moo.bar(); * * An absolute matcher "/path/to/file1|Foo.bar" matches with "Moo.bar()" because * "bar" is defined as part of Foo in /path/to/file1. */ class AbsoluteMatcher { constructor(spec) { if (!spec.match(ABSOLUTE_RE)) { throw new Error('Malformed matcher selector.'); } // JSConformance used to use a Foo.prototype.bar syntax for bar on // instances of Foo. TS doesn't surface the prototype part in the FQN, and // so you can't tell static `bar` on `foo` from the `bar` property/method // on `foo`. To avoid any confusion, throw there if we see `prototype` in // the spec: that way, it's obvious that you're not trying to match // properties. if (spec.match('.prototype.')) { throw new Error('Your pattern includes a .prototype, but the AbsoluteMatcher is ' + 'meant for non-object matches. Use the PropertyMatcher instead, or ' + 'the Property-based PatternKinds.'); } // Split spec by the separator "|". [this.filePath, this.bannedName] = spec.split('|', 2); if (this.filePath === CLOSURE) { this.bannedName = CLUTZ_SYM_PREFIX + this.bannedName; } } matches(n, tc) { ast_tools_1.debugLog(() => `start matching ${n.getText()} in ${n.parent.getText()}`); // Check if the node is being declared. Declaration may be imported without // programmer being aware of. We should not alert them about that. // Since import statments are also declarations, this have two notable // consequences. // - Match is negative for imports without renaming // - Match is positive for imports with renaming, when the imported name // is the target. Since Tsetse is flow insensitive and we don't track // symbol aliases, the import statement is the only place we can match // bad symbols if they get renamed. if (ast_tools_1.isNameInDeclaration(n)) { ast_tools_1.debugLog(() => `We don't flag symbol declarations`); return false; } // Get the symbol (or the one at the other end of this alias) that we're // looking at. const s = ast_tools_1.dealias(tc.getSymbolAtLocation(n), tc); if (!s) { ast_tools_1.debugLog(() => `cannot get symbol`); return false; } // The TS-provided FQN tells us the full identifier, and the origin file // in some circumstances. const fqn = tc.getFullyQualifiedName(s); ast_tools_1.debugLog(() => `got FQN ${fqn}`); // Name-based check: `getFullyQualifiedName` returns `"filename".foo.bar` or // just `foo.bar` if the symbol is ambient. The check here should consider // both cases. if (!fqn.endsWith('".' + this.bannedName) && fqn !== this.bannedName) { ast_tools_1.debugLog(() => `FQN ${fqn} doesn't match name ${this.bannedName}`); return false; } // If `ANY_SYMBOL` or `CLOSURE` is specified, it's sufficient to conclude we // have a match. if (this.filePath === ANY_SYMBOL || this.filePath === CLOSURE) { return true; } // If there is no declaration, the symbol is a language built-in object. // This is a match only if `GLOBAL` is specified. const declarations = s.getDeclarations(); if (declarations === undefined) { return this.filePath === GLOBAL; } // No file info in the FQN means it's imported from a .d.ts declaration // file. This can be from a core library, a JS library, or an exported local // symbol defined in another TS target. We need to extract the name of the // declaration file. if (!fqn.startsWith('"')) { if (this.filePath === GLOBAL) { return declarations.some(ast_tools_1.isInStockLibraries); } else { return declarations.some((d) => { var _a; const srcFilePath = (_a = d.getSourceFile()) === null || _a === void 0 ? void 0 : _a.fileName; return srcFilePath && srcFilePath.match(this.filePath); }); } } else { const last = fqn.indexOf('"', 1); if (last === -1) { throw new Error('Malformed fully-qualified name.'); } const filePath = fqn.substring(1, last); return filePath.match(this.filePath) !== null; } } } exports.AbsoluteMatcher = AbsoluteMatcher;