Add a property_write_non_constant engine to Tsetse's ConformancePattern.

Same as property_write, but only triggers on writing non-constant values.

PiperOrigin-RevId: 249633880
diff --git a/internal/tsetse/rules/conformance_pattern_rule.ts b/internal/tsetse/rules/conformance_pattern_rule.ts
index c78f90c..b683355 100644
--- a/internal/tsetse/rules/conformance_pattern_rule.ts
+++ b/internal/tsetse/rules/conformance_pattern_rule.ts
@@ -4,6 +4,7 @@
 import {Fixer} from '../util/fixer';
 import {Config, MatchedNodeTypes, PatternKind} from '../util/pattern_config';
 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';
 
 /**
@@ -27,7 +28,13 @@
     let engine: PatternEngine<any>;
     switch (config.kind) {
       case PatternKind.BANNED_PROPERTY_WRITE:
-        engine = new PropertyWriteEngine(config, fixer);
+        engine = new PropertyWriteEngine(
+            config as Config<PatternKind.BANNED_PROPERTY_WRITE>, fixer);
+        break;
+      case PatternKind.BANNED_PROPERTY_NON_CONSTANT_WRITE:
+        engine = new PropertyNonConstantWriteEngine(
+            config as Config<PatternKind.BANNED_PROPERTY_NON_CONSTANT_WRITE>,
+            fixer);
         break;
       default:
         throw new Error('Config type not recognized, or not implemented yet.');
diff --git a/internal/tsetse/tests/ban_conformance_pattern/property_non_constant_write_test.ts b/internal/tsetse/tests/ban_conformance_pattern/property_non_constant_write_test.ts
new file mode 100644
index 0000000..8b51796
--- /dev/null
+++ b/internal/tsetse/tests/ban_conformance_pattern/property_non_constant_write_test.ts
@@ -0,0 +1,27 @@
+import 'jasmine';
+import {ConformancePatternRule, PatternKind} from '../../rules/conformance_pattern_rule';
+import {compileAndCheck, customMatchers} from '../../util/testing/test_support';
+
+describe('BANNED_PROPERTY_NON_CONSTANT_WRITE', () => {
+  it('matches a trivial example', () => {
+    const source = `const q = document.createElement('q');\n` +
+        `q.cite = 'some example string';\n` +  // literal
+        `q.cite = window.name;\n`;             // non-literal
+    const rule = new ConformancePatternRule({
+      errorMessage: 'do not cite dynamically',
+      kind: PatternKind.BANNED_PROPERTY_NON_CONSTANT_WRITE,
+      values: ['HTMLQuoteElement.prototype.cite']
+    });
+    const results = compileAndCheck(rule, source);
+
+    expect(results.length).toBe(1);
+    expect(results[0])
+        .toBeFailureMatching(
+            {start: 71, end: 91, errorMessage: 'do not cite dynamically'});
+  });
+});
+
+
+beforeEach(() => {
+  jasmine.addMatchers(customMatchers);
+});
diff --git a/internal/tsetse/tests/ban_conformance_pattern/util_test.ts b/internal/tsetse/tests/ban_conformance_pattern/util_test.ts
index 0588b47..94ab234 100644
--- a/internal/tsetse/tests/ban_conformance_pattern/util_test.ts
+++ b/internal/tsetse/tests/ban_conformance_pattern/util_test.ts
@@ -5,8 +5,9 @@
 
 import 'jasmine';
 import {ConformancePatternRule, PatternKind} from '../../rules/conformance_pattern_rule';
-import {setDebug} from '../../util/ast_tools';
-import {compileAndCheck} from '../../util/testing/test_support';
+import {isInStockLibraries, setDebug} from '../../util/ast_tools';
+import {isLiteral} from '../../util/is_literal';
+import {compile, compileAndCheck} from '../../util/testing/test_support';
 
 describe('Debug output', () => {
   it('turns on and off', () => {
@@ -35,3 +36,62 @@
     expect(logs).toEqual([`inspecting obj.prop = 'foo'`]);
   });
 });
+
+describe('The constant-ness logic', () => {
+  it('understands constants', () => {
+    // Keep these to single-expression programs.
+    const constantExpressionsSources = [
+      `'hello'`,
+      `'hello' + 'hi'`,
+      `1`,
+      `1 + 1`,
+      '`abcdef`',
+      '`abcd${"ef"}`',
+      '`abcd${1+1}`+`hi`',
+      `1 ? 'hi' : 'hello'`,
+      `window.name ? 'hi' : 'hello'`,
+    ];
+
+    // We don't bother with a rule for this one.
+    const constantProgram = compile(...constantExpressionsSources);
+    const constantCompiledSources = constantProgram.getSourceFiles();
+    const constantTc = constantProgram.getTypeChecker();
+    const constantExpressions =
+        constantCompiledSources.filter(s => !isInStockLibraries(s))
+            .map(s => s.statements[0].getChildren()[0]);
+
+    for (const expr of constantExpressions) {
+      expect(isLiteral(constantTc, expr))
+          .toBe(
+              true,
+              `Expected "${expr.getFullText()}" to be considered constant.`);
+    }
+  });
+
+
+  it('understands non-constants', () => {
+    const nonconstantExpressionsSources = [
+      `window.name`,
+      `'hello' + window.name`,
+      `window.name + 'hello'`,
+      '`abcd${window.name}`',
+      `1 ? window.name : 'hello'`,
+      `1 ? 'hello' : window.name`,
+    ];
+
+    const nonconstantProgram = compile(...nonconstantExpressionsSources);
+    const nonconstantCompiledSources = nonconstantProgram.getSourceFiles();
+    const nonconstantTc = nonconstantProgram.getTypeChecker();
+    const nonconstantExpressions =
+        nonconstantCompiledSources.filter(s => !isInStockLibraries(s))
+            .map(s => s.statements[0].getChildren()[0]);
+
+    for (const expr of nonconstantExpressions) {
+      expect(isLiteral(nonconstantTc, expr))
+          .toBe(
+              false,
+              `Expected "${
+                  expr.getFullText()}" not to be considered constant.`);
+    }
+  });
+});
diff --git a/internal/tsetse/util/pattern_config.ts b/internal/tsetse/util/pattern_config.ts
index c2748d9..76e2d71 100644
--- a/internal/tsetse/util/pattern_config.ts
+++ b/internal/tsetse/util/pattern_config.ts
@@ -7,6 +7,7 @@
  */
 export enum PatternKind {
   BANNED_PROPERTY_WRITE = 'banned-property-write',
+  BANNED_PROPERTY_NON_CONSTANT_WRITE = 'banned-property-non-constant-write'
 }
 
 /**
@@ -30,4 +31,7 @@
   [PatternKind.BANNED_PROPERTY_WRITE]: ts.BinaryExpression&{
     left: ts.PropertyAccessExpression;
   };
+  [PatternKind.BANNED_PROPERTY_NON_CONSTANT_WRITE]: ts.BinaryExpression&{
+    left: ts.PropertyAccessExpression;
+  };
 }
diff --git a/internal/tsetse/util/pattern_engines/property_non_constant_write_engine.ts b/internal/tsetse/util/pattern_engines/property_non_constant_write_engine.ts
new file mode 100644
index 0000000..9e26cd7
--- /dev/null
+++ b/internal/tsetse/util/pattern_engines/property_non_constant_write_engine.ts
@@ -0,0 +1,58 @@
+import * as ts from 'typescript';
+import {Checker} from '../../checker';
+import {ErrorCode} from '../../error_code';
+import {Fix} from '../../failure';
+import {debugLog, isPropertyWriteExpression, shouldExamineNode} from '../ast_tools';
+import {Fixer} from '../fixer';
+import {isLiteral} from '../is_literal';
+import {PropertyMatcher} from '../match_symbol';
+import {Config, MatchedNodeTypes, PatternKind} from '../pattern_config';
+import {PatternEngine} from './pattern_engine';
+
+// Just for conciseness.
+type BanKind = PatternKind.BANNED_PROPERTY_NON_CONSTANT_WRITE;
+
+/**
+ * The engine for BANNED_PROPERTY_NON_CONSTANT_WRITE.
+ */
+export class PropertyNonConstantWriteEngine extends PatternEngine<BanKind> {
+  private readonly matcher: PropertyMatcher;
+  constructor(
+      config: Config<BanKind>, fixer?: Fixer<MatchedNodeTypes[BanKind]>) {
+    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_PROPERTY_NON_CONSTANT_WRITE expects one value, got(${
+              this.config.values.join(',')})`);
+    }
+    this.matcher = PropertyMatcher.fromSpec(this.config.values[0]);
+  }
+
+  register(checker: Checker) {
+    checker.on(
+        ts.SyntaxKind.BinaryExpression, this.check.bind(this),
+        ErrorCode.CONFORMANCE_PATTERN);
+  }
+
+  check(c: Checker, n: ts.BinaryExpression) {
+    if (!shouldExamineNode(n) || n.getSourceFile().isDeclarationFile ||
+        !isPropertyWriteExpression(n)) {
+      return;
+    }
+    debugLog(`inspecting ${n.getFullText().trim()}`);
+    if (!this.matcher.matches(n.left, c.typeChecker)) {
+      debugLog('Not an assignment to the right property');
+      return;
+    }
+    if (isLiteral(c.typeChecker, n.right)) {
+      debugLog(`Assigned value (${
+          n.right.getFullText()}) is a compile-time constant.`);
+      return;
+    }
+    const fix: Fix|undefined =
+        this.fixer ? this.fixer.getFixForFlaggedNode(n) : undefined;
+    c.addFailureAtNode(n, this.config.errorMessage, fix);
+  }
+}
diff --git a/internal/tsetse/util/testing/test_support.ts b/internal/tsetse/util/testing/test_support.ts
index 010ad8c..8adb90a 100644
--- a/internal/tsetse/util/testing/test_support.ts
+++ b/internal/tsetse/util/testing/test_support.ts
@@ -10,7 +10,13 @@
 import {AbstractRule} from '../../rule';
 
 
-function compile(...sourceCode: string[]): ts.Program {
+
+/**
+ * Turns the provided source (as strings) into a ts.Program. The source files
+ * will be named `.../file_${n}.ts`, with n the index of the source file in
+ * the `sourceCode` array.
+ */
+export function compile(...sourceCode: string[]): ts.Program {
   const temporaryFolder = os.tmpdir() +
       `/tslint_test_input_${crypto.randomBytes(16).toString('hex')}`;
   const fullPaths: string[] = [];