fix(core): avoid migration error when non-existent symbol is imported (#36367)

In rare cases a project with configured `rootDirs` that has imports to
non-existent identifiers could fail in the migration.

This happens because based on the application code, the migration could
end up trying to resolve the `ts.Symbol` of such non-existent
identifiers. This isn't a problem usually, but due to a upstream bug
in the TypeScript compiler, a runtime error is thrown.

This is because TypeScript is unable to compute a relative path from the
originating source file to the imported source file which _should_
provide the non-existent identifier. An issue for this has been reported
upstream: https://github.com/microsoft/TypeScript/issues/37731. The
issue only surfaces since our migrations don't provide an absolute base
path that is used for resolving the root directories.

To fix this, we ensure that we never use relative paths when parsing
tsconfig files. More details can be found in the TS issue.

Fixes #36346.

PR Close #36367
This commit is contained in:
Paul Gschwendtner
2020-04-02 11:01:43 +02:00
committed by Kara Erickson
parent 56af303dc3
commit dff52ecb11
11 changed files with 209 additions and 106 deletions

View File

@ -6,12 +6,39 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Tree} from '@angular-devkit/schematics';
import {relative} from 'path';
import {dirname, relative, resolve} from 'path';
import * as ts from 'typescript';
import {parseTsconfigFile} from './parse_tsconfig';
export type FakeReadFileFn = (fileName: string) => string|null;
/**
* Creates a TypeScript program instance for a TypeScript project within
* the virtual file system tree.
* @param tree Virtual file system tree that contains the source files.
* @param tsconfigPath Virtual file system path that resolves to the TypeScript project.
* @param basePath Base path for the virtual file system tree.
* @param fakeFileRead Optional file reader function. Can be used to overwrite files in
* the TypeScript program, or to add in-memory files (e.g. to add global types).
* @param additionalFiles Additional file paths that should be added to the program.
*/
export function createMigrationProgram(
tree: Tree, tsconfigPath: string, basePath: string, fakeFileRead?: FakeReadFileFn,
additionalFiles?: string[]) {
// Resolve the tsconfig path to an absolute path. This is needed as TypeScript otherwise
// is not able to resolve root directories in the given tsconfig. More details can be found
// in the following issue: https://github.com/microsoft/TypeScript/issues/37731.
tsconfigPath = resolve(basePath, tsconfigPath);
const parsed = parseTsconfigFile(tsconfigPath, dirname(tsconfigPath));
const host = createMigrationCompilerHost(tree, parsed.options, basePath, fakeFileRead);
const program =
ts.createProgram(parsed.fileNames.concat(additionalFiles || []), parsed.options, host);
return {parsed, host, program};
}
export function createMigrationCompilerHost(
tree: Tree, options: ts.CompilerOptions, basePath: string,
fakeRead?: (fileName: string) => string | null): ts.CompilerHost {
fakeRead?: FakeReadFileFn): ts.CompilerHost {
const host = ts.createCompilerHost(options, true);
// We need to overwrite the host "readFile" method, as we want the TypeScript

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as path from 'path';
import * as ts from 'typescript';
export function parseTsconfigFile(tsconfigPath: string, basePath: string): ts.ParsedCommandLine {
@ -17,5 +18,12 @@ export function parseTsconfigFile(tsconfigPath: string, basePath: string): ts.Pa
readFile: ts.sys.readFile,
};
// Throw if incorrect arguments are passed to this function. Passing relative base paths
// results in root directories not being resolved and in later type checking runtime errors.
// More details can be found here: https://github.com/microsoft/TypeScript/issues/37731.
if (!path.isAbsolute(basePath)) {
throw Error('Unexpected relative base path has been specified.');
}
return ts.parseJsonConfigFileContent(config, parseConfigHost, basePath, {});
}