Skip to content

Commit

Permalink
perf(betterer 🏎): only check changed ts files (#739)
Browse files Browse the repository at this point in the history
  • Loading branch information
phenomnomnominal authored Jun 12, 2021
1 parent 26fdd4a commit 0f8f44f
Show file tree
Hide file tree
Showing 10 changed files with 550 additions and 3 deletions.
2 changes: 2 additions & 0 deletions goldens/api/@betterer/tsquery.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export declare function tsquery(configFilePath: string, query: string): BettererFileTest;

export declare function tsqueryΔ(query: string): BettererFileTest;
2 changes: 2 additions & 0 deletions goldens/api/@betterer/typescript.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export declare function typescript(configFilePath: string, extraCompilerOptions: ts.CompilerOptions): BettererFileTest;

export declare function typescriptΔ(configFilePath: string, extraCompilerOptions?: ts.CompilerOptions): BettererFileTest;
2 changes: 1 addition & 1 deletion packages/tsquery/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { tsquery } from './tsquery';
export { tsquery, tsqueryΔ } from './tsquery';
31 changes: 31 additions & 0 deletions packages/tsquery/src/tsquery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,34 @@ export function tsquery(configFilePath: string, query: string): BettererFileTest
);
});
}

/** @internal Definitely not stable! Please don't use! */
export function tsqueryΔ(query: string): BettererFileTest {
if (!query) {
throw new BettererError(
"for `@betterer/tsquery` to work, you need to provide a query, e.g. `'CallExpression > PropertyAccessExpression'`. ❌"
);
}

const resolver = new BettererFileResolver();
return new BettererFileTest(resolver, async (filePaths, fileTestResult) => {
if (filePaths.length === 0) {
return;
}

await Promise.all(
filePaths.map(async (filePath) => {
const fileText = await fs.readFile(filePath, 'utf8');
const sourceFile = tsq.ast(fileText);
const matches = tsq.query(sourceFile, query, { visitAllChildren: true });
if (matches.length === 0) {
return;
}
const file = fileTestResult.addFile(filePath, fileText);
matches.forEach((match) => {
file.addIssue(match.getStart(), match.getEnd(), 'TSQuery match');
});
})
);
});
}
2 changes: 1 addition & 1 deletion packages/typescript/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { typescript } from './typescript';
export { typescript, typescriptΔ } from './typescript';
74 changes: 73 additions & 1 deletion packages/typescript/src/typescript.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BettererFileResolver, BettererFileTest } from '@betterer/betterer';
import { BettererFileGlobs, BettererFilePaths, BettererFileResolver, BettererFileTest } from '@betterer/betterer';
import { BettererError } from '@betterer/errors';
import * as path from 'path';
import * as ts from 'typescript';
Expand All @@ -8,6 +8,8 @@ const NEW_LINE = '\n';
type TypeScriptReadConfigResult = {
config: {
compilerOptions: ts.CompilerOptions;
files: BettererFilePaths;
include?: BettererFileGlobs;
};
};

Expand Down Expand Up @@ -74,3 +76,73 @@ export function typescript(configFilePath: string, extraCompilerOptions: ts.Comp
});
});
}

/** @internal Definitely not stable! Please don't use! */
export function typescriptΔ(configFilePath: string, extraCompilerOptions: ts.CompilerOptions = {}): BettererFileTest {
if (!configFilePath) {
throw new BettererError(
"for `@betterer/typescript` to work, you need to provide the path to a tsconfig.json file, e.g. `'./tsconfig.json'`. ❌"
);
}

const resolver = new BettererFileResolver();
const absPath = resolver.resolve(configFilePath);

return new BettererFileTest(resolver, (filePaths, fileTestResult) => {
if (filePaths.length === 0) {
return;
}

const { config } = ts.readConfigFile(absPath, ts.sys.readFile.bind(ts.sys)) as TypeScriptReadConfigResult;
const { compilerOptions } = config;
const basePath = path.dirname(absPath);

const fullCompilerOptions = {
...compilerOptions,
...extraCompilerOptions
};
config.compilerOptions = fullCompilerOptions;
config.files = filePaths;
delete config.include;

const compilerHost = ts.createCompilerHost(fullCompilerOptions);
const configHost: ts.ParseConfigHost = {
...compilerHost,
readDirectory: ts.sys.readDirectory.bind(ts.sys),
useCaseSensitiveFileNames: compilerHost.useCaseSensitiveFileNames()
};
const { options, fileNames } = ts.parseJsonConfigFileContent(config, configHost, basePath);
const incrementalHost = ts.createIncrementalCompilerHost(options);

const oldProgram = ts.readBuilderProgram(options, incrementalHost);
const program = oldProgram
? ts.createEmitAndSemanticDiagnosticsBuilderProgram(fileNames, options, incrementalHost, oldProgram)
: ts.createIncrementalProgram({
options,
rootNames: fileNames,
host: incrementalHost
});

const { diagnostics } = program.emit();

const allDiagnostics = ts.sortAndDeduplicateDiagnostics([
...diagnostics,
...program.getConfigFileParsingDiagnostics(),
...program.getOptionsDiagnostics(),
...program.getSyntacticDiagnostics(),
...program.getGlobalDiagnostics(),
...program.getSemanticDiagnostics()
]);

allDiagnostics
.filter(({ file, start, length }) => file && start != null && length != null)
.forEach((diagnostic) => {
const { start, length } = diagnostic as ts.DiagnosticWithLocation;
const source = (diagnostic as ts.DiagnosticWithLocation).file;
const { fileName } = source;
const file = fileTestResult.addFile(fileName, source.getFullText());
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, NEW_LINE);
file.addIssue(start, start + length, message);
});
});
}
135 changes: 135 additions & 0 deletions test/__snapshots__/betterer-typescript.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -563,3 +563,138 @@ Error: for \`@betterer/typescript\` to work, you need to provide compiler option
",
]
`;
exports[`betterer should work with an incremental build 1`] = `
Array [
"
/ | / _ _ _
'-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __
---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__|
.-'ooo'-. | |_)| __/ |_| || __/ | | __/ |
/ | / |_.__//___|/__|/__/___|_| /___|_|
",
"
/ | / _ _ _
'-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __
---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__|
.-'ooo'-. | |_)| __/ |_| || __/ | | __/ |
/ | / |_.__//___|/__|/__/___|_| /___|_|
🌟 Betterer (0ms):
",
"
/ | / _ _ _
'-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __
---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__|
.-'ooo'-. | |_)| __/ |_| || __/ | | __/ |
/ | / |_.__//___|/__|/__/___|_| /___|_|
🌟 Betterer (0ms): 1 test running...
🤔 typescript incremental: running \\"typescript incremental\\"!
",
"
/ | / _ _ _
'-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __
---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__|
.-'ooo'-. | |_)| __/ |_| || __/ | | __/ |
/ | / |_.__//___|/__|/__/___|_| /___|_|
🌟 Betterer (0ms): 1 test running...
✅ typescript incremental: \\"typescript incremental\\" got checked for the first time! (1 issue) 🎉
",
"
/ | / _ _ _
'-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __
---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__|
.-'ooo'-. | |_)| __/ |_| || __/ | | __/ |
/ | / |_.__//___|/__|/__/___|_| /___|_|
🎉 Betterer (0ms): 1 test done!
✅ typescript incremental: \\"typescript incremental\\" got checked for the first time! (1 issue) 🎉
",
"
/ | / _ _ _
'-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __
---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__|
.-'ooo'-. | |_)| __/ |_| || __/ | | __/ |
/ | / |_.__//___|/__|/__/___|_| /___|_|
🎉 Betterer (0ms): 1 test done!
✅ typescript incremental: \\"typescript incremental\\" got checked for the first time! (1 issue) 🎉
1 test got checked. 🤔
1 test got checked for the first time! 🎉
",
"
/ | / _ _ _
'-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __
---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__|
.-'ooo'-. | |_)| __/ |_| || __/ | | __/ |
/ | / |_.__//___|/__|/__/___|_| /___|_|
",
"
/ | / _ _ _
'-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __
---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__|
.-'ooo'-. | |_)| __/ |_| || __/ | | __/ |
/ | / |_.__//___|/__|/__/___|_| /___|_|
🌟 Betterer (0ms):
",
"
/ | / _ _ _
'-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __
---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__|
.-'ooo'-. | |_)| __/ |_| || __/ | | __/ |
/ | / |_.__//___|/__|/__/___|_| /___|_|
🌟 Betterer (0ms): 1 test running...
🤔 typescript incremental: running \\"typescript incremental\\"!
",
"
/ | / _ _ _
'-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __
---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__|
.-'ooo'-. | |_)| __/ |_| || __/ | | __/ |
/ | / |_.__//___|/__|/__/___|_| /___|_|
🌟 Betterer (0ms): 1 test running...
✅ typescript incremental: \\"typescript incremental\\" stayed the same. (1 issue) 😐
",
"
/ | / _ _ _
'-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __
---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__|
.-'ooo'-. | |_)| __/ |_| || __/ | | __/ |
/ | / |_.__//___|/__|/__/___|_| /___|_|
🎉 Betterer (0ms): 1 test done!
✅ typescript incremental: \\"typescript incremental\\" stayed the same. (1 issue) 😐
",
"
/ | / _ _ _
'-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __
---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__|
.-'ooo'-. | |_)| __/ |_| || __/ | | __/ |
/ | / |_.__//___|/__|/__/___|_| /___|_|
🎉 Betterer (0ms): 1 test done!
✅ typescript incremental: \\"typescript incremental\\" stayed the same. (1 issue) 😐
1 test got checked. 🤔
1 test stayed the same. 😐
",
]
`;
73 changes: 73 additions & 0 deletions test/betterer-typescript.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,77 @@ export default {

await cleanup();
});

it('should work with an incremental build', async () => {
const { paths, logs, resolve, cleanup, readFile, writeFile, runNames } = await createFixture(
'test-betterer-typescript-incremental',
{
'.betterer.ts': `
import { typescriptΔ } from '@betterer/typescript';
export default {
'typescript incremental': typescriptΔ('./tsconfig.json', {
incremental: true,
tsBuildInfoFile: './.betterer.tsbuildinfo'
}).include('./src/**/*.ts')
};
`,
'tsconfig.json': `
{
"compilerOptions": {
"noEmit": true,
"lib": ["esnext"],
"moduleResolution": "node",
"target": "ES5",
"typeRoots": ["../../node_modules/@types/"],
"resolveJsonModule": true,
"strict": false
},
"include": [".betterer.ts"]
}
`,
'./src/foo.ts': `
import { bar } from './bar';
export function foo (a: number, b: number, c:number) {
return bar(a * 2, b * 2, c * 2);
}
`,
'./src/bar.ts': `
export function bar (a: number, b: number, c:number) {
return (a * b) ** c;
}
`
}
);

const configPaths = [paths.config];
const resultsPath = paths.results;
const indexPath = resolve('./src/index.ts');
const buildInfoPath = resolve('./.betterer.tsbuildinfo');

await writeFile(indexPath, `import { foo } from './foo';\n\nfoo('a', 'b', 'c');`);

const newStart = new Date().getTime();
const newTestRun = await betterer({ configPaths, resultsPath });
const newTime = new Date().getTime() - newStart;

expect(runNames(newTestRun.new)).toEqual(['typescript incremental']);

const buildInfo = await readFile(buildInfoPath);

expect(buildInfo).not.toBeNull();

const sameStart = new Date().getTime();
const sameTestRun = await betterer({ configPaths, resultsPath });
const sameTime = new Date().getTime() - sameStart;

expect(sameTime).toBeLessThan(newTime);

expect(runNames(sameTestRun.same)).toEqual(['typescript incremental']);

expect(logs).toMatchSnapshot();

await cleanup();
});
});
29 changes: 29 additions & 0 deletions test/runner/__snapshots__/betterer-runner.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`betterer.runner should ignore any files outside of the scope of the tsqueryΔ tsconfig 1`] = `"// BETTERER RESULTS V2."`;

exports[`betterer.runner should ignore any files outside of the scope of the typescriptΔ test glob 1`] = `"// BETTERER RESULTS V2."`;

exports[`betterer.runner should run tsqueryΔ against a file 1`] = `
"// BETTERER RESULTS V2.
exports[\`tsquery no raw console.log\`] = {
value: \`{
\\"src/index.ts:4087252068\\": [
[0, 0, 11, \\"TSQuery match\\", \\"3870399096\\"]
]
}\`
};
"
`;
exports[`betterer.runner should run typescriptΔ against a file 1`] = `
"// BETTERER RESULTS V2.
exports[\`use typescript\`] = {
value: \`{
\\"src/index.ts:1499252024\\": [
[2, 12, 1, \\"The left-hand side of an arithmetic operation must be of type \\\\'any\\\\', \\\\'number\\\\', \\\\'bigint\\\\' or an enum type.\\", \\"177604\\"]
]
}\`
};
"
`;
Loading

0 comments on commit 0f8f44f

Please sign in to comment.