Add a new matcher (nth argument of a call to foo is literal) to Tsetse.

This literalness constraint is useful as a proxy for "no user data should be in that argument", for instance for security purposes.

PiperOrigin-RevId: 261307072
diff --git a/internal/tsetse/rules/conformance_pattern_rule.ts b/internal/tsetse/rules/conformance_pattern_rule.ts
index 677490d..0cbfe71 100644
--- a/internal/tsetse/rules/conformance_pattern_rule.ts
+++ b/internal/tsetse/rules/conformance_pattern_rule.ts
@@ -3,11 +3,13 @@
 import {AbstractRule} from '../rule';
 import {Fixer} from '../util/fixer';
 import {Config, PatternKind} from '../util/pattern_config';
+import {CallNonConstantArgumentEngine} from '../util/pattern_engines/name_call_non_constant_argument';
 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';
 
+
 /**
  * Builds a Rule that matches a certain pattern, given as parameter, and
  * that can additionally run a suggested fix generator on the matches.
@@ -32,6 +34,9 @@
       case PatternKind.BANNED_NAME:
         this.engine = new NameEngine(config, fixer);
         break;
+      case PatternKind.BANNED_NAME_CALL_NON_CONSTANT_ARGUMENT:
+        this.engine = new CallNonConstantArgumentEngine(config, fixer);
+        break;
       default:
         throw new Error('Config type not recognized, or not implemented yet.');
     }
diff --git a/internal/tsetse/tests/ban_conformance_pattern/name_call_non_constant_argument_test.ts b/internal/tsetse/tests/ban_conformance_pattern/name_call_non_constant_argument_test.ts
new file mode 100644
index 0000000..44c9a31
--- /dev/null
+++ b/internal/tsetse/tests/ban_conformance_pattern/name_call_non_constant_argument_test.ts
@@ -0,0 +1,116 @@
+import 'jasmine';
+import {ConformancePatternRule, PatternKind} from '../../rules/conformance_pattern_rule';
+import {compileAndCheck, customMatchers} from '../../util/testing/test_support';
+
+describe('BANNED_NAME_CALL_NON_CONSTANT_ARGUMENT', () => {
+  const rule = new ConformancePatternRule({
+    errorMessage: 'do not call bar.foo with non-literal 1st arg',
+    kind: PatternKind.BANNED_NAME_CALL_NON_CONSTANT_ARGUMENT,
+    values: ['bar:0']
+  });
+
+  it('matches simple examples', () => {
+    const sources = [
+      `export function bar(x:any, y:any) {}`,
+      `import * as foo from './file_0'; ` +
+          `foo.bar(1, 1); foo.bar(window.name, 1);`,
+    ];
+    const results = compileAndCheck(rule, ...sources);
+
+    expect(results.length).toBe(1);
+    expect(results[0]).toBeFailureMatching({
+      matchedCode: `foo.bar(window.name, 1)`,
+      messageText: 'do not call bar.foo with non-literal 1st arg'
+    });
+  });
+
+  it('looks at the right position', () => {
+    const sources = [
+      `export function bar(x:any, y:any) {}`,
+      `import * as foo from './file_0'; foo.bar(1, window.name);`,
+    ];
+    const results = compileAndCheck(rule, ...sources);
+
+    expect(results.length).toBe(0);
+  });
+
+  it('looks at the right position', () => {
+    const rule = new ConformancePatternRule({
+      errorMessage: 'non-literal arg',
+      kind: PatternKind.BANNED_NAME_CALL_NON_CONSTANT_ARGUMENT,
+      values: ['aaa:1', 'bbb:0']
+    });
+
+    const sources = [
+      `export function aaa(x:any, y:any) {}; export function bbb(x:any) {}`,
+      `import * as foo from './file_0'; ` +
+          `foo.aaa(1, window.name); foo.bbb(window.name);`,
+    ];
+    const results = compileAndCheck(rule, ...sources);
+
+    expect(results.length).toBe(2);
+    expect(results[0]).toBeFailureMatching({
+      matchedCode: `foo.aaa(1, window.name)`,
+    });
+    expect(results[1]).toBeFailureMatching({
+      matchedCode: `foo.bbb(window.name)`,
+    });
+  });
+
+  it('supports static methods', () => {
+    const rule = new ConformancePatternRule({
+      errorMessage: 'non-literal arg',
+      kind: PatternKind.BANNED_NAME_CALL_NON_CONSTANT_ARGUMENT,
+      values: ['Car.buildFromParts:0']
+    });
+
+    const sources = [
+      `export class Car { static buildFromParts(name:string):void {}; }`,
+      `import {Car} from './file_0';\n` +
+          `Car.buildFromParts(window.name);\n` +
+          `Car.buildFromParts('hello');`,
+    ];
+    const results = compileAndCheck(rule, ...sources);
+
+    expect(results.length).toBe(1);
+    expect(results[0]).toBeFailureMatching({
+      matchedCode: `Car.buildFromParts(window.name)`,
+    });
+  });
+
+  it('supports ambient global methods', () => {
+    const rule = new ConformancePatternRule({
+      errorMessage: 'non-literal arg',
+      kind: PatternKind.BANNED_NAME_CALL_NON_CONSTANT_ARGUMENT,
+      values: ['URL.createObjectURL:0']
+    });
+
+    const sources = [`URL.createObjectURL(window.name);\n`];
+    const results = compileAndCheck(rule, ...sources);
+
+    expect(results.length).toBe(1);
+    expect(results[0]).toBeFailureMatching({
+      matchedCode: `URL.createObjectURL(window.name)`,
+    });
+  });
+
+  it('supports ambient global methods', () => {
+    const rule = new ConformancePatternRule({
+      errorMessage: 'non-literal arg',
+      kind: PatternKind.BANNED_NAME_CALL_NON_CONSTANT_ARGUMENT,
+      values: ['eval:0']
+    });
+
+    const sources = [`eval(window.name);\n`];
+    const results = compileAndCheck(rule, ...sources);
+
+    expect(results.length).toBe(1);
+    expect(results[0]).toBeFailureMatching({
+      matchedCode: `eval(window.name)`,
+    });
+  });
+});
+
+beforeEach(() => {
+  jasmine.addMatchers(customMatchers);
+});
diff --git a/internal/tsetse/util/match_symbol.ts b/internal/tsetse/util/match_symbol.ts
index c60195b..eb588e8 100644
--- a/internal/tsetse/util/match_symbol.ts
+++ b/internal/tsetse/util/match_symbol.ts
@@ -15,6 +15,11 @@
  * 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.
+ *
+ * TODO(rjamet): add a file-based optional filter, since FQNs tell you where
+ * your imported symbols were initially defined. That would let us be more
+ * specific in matches (say, you want to ban the fromLiteral in foo.ts but not
+ * the one from bar.ts).
  */
 export class AbsoluteMatcher {
   /**
@@ -32,10 +37,11 @@
     // 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 (this.bannedName.includes('.prototype')) {
+    if (this.bannedName.match('.prototype.')) {
       throw new Error(
           'Your pattern includes a .prototype, but the AbsoluteMatcher is ' +
-          'meant for non-object matches. Use the PropertyMatcher instead.');
+          'meant for non-object matches. Use the PropertyMatcher instead, or ' +
+          'the Property-based PatternKinds.');
     }
   }
 
diff --git a/internal/tsetse/util/pattern_config.ts b/internal/tsetse/util/pattern_config.ts
index 21e9c25..bc907b1 100644
--- a/internal/tsetse/util/pattern_config.ts
+++ b/internal/tsetse/util/pattern_config.ts
@@ -7,7 +7,9 @@
 export const enum PatternKind {
   BANNED_NAME = 'banned-name',
   BANNED_PROPERTY_WRITE = 'banned-property-write',
-  BANNED_PROPERTY_NON_CONSTANT_WRITE = 'banned-property-non-constant-write'
+  BANNED_PROPERTY_NON_CONSTANT_WRITE = 'banned-property-non-constant-write',
+  // Not from JSConformance.
+  BANNED_NAME_CALL_NON_CONSTANT_ARGUMENT = 'banned-call-non-constant-argument'
 }
 
 /**
@@ -19,8 +21,8 @@
   /**
    * Values have a pattern-specific syntax.
    *
-   * TODO(rjamet): We'll document them, but for now see each patternKind's tests
-   * for examples.
+   * TODO(rjamet): We'll document them, but for now see each patternKind's
+   * tests for examples.
    */
   values: string[];
 
@@ -32,13 +34,13 @@
 }
 
 /**
- * A whitelist entry, corresponding to a logical whitelisting rule. Use these to
- * distinguish between various logical reasons for whitelisting something: for
- * instance, tie these to particular bugs that needed whitelisting, per legacy
- * project, manually reviewed entries, and so on.
+ * A whitelist entry, corresponding to a logical whitelisting rule. Use these
+ * to distinguish between various logical reasons for whitelisting something:
+ * for instance, tie these to particular bugs that needed whitelisting, per
+ * legacy project, manually reviewed entries, and so on.
  *
- * Whitelists are based on the file paths provided by the TS compiler, with both
- * regexp-based checks and prefix-based checks.
+ * Whitelists are based on the file paths provided by the TS compiler, with
+ * both regexp-based checks and prefix-based checks.
  *
  *
  * Follows the logic in
diff --git a/internal/tsetse/util/pattern_engines/name_call_non_constant_argument.ts b/internal/tsetse/util/pattern_engines/name_call_non_constant_argument.ts
new file mode 100644
index 0000000..1af786d
--- /dev/null
+++ b/internal/tsetse/util/pattern_engines/name_call_non_constant_argument.ts
@@ -0,0 +1,86 @@
+import * as ts from 'typescript';
+import {Checker} from '../../checker';
+import {ErrorCode} from '../../error_code';
+import {debugLog} from '../ast_tools';
+import {Fixer} from '../fixer';
+import {isLiteral} from '../is_literal';
+import {AbsoluteMatcher} from '../match_symbol';
+import {Config} from '../pattern_config';
+import {PatternEngine} from './pattern_engine';
+
+/**
+ * The engine for BANNED_CALL_NON_CONSTANT_ARGUMENT.
+ *
+ * This takes any amount of (functionName, argument) position pairs, separated
+ * by a colon. The first part matches symbols that were defined on the global
+ * scope, and their fields, without going through a prototype chain.
+ *
+ * For instance, "URL.createObjectURL:0" will target any createObjectURL-named
+ * call on a URL-named object (like the ambient URL declared in lib.dom.d.ts),
+ * or "Car.buildFromParts:1" will match any buildFromParts reached from a
+ * Car-named symbol, including a hypothetical class with a static member
+ * function "buildFromParts" that lives in its own module.
+ */
+export class CallNonConstantArgumentEngine extends PatternEngine {
+  private readonly matchers: Array<[AbsoluteMatcher, number]> = [];
+
+  constructor(config: Config, fixer?: Fixer) {
+    super(config, fixer);
+    for (const v of config.values) {
+      const [matcherSpec, strPosition] = v.split(':', 2);
+      if (!matcherSpec || !strPosition.match('^\\d+$')) {
+        throw new Error('Couldn\'t parse values');
+      }
+      const position = Number(strPosition);
+      this.matchers.push([new AbsoluteMatcher(matcherSpec), position]);
+    }
+  }
+
+  register(checker: Checker) {
+    checker.on(
+        ts.SyntaxKind.CallExpression, this.checkAndFilterResults.bind(this),
+        ErrorCode.CONFORMANCE_PATTERN);
+  }
+
+  check(tc: ts.TypeChecker, n: ts.Node): ts.Node|undefined {
+    if (!ts.isCallExpression(n)) {
+      debugLog(`Should not happen: node is not a CallExpression`);
+      return;
+    }
+    debugLog(`inspecting ${n.getText().trim()}`);
+
+    /**
+     * Inspects a particular CallExpression to see if it calls the target
+     * function with a non-literal parameter in the target position. Returns
+     * that CallExpression if `n` matches the search, undefined otherwise.
+     */
+    function checkIndividual(
+        n: ts.CallExpression, m: [AbsoluteMatcher, number]): ts.CallExpression|
+        undefined {
+      if (!m[0].matches(n.expression, tc)) {
+        debugLog(`Wrong symbol, not ${m[0].bannedName}`);
+        return;
+      }
+      if (n.arguments.length < m[1]) {
+        debugLog(`Good symbol, not enough arguments to match (got ${
+            n.arguments.length}, want ${m[1]})`);
+        return;
+      }
+      if (isLiteral(tc, n.arguments[m[1]])) {
+        debugLog(`Good symbol, argument literal`);
+        return;
+      }
+      debugLog(`Match. Reporting failure (boundaries: ${n.getStart()}, ${
+          n.getEnd()}] on node [${n.getText()}]`);
+      return n;
+    }
+
+    for (const m of this.matchers) {
+      // The first matching matcher will be used.
+      const r = checkIndividual(n, m);
+      if (r) return r;
+    }
+    // No match.
+    return;
+  }
+}