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