import * as ts from 'typescript' ; import { dealias , debugLog , isAllowlistedNamedDeclaration , isInStockLibraries } from './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 extends Foo { 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.
 *
 * By default, the matcher won't match symbols in import statements if the
 * symbol is not renamed. Machers can be optionally configured symbols in import
 * statements even if it's not a named import.
 */ export class AbsoluteMatcher { /**
     * From a "path/to/file.ts|foo.bar.baz", builds a Matcher.
     */ readonly filePath : string ; readonly bannedName : string ; constructor ( spec , readonly matchImport = false ) { if ( ! spec . match ( ABSOLUTE_RE ) ) { throw new Error ( 'Malformed matcher selector.' ) ; } 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.' ) ; } [ this . filePath , this . bannedName ] = spec . split ( '|' , 2 ) ; if ( this . filePath === CLOSURE ) { this . bannedName = CLUTZ_SYM_PREFIX + this . bannedName ; } } matches ( n , tc ) { const p = n . parent ; debugLog ( ( ) => `start matching ${ n . getText ( ) } in ${ p ?. getText ( ) }` ) ; if ( p !== undefined ) { if ( isAllowlistedNamedDeclaration ( p ) && p . name === n ) { if ( ! this . matchImport || ! ts . isImportSpecifier ( p ) ) { return false ; } } } const s = dealias ( tc . getSymbolAtLocation ( n ) , tc ) ; if ( ! s ) { debugLog ( ( ) => `cannot get symbol` ) ; return this . filePath === GLOBAL && matchGoogGlobal ( n , this . bannedName , tc ) ; } const fqn = tc . getFullyQualifiedName ( s ) ; debugLog ( ( ) => `got FQN ${ fqn }` ) ; if ( ! fqn . endsWith ( '".' + this . bannedName ) && fqn !== this . bannedName ) { debugLog ( ( ) => `FQN ${ fqn } doesn't match name ${ this . bannedName }` ) ; return false ; } if ( this . filePath === ANY_SYMBOL || this . filePath === CLOSURE ) { return true ; } const declarations = s . getDeclarations ( ) ; if ( declarations === undefined ) { return this . filePath === GLOBAL ; } if ( ! fqn . startsWith ( '"' ) ) { if ( this . filePath === GLOBAL ) { return declarations . some ( isInStockLibraries ) ; } else { return declarations . some ( ( d ) => { const srcFilePath = d . getSourceFile ( ) ?. 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 ; } } } function matchGoogGlobal ( n , bannedName , tc ) { if ( n . parent === undefined ) return false ; let accessExpr = n . parent ; const ids = bannedName . split ( '.' ) . reverse ( ) ; for ( const id of ids ) { let memberName ; if ( ts . isPropertyAccessExpression ( accessExpr ) ) { memberName = accessExpr . name . text ; accessExpr = accessExpr . expression ; } else if ( ts . isElementAccessExpression ( accessExpr ) ) { const argType = tc . getTypeAtLocation ( accessExpr . argumentExpression ) ; if ( argType . isStringLiteral ( ) ) { memberName = argType . value ; } else { return false ; } accessExpr = accessExpr . expression ; } else { return false ; } if ( id !== memberName ) return false ; } const s = dealias ( tc . getSymbolAtLocation ( accessExpr ) , tc ) ; if ( s === undefined ) return false ; return tc . getFullyQualifiedName ( s ) === 'goog.global' ; }