Add a BANNED_NAME engine to Tsetse's ConformancePatternRule. PiperOrigin-RevId: 252610371
diff --git a/internal/tsetse/rules/conformance_pattern_rule.ts b/internal/tsetse/rules/conformance_pattern_rule.ts index b683355..add5286 100644 --- a/internal/tsetse/rules/conformance_pattern_rule.ts +++ b/internal/tsetse/rules/conformance_pattern_rule.ts
@@ -3,6 +3,7 @@ import {AbstractRule} from '../rule'; import {Fixer} from '../util/fixer'; import {Config, MatchedNodeTypes, PatternKind} from '../util/pattern_config'; +import {NameEngine} from '../util/pattern_engines/name_engine'; import {PatternEngine} from '../util/pattern_engines/pattern_engine'; import {PropertyNonConstantWriteEngine} from '../util/pattern_engines/property_non_constant_write_engine'; import {PropertyWriteEngine} from '../util/pattern_engines/property_write_engine'; @@ -36,6 +37,10 @@ config as Config<PatternKind.BANNED_PROPERTY_NON_CONSTANT_WRITE>, fixer); break; + case PatternKind.BANNED_NAME: + engine = + new NameEngine(config as Config<PatternKind.BANNED_NAME>, fixer); + break; default: throw new Error('Config type not recognized, or not implemented yet.'); }
diff --git a/internal/tsetse/tests/ban_conformance_pattern/name_test.ts b/internal/tsetse/tests/ban_conformance_pattern/name_test.ts new file mode 100644 index 0000000..3165d38 --- /dev/null +++ b/internal/tsetse/tests/ban_conformance_pattern/name_test.ts
@@ -0,0 +1,45 @@ +import 'jasmine'; +import {ConformancePatternRule, PatternKind} from '../../rules/conformance_pattern_rule'; +import {compileAndCheck, customMatchers} from '../../util/testing/test_support'; + +describe('BANNED_NAME', () => { + it('matches simple example of globals', () => { + const rule = new ConformancePatternRule({ + errorMessage: 'no Infinity', + kind: PatternKind.BANNED_NAME, + values: ['Infinity'] + }); + const source = [ + `Infinity; 1+1;`, + ].join('\n'); + const results = compileAndCheck(rule, source); + + expect(results.length).toBe(1); + expect(results[0]).toBeFailureMatching({ + matchedCode: `Infinity`, + errorMessage: 'no Infinity' + }); + }); + + it('matches namespaced globals', () => { + const rule = new ConformancePatternRule({ + errorMessage: 'no blob url', + kind: PatternKind.BANNED_NAME, + values: ['URL.createObjectURL'] + }); + const source = [ + `URL.createObjectURL({});`, + ].join('\n'); + const results = compileAndCheck(rule, source); + + expect(results.length).toBe(1); + expect(results[0]).toBeFailureMatching({ + matchedCode: `createObjectURL`, + errorMessage: 'no blob url' + }); + }); +}); + +beforeEach(() => { + jasmine.addMatchers(customMatchers); +});
diff --git a/internal/tsetse/util/match_symbol.ts b/internal/tsetse/util/match_symbol.ts index 5df53b1..c60195b 100644 --- a/internal/tsetse/util/match_symbol.ts +++ b/internal/tsetse/util/match_symbol.ts
@@ -11,6 +11,10 @@ /** * 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. */ export class AbsoluteMatcher { /**
diff --git a/internal/tsetse/util/pattern_config.ts b/internal/tsetse/util/pattern_config.ts index c8c0927..9ec75f7 100644 --- a/internal/tsetse/util/pattern_config.ts +++ b/internal/tsetse/util/pattern_config.ts
@@ -6,6 +6,7 @@ * https://github.com/google/closure-compiler/wiki/JS-Conformance-Framework) */ export enum PatternKind { + BANNED_NAME = 'banned-name', BANNED_PROPERTY_WRITE = 'banned-property-write', BANNED_PROPERTY_NON_CONSTANT_WRITE = 'banned-property-non-constant-write' } @@ -87,4 +88,5 @@ [PatternKind.BANNED_PROPERTY_NON_CONSTANT_WRITE]: ts.BinaryExpression&{ left: ts.PropertyAccessExpression; }; + [PatternKind.BANNED_NAME]: ts.Identifier; }
diff --git a/internal/tsetse/util/pattern_engines/name_engine.ts b/internal/tsetse/util/pattern_engines/name_engine.ts new file mode 100644 index 0000000..332c6c6 --- /dev/null +++ b/internal/tsetse/util/pattern_engines/name_engine.ts
@@ -0,0 +1,43 @@ +import * as ts from 'typescript'; +import {Checker} from '../../checker'; +import {ErrorCode} from '../../error_code'; +import {debugLog, shouldExamineNode} from '../ast_tools'; +import {Fixer} from '../fixer'; +import {AbsoluteMatcher} from '../match_symbol'; +import {Config, MatchedNodeTypes, PatternKind} from '../pattern_config'; +import {PatternEngine} from './pattern_engine'; + +export class NameEngine extends PatternEngine<PatternKind.BANNED_NAME> { + private readonly matcher: AbsoluteMatcher; + constructor( + config: Config<PatternKind.BANNED_NAME>, + fixer?: Fixer<MatchedNodeTypes[PatternKind.BANNED_NAME]>) { + super(config, fixer); + // TODO: Support more than one single value here, or even build a + // multi-pattern engine. This would help for performance. + if (this.config.values.length !== 1) { + throw new Error(`BANNED_NAME expects one value, got(${ + this.config.values.join(',')})`); + } + this.matcher = new AbsoluteMatcher(this.config.values[0]); + } + + register(checker: Checker) { + checker.on( + ts.SyntaxKind.Identifier, this.checkAndFilterResults.bind(this), + ErrorCode.CONFORMANCE_PATTERN); + } + + check(tc: ts.TypeChecker, n: MatchedNodeTypes[PatternKind.BANNED_NAME]): + MatchedNodeTypes[PatternKind.BANNED_NAME]|undefined { + if (!shouldExamineNode(n) || n.getSourceFile().isDeclarationFile) { + return; + } + debugLog(`inspecting ${n.getText().trim()}`); + if (!this.matcher.matches(n, tc)) { + debugLog('Not the right global name.'); + return; + } + return n; + } +}