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;
}
}