Test the suggested fix generation.

PiperOrigin-RevId: 252588645
diff --git a/internal/tsetse/failure.ts b/internal/tsetse/failure.ts
index 7db67ef..61680ac 100644
--- a/internal/tsetse/failure.ts
+++ b/internal/tsetse/failure.ts
@@ -37,8 +37,8 @@
 
   toString(): string {
     return `{ sourceFile:${
-        this.sourceFile ? this.sourceFile.fileName :
-                          'unknown'}, start:${this.start}, end:${this.end} }`;
+        this.sourceFile ? this.sourceFile.fileName : 'unknown'}, start:${
+        this.start}, end:${this.end}, fix:${fixToString(this.suggestedFix)} }`;
   }
 }
 
@@ -51,6 +51,23 @@
    */
   changes: IndividualChange[],
 }
+
 export interface IndividualChange {
   sourceFile: ts.SourceFile, start: number, end: number, replacement: string
 }
+
+/**
+ * Stringifies a Fix, replacing the ts.SourceFile with the matching filename.
+ */
+export function fixToString(f?: Fix) {
+  if (!f) return 'undefined';
+  return '{' + JSON.stringify(f.changes.map(ic => {
+    return {
+      start: ic.start,
+      end: ic.end,
+      replacement: ic.replacement,
+      fileName: ic.sourceFile.fileName
+    };
+  })) +
+      '}'
+}
diff --git a/internal/tsetse/tests/ban_conformance_pattern/fixer_test.ts b/internal/tsetse/tests/ban_conformance_pattern/fixer_test.ts
new file mode 100644
index 0000000..1bf13dd
--- /dev/null
+++ b/internal/tsetse/tests/ban_conformance_pattern/fixer_test.ts
@@ -0,0 +1,96 @@
+import 'jasmine';
+import * as ts from 'typescript';
+import {Fix} from '../../failure';
+import {ConformancePatternRule, PatternKind} from '../../rules/conformance_pattern_rule';
+import {buildReplacementFixer, Fixer} from '../../util/fixer';
+import {compileAndCheck, customMatchers} from '../../util/testing/test_support';
+
+const uppercaseFixer: Fixer = {
+  getFixForFlaggedNode(node: ts.Node): Fix {
+    return {
+      changes: [{
+        start: node.getStart(),
+        end: node.getEnd(),
+        replacement: node.getText().toUpperCase(),
+        sourceFile: node.getSourceFile(),
+      }]
+    };
+  }
+};
+
+const uppercaseFixerBuilt: Fixer = buildReplacementFixer((node: ts.Node) => {
+  return {replaceWith: node.getText().toUpperCase()};
+})
+
+describe('ConformancePatternRule\'s fixer', () => {
+  describe('Generates basic fixes', () => {
+    const source = `export {};\n` +
+        `const q = document.createElement('q');\n` +
+        `q.cite = 'some example string';\n`;
+
+    // The initial config off which we run those checks.
+    const baseConfig = {
+      errorMessage: 'found citation',
+      kind: PatternKind.BANNED_PROPERTY_WRITE,
+      values: ['HTMLQuoteElement.prototype.cite'],
+    };
+
+    it('for a single match', () => {
+      const rule = new ConformancePatternRule(baseConfig, uppercaseFixer);
+      const results = compileAndCheck(rule, source);
+
+      expect(results).toHaveNFailures(1, baseConfig);
+      expect(results[0]).toBeFailureMatching({
+        matchedCode: `q.cite = 'some example string'`,
+        errorMessage: 'found citationz'
+      });
+      expect(results[0]).toHaveFixMatching([
+        {start: 50, end: 80, replacement: `Q.CITE = 'SOME EXAMPLE STRING'`}
+      ]);
+    });
+
+
+    it('for a single match (alternate fixer)', () => {
+      const rule = new ConformancePatternRule(baseConfig, uppercaseFixerBuilt);
+      const results = compileAndCheck(rule, source);
+
+      expect(results).toHaveNFailures(1, baseConfig);
+      expect(results[0]).toBeFailureMatching({
+        matchedCode: `q.cite = 'some example string'`,
+        errorMessage: 'found citationz'
+      });
+      expect(results[0]).toHaveFixMatching([
+        {start: 50, end: 80, replacement: `Q.CITE = 'SOME EXAMPLE STRING'`}
+      ]);
+    });
+
+    it('for several matches', () => {
+      const rule = new ConformancePatternRule(baseConfig, uppercaseFixer);
+      const sourceTwoMatches =
+          source + `q.cite = 'some other example string';\n`;
+      const results = compileAndCheck(rule, sourceTwoMatches);
+
+      expect(results).toHaveNFailures(2, baseConfig);
+      expect(results[0]).toBeFailureMatching({
+        matchedCode: `q.cite = 'some example string'`,
+        errorMessage: 'found citationz'
+      });
+      expect(results[1]).toBeFailureMatching({
+        matchedCode: `q.cite = 'some other example string'`,
+        errorMessage: 'found citationz'
+      });
+      expect(results[0]).toHaveFixMatching([
+        {start: 50, end: 80, replacement: `Q.CITE = 'SOME EXAMPLE STRING'`}
+      ]);
+      expect(results[1]).toHaveFixMatching([{
+        start: 82,
+        end: 118,
+        replacement: `Q.CITE = 'SOME OTHER EXAMPLE STRING'`
+      }]);
+    });
+  });
+});
+
+beforeEach(() => {
+  jasmine.addMatchers(customMatchers);
+});
diff --git a/internal/tsetse/util/fixer.ts b/internal/tsetse/util/fixer.ts
index f9b784e..77fa9f8 100644
--- a/internal/tsetse/util/fixer.ts
+++ b/internal/tsetse/util/fixer.ts
@@ -9,7 +9,7 @@
  * of implementing a Fixer.
  */
 export interface Fixer<NodeType extends ts.Node = ts.Node> {
-  getFixForFlaggedNode(node: NodeType, v?: boolean): Fix|undefined;
+  getFixForFlaggedNode(node: NodeType): Fix|undefined;
 }
 
 /**
@@ -18,11 +18,11 @@
  * Fixer instead.
  */
 export function buildReplacementFixer(
-    potentialReplacementGenerator: (node: ts.Node, v?: boolean) =>
+    potentialReplacementGenerator: (node: ts.Node) =>
         ({replaceWith: string} | undefined)): Fixer {
   return {
-    getFixForFlaggedNode: (n: ts.Node, v?: boolean): Fix | undefined => {
-      const partialFix = potentialReplacementGenerator(n, v);
+    getFixForFlaggedNode: (n: ts.Node): Fix | undefined => {
+      const partialFix = potentialReplacementGenerator(n);
       if (!partialFix) {
         return;
       }
diff --git a/internal/tsetse/util/testing/test_support.ts b/internal/tsetse/util/testing/test_support.ts
index 7b93b7f..b6ca582 100644
--- a/internal/tsetse/util/testing/test_support.ts
+++ b/internal/tsetse/util/testing/test_support.ts
@@ -7,7 +7,7 @@
 import * as ts from 'typescript';
 
 import {Checker} from '../../checker';
-import {Failure} from '../../failure';
+import {Failure, fixToString} from '../../failure';
 import {AbstractRule} from '../../rule';
 import {Config} from '../pattern_config';
 
@@ -111,7 +111,7 @@
         fileName?: string,
         start?: number,
         end?: number,
-        matchedCode?: string
+        matchedCode?: string,
       }) => {
         const actualDiagnostic = actualFailure.toDiagnostic();
         let regrets = '';
@@ -154,6 +154,59 @@
         return {pass: regrets === '', message: regrets};
       }
     };
+  },
+
+  /** Checks that a Failure has the expected Fix field. */
+  toHaveFixMatching(): jasmine.CustomMatcher {
+    return {
+      compare: (actualFailure: Failure, exp: [{
+                  fileName?: string,
+                  start?: number,
+                  end?: number,
+                  replacement?: string
+                }]) => {
+        let regrets = '';
+        const actualFix = actualFailure.toDiagnostic().fix;
+        if (!actualFix) {
+          regrets += `Expected ${actualFailure.toString()} to have fix ${
+              JSON.stringify(exp)}. `;
+        } else if (actualFix.changes.length != exp.length) {
+          regrets += `Expected ${exp.length} individual changes, got ${
+              actualFix.changes.length}. `;
+          if (actualFix.changes.length) {
+            regrets += '\n' + fixToString(actualFix);
+          }
+        } else {
+          for (let i = 0; i < exp.length; i++) {
+            const e = exp[i];
+            const a = actualFix.changes[i];
+            if (e.start !== undefined && e.start !== a.start) {
+              regrets += expectation(
+                  `${i}th individualChange's start`, e.start, a.start);
+            }
+            if (e.end !== undefined && e.end !== a.end) {
+              regrets +=
+                  expectation(`${i}th individualChange's end`, e.end, a.end);
+            }
+            if (e.replacement !== undefined &&
+                e.replacement !== a.replacement) {
+              regrets += expectation(
+                  `${i}th individualChange's replacement`, e.replacement,
+                  a.replacement);
+            }
+            if (e.fileName !== undefined &&
+                e.fileName !== a.sourceFile.fileName) {
+              regrets += expectation(
+                  `${i}th individualChange's fileName`, e.fileName,
+                  a.sourceFile.fileName);
+            }
+            // TODO: Consider adding matchedCode as for the failure matcher.
+          }
+        }
+
+        return {pass: regrets === '', message: regrets};
+      }
+    };
   }
 };
 
@@ -170,9 +223,13 @@
         fileName?: string,
         start?: number,
         end?: number,
-        matchedCode?: string,
+        matchedCode?: string
       }): void;
 
+      toHaveFixMatching(expected: [
+        {fileName?: string, start?: number, end?: number, replacement?: string}
+      ]): void;
+
       toHaveNFailures(expected: Number, config?: Config<any>): void;
     }
   }