refactor(ivy): implement a virtual file-system layer in ngtsc + ngcc (#30921)
To improve cross platform support, all file access (and path manipulation) is now done through a well known interface (`FileSystem`). For testing a number of `MockFileSystem` implementations are provided. These provide an in-memory file-system which emulates operating systems like OS/X, Unix and Windows. The current file system is always available via the static method, `FileSystem.getFileSystem()`. This is also used by a number of static methods on `AbsoluteFsPath` and `PathSegment`, to avoid having to pass `FileSystem` objects around all the time. The result of this is that one must be careful to ensure that the file-system has been initialized before using any of these static methods. To prevent this happening accidentally the current file system always starts out as an instance of `InvalidFileSystem`, which will throw an error if any of its methods are called. You can set the current file-system by calling `FileSystem.setFileSystem()`. During testing you can call the helper function `initMockFileSystem(os)` which takes a string name of the OS to emulate, and will also monkey-patch aspects of the TypeScript library to ensure that TS is also using the current file-system. Finally there is the `NgtscCompilerHost` to be used for any TypeScript compilation, which uses a given file-system. All tests that interact with the file-system should be tested against each of the mock file-systems. A series of helpers have been provided to support such tests: * `runInEachFileSystem()` - wrap your tests in this helper to run all the wrapped tests in each of the mock file-systems. * `addTestFilesToFileSystem()` - use this to add files and their contents to the mock file system for testing. * `loadTestFilesFromDisk()` - use this to load a mirror image of files on disk into the in-memory mock file-system. * `loadFakeCore()` - use this to load a fake version of `@angular/core` into the mock file-system. All ngcc and ngtsc source and tests now use this virtual file-system setup. PR Close #30921
This commit is contained in:

committed by
Kara Erickson

parent
1e7e065423
commit
7186f9c016
@ -10,6 +10,8 @@ ts_library(
|
||||
]),
|
||||
deps = [
|
||||
"//packages:types",
|
||||
"//packages/compiler-cli/src/ngtsc/file_system",
|
||||
"//packages/compiler-cli/src/ngtsc/file_system/testing",
|
||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||
"//packages/compiler-cli/src/ngtsc/testing",
|
||||
"@npm//typescript",
|
||||
|
@ -5,47 +5,52 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
|
||||
import {absoluteFrom} from '../../file_system';
|
||||
import {runInEachFileSystem} from '../../file_system/testing';
|
||||
import {getDeclaration, makeProgram} from '../../testing';
|
||||
import {CtorParameter} from '../src/host';
|
||||
import {TypeScriptReflectionHost} from '../src/typescript';
|
||||
import {isNamedClassDeclaration} from '../src/util';
|
||||
|
||||
describe('reflector', () => {
|
||||
describe('ctor params', () => {
|
||||
it('should reflect a single argument', () => {
|
||||
const {program} = makeProgram([{
|
||||
name: 'entry.ts',
|
||||
contents: `
|
||||
runInEachFileSystem(() => {
|
||||
describe('reflector', () => {
|
||||
let _: typeof absoluteFrom;
|
||||
|
||||
beforeEach(() => _ = absoluteFrom);
|
||||
|
||||
describe('ctor params', () => {
|
||||
it('should reflect a single argument', () => {
|
||||
const {program} = makeProgram([{
|
||||
name: _('/entry.ts'),
|
||||
contents: `
|
||||
class Bar {}
|
||||
|
||||
class Foo {
|
||||
constructor(bar: Bar) {}
|
||||
}
|
||||
`
|
||||
}]);
|
||||
const clazz = getDeclaration(program, 'entry.ts', 'Foo', isNamedClassDeclaration);
|
||||
const checker = program.getTypeChecker();
|
||||
const host = new TypeScriptReflectionHost(checker);
|
||||
const args = host.getConstructorParameters(clazz) !;
|
||||
expect(args.length).toBe(1);
|
||||
expectParameter(args[0], 'bar', 'Bar');
|
||||
});
|
||||
}]);
|
||||
const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration);
|
||||
const checker = program.getTypeChecker();
|
||||
const host = new TypeScriptReflectionHost(checker);
|
||||
const args = host.getConstructorParameters(clazz) !;
|
||||
expect(args.length).toBe(1);
|
||||
expectParameter(args[0], 'bar', 'Bar');
|
||||
});
|
||||
|
||||
it('should reflect a decorated argument', () => {
|
||||
const {program} = makeProgram([
|
||||
{
|
||||
name: 'dec.ts',
|
||||
contents: `
|
||||
it('should reflect a decorated argument', () => {
|
||||
const {program} = makeProgram([
|
||||
{
|
||||
name: _('/dec.ts'),
|
||||
contents: `
|
||||
export function dec(target: any, key: string, index: number) {
|
||||
}
|
||||
`
|
||||
},
|
||||
{
|
||||
name: 'entry.ts',
|
||||
contents: `
|
||||
},
|
||||
{
|
||||
name: _('/entry.ts'),
|
||||
contents: `
|
||||
import {dec} from './dec';
|
||||
class Bar {}
|
||||
|
||||
@ -53,28 +58,28 @@ describe('reflector', () => {
|
||||
constructor(@dec bar: Bar) {}
|
||||
}
|
||||
`
|
||||
}
|
||||
]);
|
||||
const clazz = getDeclaration(program, 'entry.ts', 'Foo', isNamedClassDeclaration);
|
||||
const checker = program.getTypeChecker();
|
||||
const host = new TypeScriptReflectionHost(checker);
|
||||
const args = host.getConstructorParameters(clazz) !;
|
||||
expect(args.length).toBe(1);
|
||||
expectParameter(args[0], 'bar', 'Bar', 'dec', './dec');
|
||||
});
|
||||
}
|
||||
]);
|
||||
const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration);
|
||||
const checker = program.getTypeChecker();
|
||||
const host = new TypeScriptReflectionHost(checker);
|
||||
const args = host.getConstructorParameters(clazz) !;
|
||||
expect(args.length).toBe(1);
|
||||
expectParameter(args[0], 'bar', 'Bar', 'dec', './dec');
|
||||
});
|
||||
|
||||
it('should reflect a decorated argument with a call', () => {
|
||||
const {program} = makeProgram([
|
||||
{
|
||||
name: 'dec.ts',
|
||||
contents: `
|
||||
it('should reflect a decorated argument with a call', () => {
|
||||
const {program} = makeProgram([
|
||||
{
|
||||
name: _('/dec.ts'),
|
||||
contents: `
|
||||
export function dec(target: any, key: string, index: number) {
|
||||
}
|
||||
`
|
||||
},
|
||||
{
|
||||
name: 'entry.ts',
|
||||
contents: `
|
||||
},
|
||||
{
|
||||
name: _('/entry.ts'),
|
||||
contents: `
|
||||
import {dec} from './dec';
|
||||
class Bar {}
|
||||
|
||||
@ -82,27 +87,27 @@ describe('reflector', () => {
|
||||
constructor(@dec bar: Bar) {}
|
||||
}
|
||||
`
|
||||
}
|
||||
]);
|
||||
const clazz = getDeclaration(program, 'entry.ts', 'Foo', isNamedClassDeclaration);
|
||||
const checker = program.getTypeChecker();
|
||||
const host = new TypeScriptReflectionHost(checker);
|
||||
const args = host.getConstructorParameters(clazz) !;
|
||||
expect(args.length).toBe(1);
|
||||
expectParameter(args[0], 'bar', 'Bar', 'dec', './dec');
|
||||
});
|
||||
}
|
||||
]);
|
||||
const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration);
|
||||
const checker = program.getTypeChecker();
|
||||
const host = new TypeScriptReflectionHost(checker);
|
||||
const args = host.getConstructorParameters(clazz) !;
|
||||
expect(args.length).toBe(1);
|
||||
expectParameter(args[0], 'bar', 'Bar', 'dec', './dec');
|
||||
});
|
||||
|
||||
it('should reflect a decorated argument with an indirection', () => {
|
||||
const {program} = makeProgram([
|
||||
{
|
||||
name: 'bar.ts',
|
||||
contents: `
|
||||
it('should reflect a decorated argument with an indirection', () => {
|
||||
const {program} = makeProgram([
|
||||
{
|
||||
name: _('/bar.ts'),
|
||||
contents: `
|
||||
export class Bar {}
|
||||
`
|
||||
},
|
||||
{
|
||||
name: 'entry.ts',
|
||||
contents: `
|
||||
},
|
||||
{
|
||||
name: _('/entry.ts'),
|
||||
contents: `
|
||||
import {Bar} from './bar';
|
||||
import * as star from './bar';
|
||||
|
||||
@ -110,187 +115,188 @@ describe('reflector', () => {
|
||||
constructor(bar: Bar, otherBar: star.Bar) {}
|
||||
}
|
||||
`
|
||||
}
|
||||
]);
|
||||
const clazz = getDeclaration(program, 'entry.ts', 'Foo', isNamedClassDeclaration);
|
||||
const checker = program.getTypeChecker();
|
||||
const host = new TypeScriptReflectionHost(checker);
|
||||
const args = host.getConstructorParameters(clazz) !;
|
||||
expect(args.length).toBe(2);
|
||||
expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'});
|
||||
expectParameter(args[1], 'otherBar', {moduleName: './bar', name: 'Bar'});
|
||||
});
|
||||
}
|
||||
]);
|
||||
const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration);
|
||||
const checker = program.getTypeChecker();
|
||||
const host = new TypeScriptReflectionHost(checker);
|
||||
const args = host.getConstructorParameters(clazz) !;
|
||||
expect(args.length).toBe(2);
|
||||
expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'});
|
||||
expectParameter(args[1], 'otherBar', {moduleName: './bar', name: 'Bar'});
|
||||
});
|
||||
|
||||
it('should reflect an argument from an aliased import', () => {
|
||||
const {program} = makeProgram([
|
||||
{
|
||||
name: 'bar.ts',
|
||||
contents: `
|
||||
it('should reflect an argument from an aliased import', () => {
|
||||
const {program} = makeProgram([
|
||||
{
|
||||
name: _('/bar.ts'),
|
||||
contents: `
|
||||
export class Bar {}
|
||||
`
|
||||
},
|
||||
{
|
||||
name: 'entry.ts',
|
||||
contents: `
|
||||
},
|
||||
{
|
||||
name: _('/entry.ts'),
|
||||
contents: `
|
||||
import {Bar as LocalBar} from './bar';
|
||||
|
||||
class Foo {
|
||||
constructor(bar: LocalBar) {}
|
||||
}
|
||||
`
|
||||
}
|
||||
]);
|
||||
const clazz = getDeclaration(program, 'entry.ts', 'Foo', isNamedClassDeclaration);
|
||||
const checker = program.getTypeChecker();
|
||||
const host = new TypeScriptReflectionHost(checker);
|
||||
const args = host.getConstructorParameters(clazz) !;
|
||||
expect(args.length).toBe(1);
|
||||
expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'});
|
||||
});
|
||||
}
|
||||
]);
|
||||
const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration);
|
||||
const checker = program.getTypeChecker();
|
||||
const host = new TypeScriptReflectionHost(checker);
|
||||
const args = host.getConstructorParameters(clazz) !;
|
||||
expect(args.length).toBe(1);
|
||||
expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'});
|
||||
});
|
||||
|
||||
it('should reflect an argument from a default import', () => {
|
||||
const {program} = makeProgram([
|
||||
{
|
||||
name: 'bar.ts',
|
||||
contents: `
|
||||
it('should reflect an argument from a default import', () => {
|
||||
const {program} = makeProgram([
|
||||
{
|
||||
name: _('/bar.ts'),
|
||||
contents: `
|
||||
export default class Bar {}
|
||||
`
|
||||
},
|
||||
{
|
||||
name: 'entry.ts',
|
||||
contents: `
|
||||
},
|
||||
{
|
||||
name: _('/entry.ts'),
|
||||
contents: `
|
||||
import Bar from './bar';
|
||||
|
||||
class Foo {
|
||||
constructor(bar: Bar) {}
|
||||
}
|
||||
`
|
||||
}
|
||||
]);
|
||||
const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration);
|
||||
const checker = program.getTypeChecker();
|
||||
const host = new TypeScriptReflectionHost(checker);
|
||||
const args = host.getConstructorParameters(clazz) !;
|
||||
expect(args.length).toBe(1);
|
||||
const param = args[0].typeValueReference;
|
||||
if (param === null || !param.local) {
|
||||
return fail('Expected local parameter');
|
||||
}
|
||||
]);
|
||||
const clazz = getDeclaration(program, 'entry.ts', 'Foo', isNamedClassDeclaration);
|
||||
const checker = program.getTypeChecker();
|
||||
const host = new TypeScriptReflectionHost(checker);
|
||||
const args = host.getConstructorParameters(clazz) !;
|
||||
expect(args.length).toBe(1);
|
||||
const param = args[0].typeValueReference;
|
||||
if (param === null || !param.local) {
|
||||
return fail('Expected local parameter');
|
||||
}
|
||||
expect(param).not.toBeNull();
|
||||
expect(param.defaultImportStatement).not.toBeNull();
|
||||
});
|
||||
expect(param).not.toBeNull();
|
||||
expect(param.defaultImportStatement).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should reflect a nullable argument', () => {
|
||||
const {program} = makeProgram([
|
||||
{
|
||||
name: 'bar.ts',
|
||||
contents: `
|
||||
it('should reflect a nullable argument', () => {
|
||||
const {program} = makeProgram([
|
||||
{
|
||||
name: _('/bar.ts'),
|
||||
contents: `
|
||||
export class Bar {}
|
||||
`
|
||||
},
|
||||
{
|
||||
name: 'entry.ts',
|
||||
contents: `
|
||||
},
|
||||
{
|
||||
name: _('/entry.ts'),
|
||||
contents: `
|
||||
import {Bar} from './bar';
|
||||
|
||||
class Foo {
|
||||
constructor(bar: Bar|null) {}
|
||||
}
|
||||
`
|
||||
}
|
||||
]);
|
||||
const clazz = getDeclaration(program, 'entry.ts', 'Foo', isNamedClassDeclaration);
|
||||
const checker = program.getTypeChecker();
|
||||
const host = new TypeScriptReflectionHost(checker);
|
||||
const args = host.getConstructorParameters(clazz) !;
|
||||
expect(args.length).toBe(1);
|
||||
expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'});
|
||||
}
|
||||
]);
|
||||
const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration);
|
||||
const checker = program.getTypeChecker();
|
||||
const host = new TypeScriptReflectionHost(checker);
|
||||
const args = host.getConstructorParameters(clazz) !;
|
||||
expect(args.length).toBe(1);
|
||||
expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should reflect a re-export', () => {
|
||||
const {program} = makeProgram([
|
||||
{name: '/node_modules/absolute/index.ts', contents: 'export class Target {}'},
|
||||
{name: 'local1.ts', contents: `export {Target as AliasTarget} from 'absolute';`},
|
||||
{name: 'local2.ts', contents: `export {AliasTarget as Target} from './local1';`}, {
|
||||
name: 'entry.ts',
|
||||
contents: `
|
||||
it('should reflect a re-export', () => {
|
||||
const {program} = makeProgram([
|
||||
{name: _('/node_modules/absolute/index.ts'), contents: 'export class Target {}'},
|
||||
{name: _('/local1.ts'), contents: `export {Target as AliasTarget} from 'absolute';`},
|
||||
{name: _('/local2.ts'), contents: `export {AliasTarget as Target} from './local1';`}, {
|
||||
name: _('/entry.ts'),
|
||||
contents: `
|
||||
import {Target} from './local2';
|
||||
import {Target as DirectTarget} from 'absolute';
|
||||
|
||||
const target = Target;
|
||||
const directTarget = DirectTarget;
|
||||
`
|
||||
}
|
||||
]);
|
||||
const target = getDeclaration(program, _('/entry.ts'), 'target', ts.isVariableDeclaration);
|
||||
if (target.initializer === undefined || !ts.isIdentifier(target.initializer)) {
|
||||
return fail('Unexpected initializer for target');
|
||||
}
|
||||
]);
|
||||
const target = getDeclaration(program, 'entry.ts', 'target', ts.isVariableDeclaration);
|
||||
if (target.initializer === undefined || !ts.isIdentifier(target.initializer)) {
|
||||
return fail('Unexpected initializer for target');
|
||||
}
|
||||
const directTarget =
|
||||
getDeclaration(program, 'entry.ts', 'directTarget', ts.isVariableDeclaration);
|
||||
if (directTarget.initializer === undefined || !ts.isIdentifier(directTarget.initializer)) {
|
||||
return fail('Unexpected initializer for directTarget');
|
||||
}
|
||||
const Target = target.initializer;
|
||||
const DirectTarget = directTarget.initializer;
|
||||
const directTarget =
|
||||
getDeclaration(program, _('/entry.ts'), 'directTarget', ts.isVariableDeclaration);
|
||||
if (directTarget.initializer === undefined || !ts.isIdentifier(directTarget.initializer)) {
|
||||
return fail('Unexpected initializer for directTarget');
|
||||
}
|
||||
const Target = target.initializer;
|
||||
const DirectTarget = directTarget.initializer;
|
||||
|
||||
const checker = program.getTypeChecker();
|
||||
const host = new TypeScriptReflectionHost(checker);
|
||||
const targetDecl = host.getDeclarationOfIdentifier(Target);
|
||||
const directTargetDecl = host.getDeclarationOfIdentifier(DirectTarget);
|
||||
if (targetDecl === null) {
|
||||
return fail('No declaration found for Target');
|
||||
} else if (directTargetDecl === null) {
|
||||
return fail('No declaration found for DirectTarget');
|
||||
}
|
||||
expect(targetDecl.node.getSourceFile().fileName).toBe('/node_modules/absolute/index.ts');
|
||||
expect(ts.isClassDeclaration(targetDecl.node)).toBe(true);
|
||||
expect(directTargetDecl.viaModule).toBe('absolute');
|
||||
expect(directTargetDecl.node).toBe(targetDecl.node);
|
||||
const checker = program.getTypeChecker();
|
||||
const host = new TypeScriptReflectionHost(checker);
|
||||
const targetDecl = host.getDeclarationOfIdentifier(Target);
|
||||
const directTargetDecl = host.getDeclarationOfIdentifier(DirectTarget);
|
||||
if (targetDecl === null) {
|
||||
return fail('No declaration found for Target');
|
||||
} else if (directTargetDecl === null) {
|
||||
return fail('No declaration found for DirectTarget');
|
||||
}
|
||||
expect(targetDecl.node.getSourceFile().fileName).toBe(_('/node_modules/absolute/index.ts'));
|
||||
expect(ts.isClassDeclaration(targetDecl.node)).toBe(true);
|
||||
expect(directTargetDecl.viaModule).toBe('absolute');
|
||||
expect(directTargetDecl.node).toBe(targetDecl.node);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function expectParameter(
|
||||
param: CtorParameter, name: string, type?: string | {name: string, moduleName: string},
|
||||
decorator?: string, decoratorFrom?: string): void {
|
||||
expect(param.name !).toEqual(name);
|
||||
if (type === undefined) {
|
||||
expect(param.typeValueReference).toBeNull();
|
||||
} else {
|
||||
if (param.typeValueReference === null) {
|
||||
return fail(`Expected parameter ${name} to have a typeValueReference`);
|
||||
}
|
||||
if (param.typeValueReference.local && typeof type === 'string') {
|
||||
expect(argExpressionToString(param.typeValueReference.expression)).toEqual(type);
|
||||
} else if (!param.typeValueReference.local && typeof type !== 'string') {
|
||||
expect(param.typeValueReference.moduleName).toEqual(type.moduleName);
|
||||
expect(param.typeValueReference.name).toEqual(type.name);
|
||||
function expectParameter(
|
||||
param: CtorParameter, name: string, type?: string | {name: string, moduleName: string},
|
||||
decorator?: string, decoratorFrom?: string): void {
|
||||
expect(param.name !).toEqual(name);
|
||||
if (type === undefined) {
|
||||
expect(param.typeValueReference).toBeNull();
|
||||
} else {
|
||||
return fail(
|
||||
`Mismatch between typeValueReference and expected type: ${param.name} / ${param.typeValueReference.local}`);
|
||||
if (param.typeValueReference === null) {
|
||||
return fail(`Expected parameter ${name} to have a typeValueReference`);
|
||||
}
|
||||
if (param.typeValueReference.local && typeof type === 'string') {
|
||||
expect(argExpressionToString(param.typeValueReference.expression)).toEqual(type);
|
||||
} else if (!param.typeValueReference.local && typeof type !== 'string') {
|
||||
expect(param.typeValueReference.moduleName).toEqual(type.moduleName);
|
||||
expect(param.typeValueReference.name).toEqual(type.name);
|
||||
} else {
|
||||
return fail(
|
||||
`Mismatch between typeValueReference and expected type: ${param.name} / ${param.typeValueReference.local}`);
|
||||
}
|
||||
}
|
||||
if (decorator !== undefined) {
|
||||
expect(param.decorators).not.toBeNull();
|
||||
expect(param.decorators !.length).toBeGreaterThan(0);
|
||||
expect(param.decorators !.some(
|
||||
dec => dec.name === decorator && dec.import !== null &&
|
||||
dec.import.from === decoratorFrom))
|
||||
.toBe(true);
|
||||
}
|
||||
}
|
||||
if (decorator !== undefined) {
|
||||
expect(param.decorators).not.toBeNull();
|
||||
expect(param.decorators !.length).toBeGreaterThan(0);
|
||||
expect(param.decorators !.some(
|
||||
dec => dec.name === decorator && dec.import !== null &&
|
||||
dec.import.from === decoratorFrom))
|
||||
.toBe(true);
|
||||
}
|
||||
}
|
||||
|
||||
function argExpressionToString(name: ts.Node | null): string {
|
||||
if (name == null) {
|
||||
throw new Error('\'name\' argument can\'t be null');
|
||||
}
|
||||
function argExpressionToString(name: ts.Node | null): string {
|
||||
if (name == null) {
|
||||
throw new Error('\'name\' argument can\'t be null');
|
||||
}
|
||||
|
||||
if (ts.isIdentifier(name)) {
|
||||
return name.text;
|
||||
} else if (ts.isPropertyAccessExpression(name)) {
|
||||
return `${argExpressionToString(name.expression)}.${name.name.text}`;
|
||||
} else {
|
||||
throw new Error(`Unexpected node in arg expression: ${ts.SyntaxKind[name.kind]}.`);
|
||||
if (ts.isIdentifier(name)) {
|
||||
return name.text;
|
||||
} else if (ts.isPropertyAccessExpression(name)) {
|
||||
return `${argExpressionToString(name.expression)}.${name.name.text}`;
|
||||
} else {
|
||||
throw new Error(`Unexpected node in arg expression: ${ts.SyntaxKind[name.kind]}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
Reference in New Issue
Block a user