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:
Pete Bacon Darwin
2019-06-06 20:22:32 +01:00
committed by Kara Erickson
parent 1e7e065423
commit 7186f9c016
177 changed files with 16598 additions and 14829 deletions

View File

@ -7,7 +7,6 @@
* found in the LICENSE file at https://angular.io/license
*/
/**
* Extract i18n messages from source code
*/
@ -16,11 +15,12 @@ import 'reflect-metadata';
import * as api from './transformers/api';
import {ParsedConfiguration} from './perform_compile';
import {main, readCommandLineAndConfiguration} from './main';
import {setFileSystem, NodeJSFileSystem} from './ngtsc/file_system';
export function mainXi18n(
args: string[], consoleError: (msg: string) => void = console.error): number {
const config = readXi18nCommandLineAndConfiguration(args);
return main(args, consoleError, config);
return main(args, consoleError, config, undefined, undefined, undefined);
}
function readXi18nCommandLineAndConfiguration(args: string[]): ParsedConfiguration {
@ -42,5 +42,7 @@ function readXi18nCommandLineAndConfiguration(args: string[]): ParsedConfigurati
// Entry point
if (require.main === module) {
const args = process.argv.slice(2);
// We are running the real compiler so run against the real file-system
setFileSystem(new NodeJSFileSystem());
process.exitCode = mainXi18n(args);
}

View File

@ -17,8 +17,9 @@ import {replaceTsWithNgInErrors} from './ngtsc/diagnostics';
import * as api from './transformers/api';
import {GENERATED_FILES} from './transformers/util';
import {exitCodeFromResult, performCompilation, readConfiguration, formatDiagnostics, Diagnostics, ParsedConfiguration, PerformCompilationResult, filterErrorsAndWarnings} from './perform_compile';
import {exitCodeFromResult, performCompilation, readConfiguration, formatDiagnostics, Diagnostics, ParsedConfiguration, filterErrorsAndWarnings} from './perform_compile';
import {performWatchCompilation, createPerformWatchHost} from './perform_watch';
import {NodeJSFileSystem, setFileSystem} from './ngtsc/file_system';
export function main(
args: string[], consoleError: (s: string) => void = console.error,
@ -227,5 +228,7 @@ export function watchMode(
// CLI entry point
if (require.main === module) {
const args = process.argv.slice(2);
// We are running the real compiler so run against the real file-system
setFileSystem(new NodeJSFileSystem());
process.exitCode = main(args);
}

View File

@ -25,7 +25,6 @@ import {ParseSourceSpan} from '@angular/compiler';
import * as ts from 'typescript';
import {formatDiagnostics as formatDiagnosticsOrig} from './perform_compile';
import {Program as ProgramOrig} from './transformers/api';
import {createCompilerHost as createCompilerOrig} from './transformers/compiler_host';
import {createProgram as createProgramOrig} from './transformers/program';

View File

@ -11,6 +11,7 @@ ts_library(
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/cycles",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/indexer",
"//packages/compiler-cli/src/ngtsc/metadata",

View File

@ -7,11 +7,11 @@
*/
import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, InterpolationConfig, LexerRange, ParseError, ParseSourceFile, ParseTemplateOptions, R3ComponentMetadata, R3TargetBinder, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
import * as path from 'path';
import * as ts from 'typescript';
import {CycleAnalyzer} from '../../cycles';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {absoluteFrom, relative} from '../../file_system';
import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
import {IndexingContext} from '../../indexer';
import {DirectiveMeta, MetadataReader, MetadataRegistry, extractDirectiveGuards} from '../../metadata';
@ -156,7 +156,7 @@ export class ComponentDecoratorHandler implements
// Go through the root directories for this project, and select the one with the smallest
// relative path representation.
const relativeContextFilePath = this.rootDirs.reduce<string|undefined>((previous, rootDir) => {
const candidate = path.posix.relative(rootDir, containingFile);
const candidate = relative(absoluteFrom(rootDir), absoluteFrom(containingFile));
if (previous === undefined || candidate.length < previous.length) {
return candidate;
} else {
@ -205,7 +205,7 @@ export class ComponentDecoratorHandler implements
/* escapedString */ false, options);
} else {
// Expect an inline template to be present.
const inlineTemplate = this._extractInlineTemplate(component, relativeContextFilePath);
const inlineTemplate = this._extractInlineTemplate(component, containingFile);
if (inlineTemplate === null) {
throw new FatalDiagnosticError(
ErrorCode.COMPONENT_MISSING_TEMPLATE, decorator.node,
@ -583,8 +583,7 @@ export class ComponentDecoratorHandler implements
}
}
private _extractInlineTemplate(
component: Map<string, ts.Expression>, relativeContextFilePath: string): {
private _extractInlineTemplate(component: Map<string, ts.Expression>, containingFile: string): {
templateStr: string,
templateUrl: string,
templateRange: LexerRange|undefined,
@ -606,7 +605,7 @@ export class ComponentDecoratorHandler implements
// strip
templateRange = getTemplateRange(templateExpr);
templateStr = templateExpr.getSourceFile().text;
templateUrl = relativeContextFilePath;
templateUrl = containingFile;
escapedString = true;
} else {
const resolvedTemplate = this.evaluator.evaluate(templateExpr);

View File

@ -8,7 +8,6 @@
import * as ts from 'typescript';
import {Reference} from '../../imports';
import {Declaration} from '../../reflection';
/**
* Implement this interface if you want DecoratorHandlers to register

View File

@ -14,15 +14,15 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/annotations",
"//packages/compiler-cli/src/ngtsc/cycles",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/scope",
"//packages/compiler-cli/src/ngtsc/testing",
"//packages/compiler-cli/src/ngtsc/translator",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//typescript",
],
)

View File

@ -7,12 +7,14 @@
*/
import {CycleAnalyzer, ImportGraph} from '../../cycles';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {absoluteFrom} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports';
import {CompoundMetadataReader, DtsMetadataReader, LocalMetadataRegistry} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator';
import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {getDeclaration, makeProgram} from '../../testing';
import {ResourceLoader} from '../src/api';
import {ComponentDecoratorHandler} from '../src/component';
@ -22,62 +24,64 @@ export class NoopResourceLoader implements ResourceLoader {
load(): string { throw new Error('Not implemented'); }
preload(): Promise<void>|undefined { throw new Error('Not implemented'); }
}
runInEachFileSystem(() => {
describe('ComponentDecoratorHandler', () => {
let _: typeof absoluteFrom;
beforeEach(() => _ = absoluteFrom);
describe('ComponentDecoratorHandler', () => {
it('should produce a diagnostic when @Component has non-literal argument', () => {
const {program, options, host} = makeProgram([
{
name: 'node_modules/@angular/core/index.d.ts',
contents: 'export const Component: any;',
},
{
name: 'entry.ts',
contents: `
it('should produce a diagnostic when @Component has non-literal argument', () => {
const {program, options, host} = makeProgram([
{
name: _('/node_modules/@angular/core/index.d.ts'),
contents: 'export const Component: any;',
},
{
name: _('/entry.ts'),
contents: `
import {Component} from '@angular/core';
const TEST = '';
@Component(TEST) class TestCmp {}
`
},
]);
const checker = program.getTypeChecker();
const reflectionHost = new TypeScriptReflectionHost(checker);
const evaluator = new PartialEvaluator(reflectionHost, checker);
const moduleResolver = new ModuleResolver(program, options, host);
const importGraph = new ImportGraph(moduleResolver);
const cycleAnalyzer = new CycleAnalyzer(importGraph);
const metaRegistry = new LocalMetadataRegistry();
const dtsReader = new DtsMetadataReader(checker, reflectionHost);
const scopeRegistry = new LocalModuleScopeRegistry(
metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]),
null);
const metaReader = new CompoundMetadataReader([metaRegistry, dtsReader]);
const refEmitter = new ReferenceEmitter([]);
},
]);
const checker = program.getTypeChecker();
const reflectionHost = new TypeScriptReflectionHost(checker);
const evaluator = new PartialEvaluator(reflectionHost, checker);
const moduleResolver = new ModuleResolver(program, options, host);
const importGraph = new ImportGraph(moduleResolver);
const cycleAnalyzer = new CycleAnalyzer(importGraph);
const metaRegistry = new LocalMetadataRegistry();
const dtsReader = new DtsMetadataReader(checker, reflectionHost);
const scopeRegistry = new LocalModuleScopeRegistry(
metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null),
new ReferenceEmitter([]), null);
const metaReader = new CompoundMetadataReader([metaRegistry, dtsReader]);
const refEmitter = new ReferenceEmitter([]);
const handler = new ComponentDecoratorHandler(
reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, false,
new NoopResourceLoader(), [''], false, true, moduleResolver, cycleAnalyzer, refEmitter,
NOOP_DEFAULT_IMPORT_RECORDER);
const TestCmp = getDeclaration(program, 'entry.ts', 'TestCmp', isNamedClassDeclaration);
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
if (detected === undefined) {
return fail('Failed to recognize @Component');
}
try {
handler.analyze(TestCmp, detected.metadata);
return fail('Analysis should have failed');
} catch (err) {
if (!(err instanceof FatalDiagnosticError)) {
return fail('Error should be a FatalDiagnosticError');
const handler = new ComponentDecoratorHandler(
reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, false,
new NoopResourceLoader(), [''], false, true, moduleResolver, cycleAnalyzer, refEmitter,
NOOP_DEFAULT_IMPORT_RECORDER);
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
if (detected === undefined) {
return fail('Failed to recognize @Component');
}
const diag = err.toDiagnostic();
expect(diag.code).toEqual(ivyCode(ErrorCode.DECORATOR_ARG_NOT_LITERAL));
expect(diag.file.fileName.endsWith('entry.ts')).toBe(true);
expect(diag.start).toBe(detected.metadata.args ![0].getStart());
}
try {
handler.analyze(TestCmp, detected.metadata);
return fail('Analysis should have failed');
} catch (err) {
if (!(err instanceof FatalDiagnosticError)) {
return fail('Error should be a FatalDiagnosticError');
}
const diag = err.toDiagnostic();
expect(diag.code).toEqual(ivyCode(ErrorCode.DECORATOR_ARG_NOT_LITERAL));
expect(diag.file.fileName.endsWith('entry.ts')).toBe(true);
expect(diag.start).toBe(detected.metadata.args ![0].getStart());
}
});
});
});
function ivyCode(code: ErrorCode): number {
return Number('-99' + code.valueOf());
}
function ivyCode(code: ErrorCode): number { return Number('-99' + code.valueOf()); }
});

View File

@ -5,25 +5,30 @@
* 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 {absoluteFrom} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports';
import {DtsMetadataReader, LocalMetadataRegistry} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator';
import {ClassDeclaration, TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {getDeclaration, makeProgram} from '../../testing';
import {DirectiveDecoratorHandler} from '../src/directive';
runInEachFileSystem(() => {
describe('DirectiveDecoratorHandler', () => {
let _: typeof absoluteFrom;
beforeEach(() => _ = absoluteFrom);
describe('DirectiveDecoratorHandler', () => {
it('should use the `ReflectionHost` to detect class inheritance', () => {
const {program} = makeProgram([
{
name: 'node_modules/@angular/core/index.d.ts',
contents: 'export const Directive: any;',
},
{
name: 'entry.ts',
contents: `
it('should use the `ReflectionHost` to detect class inheritance', () => {
const {program} = makeProgram([
{
name: _('/node_modules/@angular/core/index.d.ts'),
contents: 'export const Directive: any;',
},
{
name: _('/entry.ts'),
contents: `
import {Directive} from '@angular/core';
@Directive({selector: 'test-dir-1'})
@ -32,51 +37,53 @@ describe('DirectiveDecoratorHandler', () => {
@Directive({selector: 'test-dir-2'})
export class TestDir2 {}
`,
},
]);
},
]);
const checker = program.getTypeChecker();
const reflectionHost = new TestReflectionHost(checker);
const evaluator = new PartialEvaluator(reflectionHost, checker);
const metaReader = new LocalMetadataRegistry();
const dtsReader = new DtsMetadataReader(checker, reflectionHost);
const scopeRegistry = new LocalModuleScopeRegistry(
metaReader, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]),
null);
const handler = new DirectiveDecoratorHandler(
reflectionHost, evaluator, scopeRegistry, NOOP_DEFAULT_IMPORT_RECORDER, false);
const checker = program.getTypeChecker();
const reflectionHost = new TestReflectionHost(checker);
const evaluator = new PartialEvaluator(reflectionHost, checker);
const metaReader = new LocalMetadataRegistry();
const dtsReader = new DtsMetadataReader(checker, reflectionHost);
const scopeRegistry = new LocalModuleScopeRegistry(
metaReader, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]),
null);
const handler = new DirectiveDecoratorHandler(
reflectionHost, evaluator, scopeRegistry, NOOP_DEFAULT_IMPORT_RECORDER, false);
const analyzeDirective = (dirName: string) => {
const DirNode = getDeclaration(program, 'entry.ts', dirName, isNamedClassDeclaration);
const analyzeDirective = (dirName: string) => {
const DirNode = getDeclaration(program, _('/entry.ts'), dirName, isNamedClassDeclaration);
const detected = handler.detect(DirNode, reflectionHost.getDecoratorsOfDeclaration(DirNode));
if (detected === undefined) {
throw new Error(`Failed to recognize @Directive (${dirName}).`);
}
const detected =
handler.detect(DirNode, reflectionHost.getDecoratorsOfDeclaration(DirNode));
if (detected === undefined) {
throw new Error(`Failed to recognize @Directive (${dirName}).`);
}
const {analysis} = handler.analyze(DirNode, detected.metadata);
if (analysis === undefined) {
throw new Error(`Failed to analyze @Directive (${dirName}).`);
}
const {analysis} = handler.analyze(DirNode, detected.metadata);
if (analysis === undefined) {
throw new Error(`Failed to analyze @Directive (${dirName}).`);
}
return analysis;
};
return analysis;
};
// By default, `TestReflectionHost#hasBaseClass()` returns `false`.
const analysis1 = analyzeDirective('TestDir1');
expect(analysis1.meta.usesInheritance).toBe(false);
// By default, `TestReflectionHost#hasBaseClass()` returns `false`.
const analysis1 = analyzeDirective('TestDir1');
expect(analysis1.meta.usesInheritance).toBe(false);
// Tweak `TestReflectionHost#hasBaseClass()` to return true.
reflectionHost.hasBaseClassReturnValue = true;
// Tweak `TestReflectionHost#hasBaseClass()` to return true.
reflectionHost.hasBaseClassReturnValue = true;
const analysis2 = analyzeDirective('TestDir2');
expect(analysis2.meta.usesInheritance).toBe(true);
const analysis2 = analyzeDirective('TestDir2');
expect(analysis2.meta.usesInheritance).toBe(true);
});
});
// Helpers
class TestReflectionHost extends TypeScriptReflectionHost {
hasBaseClassReturnValue = false;
hasBaseClass(clazz: ClassDeclaration): boolean { return this.hasBaseClassReturnValue; }
}
});
// Helpers
class TestReflectionHost extends TypeScriptReflectionHost {
hasBaseClassReturnValue = false;
hasBaseClass(clazz: ClassDeclaration): boolean { return this.hasBaseClassReturnValue; }
}

View File

@ -5,53 +5,44 @@
* 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 {absoluteFrom, getSourceFileOrError} from '../../file_system';
import {TestFile, runInEachFileSystem} from '../../file_system/testing';
import {NOOP_DEFAULT_IMPORT_RECORDER, NoopImportRewriter} from '../../imports';
import {TypeScriptReflectionHost} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {getDeclaration, makeProgram} from '../../testing';
import {ImportManager, translateStatement} from '../../translator';
import {generateSetClassMetadataCall} from '../src/metadata';
const CORE = {
name: 'node_modules/@angular/core/index.d.ts',
contents: `
export declare function Input(...args: any[]): any;
export declare function Inject(...args: any[]): any;
export declare function Component(...args: any[]): any;
export declare class Injector {}
`
};
describe('ngtsc setClassMetadata converter', () => {
it('should convert decorated class metadata', () => {
const res = compileAndPrint(`
runInEachFileSystem(() => {
describe('ngtsc setClassMetadata converter', () => {
it('should convert decorated class metadata', () => {
const res = compileAndPrint(`
import {Component} from '@angular/core';
@Component('metadata') class Target {}
`);
expect(res).toEqual(
`/*@__PURE__*/ i0.ɵsetClassMetadata(Target, [{ type: Component, args: ['metadata'] }], null, null);`);
});
expect(res).toEqual(
`/*@__PURE__*/ i0.ɵsetClassMetadata(Target, [{ type: Component, args: ['metadata'] }], null, null);`);
});
it('should convert decorated class constructor parameter metadata', () => {
const res = compileAndPrint(`
it('should convert decorated class constructor parameter metadata', () => {
const res = compileAndPrint(`
import {Component, Inject, Injector} from '@angular/core';
const FOO = 'foo';
@Component('metadata') class Target {
constructor(@Inject(FOO) foo: any, bar: Injector) {}
}
`);
expect(res).toContain(
`function () { return [{ type: undefined, decorators: [{ type: Inject, args: [FOO] }] }, { type: i0.Injector }]; }, null);`);
});
expect(res).toContain(
`function () { return [{ type: undefined, decorators: [{ type: Inject, args: [FOO] }] }, { type: i0.Injector }]; }, null);`);
});
it('should convert decorated field metadata', () => {
const res = compileAndPrint(`
it('should convert decorated field metadata', () => {
const res = compileAndPrint(`
import {Component, Input} from '@angular/core';
@Component('metadata') class Target {
@Input() foo: string;
@ -60,35 +51,47 @@ describe('ngtsc setClassMetadata converter', () => {
notDecorated: string;
}
`);
expect(res).toContain(`{ foo: [{ type: Input }], bar: [{ type: Input, args: ['value'] }] })`);
});
expect(res).toContain(`{ foo: [{ type: Input }], bar: [{ type: Input, args: ['value'] }] })`);
});
it('should not convert non-angular decorators to metadata', () => {
const res = compileAndPrint(`
it('should not convert non-angular decorators to metadata', () => {
const res = compileAndPrint(`
declare function NotAComponent(...args: any[]): any;
@NotAComponent('metadata') class Target {}
`);
expect(res).toBe('');
expect(res).toBe('');
});
});
});
function compileAndPrint(contents: string): string {
const {program} = makeProgram([
CORE, {
name: 'index.ts',
contents,
function compileAndPrint(contents: string): string {
const _ = absoluteFrom;
const CORE: TestFile = {
name: _('/node_modules/@angular/core/index.d.ts'),
contents: `
export declare function Input(...args: any[]): any;
export declare function Inject(...args: any[]): any;
export declare function Component(...args: any[]): any;
export declare class Injector {}
`
};
const {program} = makeProgram([
CORE, {
name: _('/index.ts'),
contents,
}
]);
const host = new TypeScriptReflectionHost(program.getTypeChecker());
const target = getDeclaration(program, _('/index.ts'), 'Target', ts.isClassDeclaration);
const call = generateSetClassMetadataCall(target, host, NOOP_DEFAULT_IMPORT_RECORDER, false);
if (call === null) {
return '';
}
]);
const host = new TypeScriptReflectionHost(program.getTypeChecker());
const target = getDeclaration(program, 'index.ts', 'Target', ts.isClassDeclaration);
const call = generateSetClassMetadataCall(target, host, NOOP_DEFAULT_IMPORT_RECORDER, false);
if (call === null) {
return '';
const sf = getSourceFileOrError(program, _('/index.ts'));
const im = new ImportManager(new NoopImportRewriter(), 'i');
const tsStatement = translateStatement(call, im, NOOP_DEFAULT_IMPORT_RECORDER);
const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, tsStatement, sf);
return res.replace(/\s+/g, ' ');
}
const sf = program.getSourceFile('index.ts') !;
const im = new ImportManager(new NoopImportRewriter(), 'i');
const tsStatement = translateStatement(call, im, NOOP_DEFAULT_IMPORT_RECORDER);
const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, tsStatement, sf);
return res.replace(/\s+/g, ' ');
}
});

View File

@ -5,34 +5,36 @@
* 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 {WrappedNodeExpr} from '@angular/compiler';
import {R3Reference} from '@angular/compiler/src/compiler';
import * as ts from 'typescript';
import {absoluteFrom} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {LocalIdentifierStrategy, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports';
import {DtsMetadataReader, LocalMetadataRegistry} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator';
import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {getDeclaration, makeProgram} from '../../testing';
import {NgModuleDecoratorHandler} from '../src/ng_module';
import {NoopReferencesRegistry} from '../src/references_registry';
describe('NgModuleDecoratorHandler', () => {
it('should resolve forwardRef', () => {
const {program} = makeProgram([
{
name: 'node_modules/@angular/core/index.d.ts',
contents: `
runInEachFileSystem(() => {
describe('NgModuleDecoratorHandler', () => {
it('should resolve forwardRef', () => {
const _ = absoluteFrom;
const {program} = makeProgram([
{
name: _('/node_modules/@angular/core/index.d.ts'),
contents: `
export const Component: any;
export const NgModule: any;
export declare function forwardRef(fn: () => any): any;
`,
},
{
name: 'entry.ts',
contents: `
},
{
name: _('/entry.ts'),
contents: `
import {Component, forwardRef, NgModule} from '@angular/core';
@Component({
@ -50,37 +52,38 @@ describe('NgModuleDecoratorHandler', () => {
})
export class TestModule {}
`
},
]);
const checker = program.getTypeChecker();
const reflectionHost = new TypeScriptReflectionHost(checker);
const evaluator = new PartialEvaluator(reflectionHost, checker);
const referencesRegistry = new NoopReferencesRegistry();
const metaRegistry = new LocalMetadataRegistry();
const dtsReader = new DtsMetadataReader(checker, reflectionHost);
const scopeRegistry = new LocalModuleScopeRegistry(
metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]),
null);
const refEmitter = new ReferenceEmitter([new LocalIdentifierStrategy()]);
},
]);
const checker = program.getTypeChecker();
const reflectionHost = new TypeScriptReflectionHost(checker);
const evaluator = new PartialEvaluator(reflectionHost, checker);
const referencesRegistry = new NoopReferencesRegistry();
const metaRegistry = new LocalMetadataRegistry();
const dtsReader = new DtsMetadataReader(checker, reflectionHost);
const scopeRegistry = new LocalModuleScopeRegistry(
metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null),
new ReferenceEmitter([]), null);
const refEmitter = new ReferenceEmitter([new LocalIdentifierStrategy()]);
const handler = new NgModuleDecoratorHandler(
reflectionHost, evaluator, metaRegistry, scopeRegistry, referencesRegistry, false, null,
refEmitter, NOOP_DEFAULT_IMPORT_RECORDER);
const TestModule = getDeclaration(program, 'entry.ts', 'TestModule', isNamedClassDeclaration);
const detected =
handler.detect(TestModule, reflectionHost.getDecoratorsOfDeclaration(TestModule));
if (detected === undefined) {
return fail('Failed to recognize @NgModule');
}
const moduleDef = handler.analyze(TestModule, detected.metadata).analysis !.ngModuleDef;
const handler = new NgModuleDecoratorHandler(
reflectionHost, evaluator, metaRegistry, scopeRegistry, referencesRegistry, false, null,
refEmitter, NOOP_DEFAULT_IMPORT_RECORDER);
const TestModule =
getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration);
const detected =
handler.detect(TestModule, reflectionHost.getDecoratorsOfDeclaration(TestModule));
if (detected === undefined) {
return fail('Failed to recognize @NgModule');
}
const moduleDef = handler.analyze(TestModule, detected.metadata).analysis !.ngModuleDef;
expect(getReferenceIdentifierTexts(moduleDef.declarations)).toEqual(['TestComp']);
expect(getReferenceIdentifierTexts(moduleDef.exports)).toEqual(['TestComp']);
expect(getReferenceIdentifierTexts(moduleDef.imports)).toEqual(['TestModuleDependency']);
expect(getReferenceIdentifierTexts(moduleDef.declarations)).toEqual(['TestComp']);
expect(getReferenceIdentifierTexts(moduleDef.exports)).toEqual(['TestComp']);
expect(getReferenceIdentifierTexts(moduleDef.imports)).toEqual(['TestModuleDependency']);
function getReferenceIdentifierTexts(references: R3Reference[]) {
return references.map(ref => (ref.value as WrappedNodeExpr<ts.Identifier>).node.text);
}
function getReferenceIdentifierTexts(references: R3Reference[]) {
return references.map(ref => (ref.value as WrappedNodeExpr<ts.Identifier>).node.text);
}
});
});
});

View File

@ -11,6 +11,8 @@ ts_library(
deps = [
"//packages:types",
"//packages/compiler-cli/src/ngtsc/cycles",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/testing",
"@npm//typescript",

View File

@ -5,62 +5,66 @@
* 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 {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {ModuleResolver} from '../../imports';
import {CycleAnalyzer} from '../src/analyzer';
import {ImportGraph} from '../src/imports';
import {makeProgramFromGraph} from './util';
describe('cycle analyzer', () => {
it('should not detect a cycle when there isn\'t one', () => {
const {program, analyzer} = makeAnalyzer('a:b,c;b;c');
const b = program.getSourceFile('b.ts') !;
const c = program.getSourceFile('c.ts') !;
expect(analyzer.wouldCreateCycle(b, c)).toBe(false);
expect(analyzer.wouldCreateCycle(c, b)).toBe(false);
runInEachFileSystem(() => {
describe('cycle analyzer', () => {
let _: typeof absoluteFrom;
beforeEach(() => _ = absoluteFrom);
it('should not detect a cycle when there isn\'t one', () => {
const {program, analyzer} = makeAnalyzer('a:b,c;b;c');
const b = getSourceFileOrError(program, (_('/b.ts')));
const c = getSourceFileOrError(program, (_('/c.ts')));
expect(analyzer.wouldCreateCycle(b, c)).toBe(false);
expect(analyzer.wouldCreateCycle(c, b)).toBe(false);
});
it('should detect a simple cycle between two files', () => {
const {program, analyzer} = makeAnalyzer('a:b;b');
const a = getSourceFileOrError(program, (_('/a.ts')));
const b = getSourceFileOrError(program, (_('/b.ts')));
expect(analyzer.wouldCreateCycle(a, b)).toBe(false);
expect(analyzer.wouldCreateCycle(b, a)).toBe(true);
});
it('should detect a cycle with a re-export in the chain', () => {
const {program, analyzer} = makeAnalyzer('a:*b;b:c;c');
const a = getSourceFileOrError(program, (_('/a.ts')));
const c = getSourceFileOrError(program, (_('/c.ts')));
expect(analyzer.wouldCreateCycle(a, c)).toBe(false);
expect(analyzer.wouldCreateCycle(c, a)).toBe(true);
});
it('should detect a cycle in a more complex program', () => {
const {program, analyzer} = makeAnalyzer('a:*b,*c;b:*e,*f;c:*g,*h;e:f;f:c;g;h:g');
const b = getSourceFileOrError(program, (_('/b.ts')));
const g = getSourceFileOrError(program, (_('/g.ts')));
expect(analyzer.wouldCreateCycle(b, g)).toBe(false);
expect(analyzer.wouldCreateCycle(g, b)).toBe(true);
});
it('should detect a cycle caused by a synthetic edge', () => {
const {program, analyzer} = makeAnalyzer('a:b,c;b;c');
const b = getSourceFileOrError(program, (_('/b.ts')));
const c = getSourceFileOrError(program, (_('/c.ts')));
expect(analyzer.wouldCreateCycle(b, c)).toBe(false);
analyzer.recordSyntheticImport(c, b);
expect(analyzer.wouldCreateCycle(b, c)).toBe(true);
});
});
it('should detect a simple cycle between two files', () => {
const {program, analyzer} = makeAnalyzer('a:b;b');
const a = program.getSourceFile('a.ts') !;
const b = program.getSourceFile('b.ts') !;
expect(analyzer.wouldCreateCycle(a, b)).toBe(false);
expect(analyzer.wouldCreateCycle(b, a)).toBe(true);
});
it('should detect a cycle with a re-export in the chain', () => {
const {program, analyzer} = makeAnalyzer('a:*b;b:c;c');
const a = program.getSourceFile('a.ts') !;
const c = program.getSourceFile('c.ts') !;
expect(analyzer.wouldCreateCycle(a, c)).toBe(false);
expect(analyzer.wouldCreateCycle(c, a)).toBe(true);
});
it('should detect a cycle in a more complex program', () => {
const {program, analyzer} = makeAnalyzer('a:*b,*c;b:*e,*f;c:*g,*h;e:f;f:c;g;h:g');
const b = program.getSourceFile('b.ts') !;
const g = program.getSourceFile('g.ts') !;
expect(analyzer.wouldCreateCycle(b, g)).toBe(false);
expect(analyzer.wouldCreateCycle(g, b)).toBe(true);
});
it('should detect a cycle caused by a synthetic edge', () => {
const {program, analyzer} = makeAnalyzer('a:b,c;b;c');
const b = program.getSourceFile('b.ts') !;
const c = program.getSourceFile('c.ts') !;
expect(analyzer.wouldCreateCycle(b, c)).toBe(false);
analyzer.recordSyntheticImport(c, b);
expect(analyzer.wouldCreateCycle(b, c)).toBe(true);
});
function makeAnalyzer(graph: string): {program: ts.Program, analyzer: CycleAnalyzer} {
const {program, options, host} = makeProgramFromGraph(getFileSystem(), graph);
return {
program,
analyzer: new CycleAnalyzer(new ImportGraph(new ModuleResolver(program, options, host))),
};
}
});
function makeAnalyzer(graph: string): {program: ts.Program, analyzer: CycleAnalyzer} {
const {program, options, host} = makeProgramFromGraph(graph);
return {
program,
analyzer: new CycleAnalyzer(new ImportGraph(new ModuleResolver(program, options, host))),
};
}

View File

@ -5,59 +5,67 @@
* 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 {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {ModuleResolver} from '../../imports';
import {ImportGraph} from '../src/imports';
import {makeProgramFromGraph} from './util';
describe('import graph', () => {
it('should record imports of a simple program', () => {
const {program, graph} = makeImportGraph('a:b;b:c;c');
const a = program.getSourceFile('a.ts') !;
const b = program.getSourceFile('b.ts') !;
const c = program.getSourceFile('c.ts') !;
expect(importsToString(graph.importsOf(a))).toBe('b');
expect(importsToString(graph.importsOf(b))).toBe('c');
runInEachFileSystem(() => {
describe('import graph', () => {
let _: typeof absoluteFrom;
beforeEach(() => _ = absoluteFrom);
it('should record imports of a simple program', () => {
const {program, graph} = makeImportGraph('a:b;b:c;c');
const a = getSourceFileOrError(program, (_('/a.ts')));
const b = getSourceFileOrError(program, (_('/b.ts')));
const c = getSourceFileOrError(program, (_('/c.ts')));
expect(importsToString(graph.importsOf(a))).toBe('b');
expect(importsToString(graph.importsOf(b))).toBe('c');
});
it('should calculate transitive imports of a simple program', () => {
const {program, graph} = makeImportGraph('a:b;b:c;c');
const a = getSourceFileOrError(program, (_('/a.ts')));
const b = getSourceFileOrError(program, (_('/b.ts')));
const c = getSourceFileOrError(program, (_('/c.ts')));
expect(importsToString(graph.transitiveImportsOf(a))).toBe('a,b,c');
});
it('should calculate transitive imports in a more complex program (with a cycle)', () => {
const {program, graph} = makeImportGraph('a:*b,*c;b:*e,*f;c:*g,*h;e:f;f;g:e;h:g');
const c = getSourceFileOrError(program, (_('/c.ts')));
expect(importsToString(graph.transitiveImportsOf(c))).toBe('c,e,f,g,h');
});
it('should reflect the addition of a synthetic import', () => {
const {program, graph} = makeImportGraph('a:b,c,d;b;c;d:b');
const b = getSourceFileOrError(program, (_('/b.ts')));
const c = getSourceFileOrError(program, (_('/c.ts')));
const d = getSourceFileOrError(program, (_('/d.ts')));
expect(importsToString(graph.importsOf(b))).toEqual('');
expect(importsToString(graph.transitiveImportsOf(d))).toEqual('b,d');
graph.addSyntheticImport(b, c);
expect(importsToString(graph.importsOf(b))).toEqual('c');
expect(importsToString(graph.transitiveImportsOf(d))).toEqual('b,c,d');
});
});
it('should calculate transitive imports of a simple program', () => {
const {program, graph} = makeImportGraph('a:b;b:c;c');
const a = program.getSourceFile('a.ts') !;
const b = program.getSourceFile('b.ts') !;
const c = program.getSourceFile('c.ts') !;
expect(importsToString(graph.transitiveImportsOf(a))).toBe('a,b,c');
});
function makeImportGraph(graph: string): {program: ts.Program, graph: ImportGraph} {
const {program, options, host} = makeProgramFromGraph(getFileSystem(), graph);
return {
program,
graph: new ImportGraph(new ModuleResolver(program, options, host)),
};
}
it('should calculate transitive imports in a more complex program (with a cycle)', () => {
const {program, graph} = makeImportGraph('a:*b,*c;b:*e,*f;c:*g,*h;e:f;f;g:e;h:g');
const c = program.getSourceFile('c.ts') !;
expect(importsToString(graph.transitiveImportsOf(c))).toBe('c,e,f,g,h');
});
it('should reflect the addition of a synthetic import', () => {
const {program, graph} = makeImportGraph('a:b,c,d;b;c;d:b');
const b = program.getSourceFile('b.ts') !;
const c = program.getSourceFile('c.ts') !;
const d = program.getSourceFile('d.ts') !;
expect(importsToString(graph.importsOf(b))).toEqual('');
expect(importsToString(graph.transitiveImportsOf(d))).toEqual('b,d');
graph.addSyntheticImport(b, c);
expect(importsToString(graph.importsOf(b))).toEqual('c');
expect(importsToString(graph.transitiveImportsOf(d))).toEqual('b,c,d');
});
function importsToString(imports: Set<ts.SourceFile>): string {
const fs = getFileSystem();
return Array.from(imports)
.map(sf => fs.basename(sf.fileName).replace('.ts', ''))
.sort()
.join(',');
}
});
function makeImportGraph(graph: string): {program: ts.Program, graph: ImportGraph} {
const {program, options, host} = makeProgramFromGraph(graph);
return {
program,
graph: new ImportGraph(new ModuleResolver(program, options, host)),
};
}
function importsToString(imports: Set<ts.SourceFile>): string {
return Array.from(imports).map(sf => sf.fileName.substr(1).replace('.ts', '')).sort().join(',');
}

View File

@ -5,10 +5,10 @@
* 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 {makeProgram} from '../../testing/in_memory_typescript';
import {FileSystem} from '../../file_system';
import {TestFile} from '../../file_system/testing';
import {makeProgram} from '../../testing';
/**
* Construct a TS program consisting solely of an import graph, from a string-based representation
@ -31,12 +31,12 @@ import {makeProgram} from '../../testing/in_memory_typescript';
*
* represents a program where a.ts exports from b.ts and imports from c.ts.
*/
export function makeProgramFromGraph(graph: string): {
export function makeProgramFromGraph(fs: FileSystem, graph: string): {
program: ts.Program,
host: ts.CompilerHost,
options: ts.CompilerOptions,
} {
const files = graph.split(';').map(fileSegment => {
const files: TestFile[] = graph.split(';').map(fileSegment => {
const [name, importList] = fileSegment.split(':');
const contents = (importList ? importList.split(',') : [])
.map(i => {
@ -50,7 +50,7 @@ export function makeProgramFromGraph(graph: string): {
.join('\n') +
`export const ${name} = '${name}';\n`;
return {
name: `${name}.ts`,
name: fs.resolve(`/${name}.ts`),
contents,
};
});

View File

@ -10,7 +10,7 @@ ts_library(
module_name = "@angular/compiler-cli/src/ngtsc/entry_point",
deps = [
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/shims",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//@types/node",

View File

@ -8,9 +8,9 @@
/// <reference types="node" />
import * as path from 'path';
import * as ts from 'typescript';
import {AbsoluteFsPath, dirname, join} from '../../file_system';
import {ShimGenerator} from '../../shims';
import {relativePathBetween} from '../../util/src/path';
@ -18,11 +18,10 @@ export class FlatIndexGenerator implements ShimGenerator {
readonly flatIndexPath: string;
constructor(
readonly entryPoint: string, relativeFlatIndexPath: string,
readonly entryPoint: AbsoluteFsPath, relativeFlatIndexPath: string,
readonly moduleName: string|null) {
this.flatIndexPath = path.posix.join(path.posix.dirname(entryPoint), relativeFlatIndexPath)
.replace(/\.js$/, '') +
'.ts';
this.flatIndexPath =
join(dirname(entryPoint), relativeFlatIndexPath).replace(/\.js$/, '') + '.ts';
}
recognize(fileName: string): boolean { return fileName === this.flatIndexPath; }

View File

@ -6,15 +6,16 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AbsoluteFsPath} from '../../path/src/types';
import {AbsoluteFsPath, getFileSystem} from '../../file_system';
import {isNonDeclarationTsPath} from '../../util/src/typescript';
export function findFlatIndexEntryPoint(rootFiles: ReadonlyArray<AbsoluteFsPath>): string|null {
export function findFlatIndexEntryPoint(rootFiles: ReadonlyArray<AbsoluteFsPath>): AbsoluteFsPath|
null {
// There are two ways for a file to be recognized as the flat module index:
// 1) if it's the only file!!!!!!
// 2) (deprecated) if it's named 'index.ts' and has the shortest path of all such files.
const tsFiles = rootFiles.filter(file => isNonDeclarationTsPath(file));
let resolvedEntryPoint: string|null = null;
let resolvedEntryPoint: AbsoluteFsPath|null = null;
if (tsFiles.length === 1) {
// There's only one file - this is the flat module index.
@ -26,7 +27,7 @@ export function findFlatIndexEntryPoint(rootFiles: ReadonlyArray<AbsoluteFsPath>
//
// This behavior is DEPRECATED and only exists to support existing usages.
for (const tsFile of tsFiles) {
if (tsFile.endsWith('/index.ts') &&
if (getFileSystem().basename(tsFile) === 'index.ts' &&
(resolvedEntryPoint === null || tsFile.length <= resolvedEntryPoint.length)) {
resolvedEntryPoint = tsFile;
}

View File

@ -11,7 +11,8 @@ ts_library(
deps = [
"//packages:types",
"//packages/compiler-cli/src/ngtsc/entry_point",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"@npm//typescript",
],
)

View File

@ -6,24 +6,25 @@
* 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 {AbsoluteFsPath} from '../../path/src/types';
import {absoluteFrom} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {findFlatIndexEntryPoint} from '../src/logic';
describe('entry_point logic', () => {
runInEachFileSystem(() => {
describe('entry_point logic', () => {
let _: typeof absoluteFrom;
beforeEach(() => _ = absoluteFrom);
describe('findFlatIndexEntryPoint', () => {
describe('findFlatIndexEntryPoint', () => {
it('should use the only source file if only a single one is specified', () => {
expect(findFlatIndexEntryPoint([AbsoluteFsPath.fromUnchecked('/src/index.ts')]))
.toBe('/src/index.ts');
});
it('should use the only source file if only a single one is specified',
() => { expect(findFlatIndexEntryPoint([_('/src/index.ts')])).toBe(_('/src/index.ts')); });
it('should use the shortest source file ending with "index.ts" for multiple files', () => {
expect(findFlatIndexEntryPoint([
AbsoluteFsPath.fromUnchecked('/src/deep/index.ts'),
AbsoluteFsPath.fromUnchecked('/src/index.ts'), AbsoluteFsPath.fromUnchecked('/index.ts')
])).toBe('/index.ts');
it('should use the shortest source file ending with "index.ts" for multiple files', () => {
expect(findFlatIndexEntryPoint([
_('/src/deep/index.ts'), _('/src/index.ts'), _('/index.ts')
])).toBe(_('/index.ts'));
});
});
});
});

View File

@ -3,7 +3,7 @@ package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ts_library")
ts_library(
name = "path",
name = "file_system",
srcs = ["index.ts"] + glob([
"src/*.ts",
]),

View File

@ -0,0 +1,42 @@
# Virtual file-system layer
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 supplied.
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 helper method,
`getFileSystem()`. This is also used by a number of helper
methods 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 helper 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 `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, it calls `initMockFileSystem()`
for each OS to emulate.
* `loadTestFiles()` - use this to add files and their contents
to the mock file system for testing.
* `loadStandardTestFiles()` - 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.

View File

@ -0,0 +1,13 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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
*/
export {NgtscCompilerHost} from './src/compiler_host';
export {absoluteFrom, absoluteFromSourceFile, basename, dirname, getFileSystem, isRoot, join, relative, relativeFrom, resolve, setFileSystem} from './src/helpers';
export {LogicalFileSystem, LogicalProjectPath} from './src/logical';
export {NodeJSFileSystem} from './src/node_js_file_system';
export {AbsoluteFsPath, FileStats, FileSystem, PathSegment, PathString} from './src/types';
export {getSourceFileOrError} from './src/util';

View File

@ -0,0 +1,71 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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
*/
/// <reference types="node" />
import * as os from 'os';
import * as ts from 'typescript';
import {absoluteFrom} from './helpers';
import {FileSystem} from './types';
export class NgtscCompilerHost implements ts.CompilerHost {
constructor(protected fs: FileSystem, protected options: ts.CompilerOptions = {}) {}
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined {
const text = this.readFile(fileName);
return text !== undefined ? ts.createSourceFile(fileName, text, languageVersion, true) :
undefined;
}
getDefaultLibFileName(options: ts.CompilerOptions): string {
return this.fs.join(this.getDefaultLibLocation(), ts.getDefaultLibFileName(options));
}
getDefaultLibLocation(): string { return this.fs.getDefaultLibLocation(); }
writeFile(
fileName: string, data: string, writeByteOrderMark: boolean,
onError: ((message: string) => void)|undefined,
sourceFiles?: ReadonlyArray<ts.SourceFile>): void {
const path = absoluteFrom(fileName);
this.fs.ensureDir(this.fs.dirname(path));
this.fs.writeFile(path, data);
}
getCurrentDirectory(): string { return this.fs.pwd(); }
getCanonicalFileName(fileName: string): string {
return this.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase();
}
useCaseSensitiveFileNames(): boolean { return this.fs.isCaseSensitive(); }
getNewLine(): string {
switch (this.options.newLine) {
case ts.NewLineKind.CarriageReturnLineFeed:
return '\r\n';
case ts.NewLineKind.LineFeed:
return '\n';
default:
return os.EOL;
}
}
fileExists(fileName: string): boolean {
const absPath = this.fs.resolve(fileName);
return this.fs.exists(absPath);
}
readFile(fileName: string): string|undefined {
const absPath = this.fs.resolve(fileName);
if (!this.fileExists(absPath)) {
return undefined;
}
return this.fs.readFile(absPath);
}
}

View File

@ -0,0 +1,88 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {InvalidFileSystem} from './invalid_file_system';
import {AbsoluteFsPath, FileSystem, PathSegment, PathString} from './types';
import {normalizeSeparators} from './util';
let fs: FileSystem = new InvalidFileSystem();
export function getFileSystem(): FileSystem {
return fs;
}
export function setFileSystem(fileSystem: FileSystem) {
fs = fileSystem;
}
/**
* Convert the path `path` to an `AbsoluteFsPath`, throwing an error if it's not an absolute path.
*/
export function absoluteFrom(path: string): AbsoluteFsPath {
if (!fs.isRooted(path)) {
throw new Error(`Internal Error: absoluteFrom(${path}): path is not absolute`);
}
return fs.resolve(path);
}
/**
* Extract an `AbsoluteFsPath` from a `ts.SourceFile`.
*/
export function absoluteFromSourceFile(sf: ts.SourceFile): AbsoluteFsPath {
return fs.resolve(sf.fileName);
}
/**
* Convert the path `path` to a `PathSegment`, throwing an error if it's not a relative path.
*/
export function relativeFrom(path: string): PathSegment {
const normalized = normalizeSeparators(path);
if (fs.isRooted(normalized)) {
throw new Error(`Internal Error: relativeFrom(${path}): path is not relative`);
}
return normalized as PathSegment;
}
/**
* Static access to `dirname`.
*/
export function dirname<T extends PathString>(file: T): T {
return fs.dirname(file);
}
/**
* Static access to `join`.
*/
export function join<T extends PathString>(basePath: T, ...paths: string[]): T {
return fs.join(basePath, ...paths);
}
/**
* Static access to `resolve`s.
*/
export function resolve(basePath: string, ...paths: string[]): AbsoluteFsPath {
return fs.resolve(basePath, ...paths);
}
/** Returns true when the path provided is the root path. */
export function isRoot(path: AbsoluteFsPath): boolean {
return fs.isRoot(path);
}
/**
* Static access to `relative`.
*/
export function relative<T extends PathString>(from: T, to: T): PathSegment {
return fs.relative(from, to);
}
/**
* Static access to `basename`.
*/
export function basename(filePath: PathString, extension?: string): PathSegment {
return fs.basename(filePath, extension) as PathSegment;
}

View File

@ -0,0 +1,48 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {AbsoluteFsPath, FileStats, FileSystem, PathSegment, PathString} from './types';
/**
* The default `FileSystem` that will always fail.
*
* This is a way of ensuring that the developer consciously chooses and
* configures the `FileSystem` before using it; particularly important when
* considering static functions like `absoluteFrom()` which rely on
* the `FileSystem` under the hood.
*/
export class InvalidFileSystem implements FileSystem {
exists(path: AbsoluteFsPath): boolean { throw makeError(); }
readFile(path: AbsoluteFsPath): string { throw makeError(); }
writeFile(path: AbsoluteFsPath, data: string): void { throw makeError(); }
symlink(target: AbsoluteFsPath, path: AbsoluteFsPath): void { throw makeError(); }
readdir(path: AbsoluteFsPath): PathSegment[] { throw makeError(); }
lstat(path: AbsoluteFsPath): FileStats { throw makeError(); }
stat(path: AbsoluteFsPath): FileStats { throw makeError(); }
pwd(): AbsoluteFsPath { throw makeError(); }
extname(path: AbsoluteFsPath|PathSegment): string { throw makeError(); }
copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { throw makeError(); }
moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { throw makeError(); }
mkdir(path: AbsoluteFsPath): void { throw makeError(); }
ensureDir(path: AbsoluteFsPath): void { throw makeError(); }
isCaseSensitive(): boolean { throw makeError(); }
resolve(...paths: string[]): AbsoluteFsPath { throw makeError(); }
dirname<T extends PathString>(file: T): T { throw makeError(); }
join<T extends PathString>(basePath: T, ...paths: string[]): T { throw makeError(); }
isRoot(path: AbsoluteFsPath): boolean { throw makeError(); }
isRooted(path: string): boolean { throw makeError(); }
relative<T extends PathString>(from: T, to: T): PathSegment { throw makeError(); }
basename(filePath: string, extension?: string): PathSegment { throw makeError(); }
realpath(filePath: AbsoluteFsPath): AbsoluteFsPath { throw makeError(); }
getDefaultLibLocation(): AbsoluteFsPath { throw makeError(); }
normalize<T extends PathString>(path: T): T { throw makeError(); }
}
function makeError() {
return new Error(
'FileSystem has not been configured. Please call `setFileSystem()` before calling this method.');
}

View File

@ -5,15 +5,14 @@
* 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
*/
/// <reference types="node" />
import * as path from 'path';
import * as ts from 'typescript';
import {absoluteFrom, dirname, relative, resolve} from './helpers';
import {AbsoluteFsPath, BrandedPath, PathSegment} from './types';
import {stripExtension} from './util';
/**
* A path that's relative to the logical root of a TypeScript project (one of the project's
* rootDirs).
@ -30,9 +29,9 @@ export const LogicalProjectPath = {
* importing from `to`.
*/
relativePathBetween: function(from: LogicalProjectPath, to: LogicalProjectPath): PathSegment {
let relativePath = path.posix.relative(path.posix.dirname(from), to);
let relativePath = relative(dirname(resolve(from)), resolve(to));
if (!relativePath.startsWith('../')) {
relativePath = ('./' + relativePath);
relativePath = ('./' + relativePath) as PathSegment;
}
return relativePath as PathSegment;
},
@ -64,10 +63,10 @@ export class LogicalFileSystem {
* Get the logical path in the project of a `ts.SourceFile`.
*
* This method is provided as a convenient alternative to calling
* `logicalPathOfFile(AbsoluteFsPath.fromSourceFile(sf))`.
* `logicalPathOfFile(absoluteFromSourceFile(sf))`.
*/
logicalPathOfSf(sf: ts.SourceFile): LogicalProjectPath|null {
return this.logicalPathOfFile(AbsoluteFsPath.from(sf.fileName));
return this.logicalPathOfFile(absoluteFrom(sf.fileName));
}
/**

View File

@ -0,0 +1,81 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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
*/
/// <reference types="node" />
import * as fs from 'fs';
import * as p from 'path';
import {absoluteFrom, relativeFrom} from './helpers';
import {AbsoluteFsPath, FileStats, FileSystem, PathSegment, PathString} from './types';
/**
* A wrapper around the Node.js file-system (i.e the `fs` package).
*/
export class NodeJSFileSystem implements FileSystem {
private _caseSensitive: boolean|undefined = undefined;
exists(path: AbsoluteFsPath): boolean { return fs.existsSync(path); }
readFile(path: AbsoluteFsPath): string { return fs.readFileSync(path, 'utf8'); }
writeFile(path: AbsoluteFsPath, data: string): void {
return fs.writeFileSync(path, data, 'utf8');
}
symlink(target: AbsoluteFsPath, path: AbsoluteFsPath): void { fs.symlinkSync(target, path); }
readdir(path: AbsoluteFsPath): PathSegment[] { return fs.readdirSync(path) as PathSegment[]; }
lstat(path: AbsoluteFsPath): FileStats { return fs.lstatSync(path); }
stat(path: AbsoluteFsPath): FileStats { return fs.statSync(path); }
pwd(): AbsoluteFsPath { return this.normalize(process.cwd()) as AbsoluteFsPath; }
copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { fs.copyFileSync(from, to); }
moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { fs.renameSync(from, to); }
mkdir(path: AbsoluteFsPath): void { fs.mkdirSync(path); }
ensureDir(path: AbsoluteFsPath): void {
const parents: AbsoluteFsPath[] = [];
while (!this.isRoot(path) && !this.exists(path)) {
parents.push(path);
path = this.dirname(path);
}
while (parents.length) {
this.mkdir(parents.pop() !);
}
}
isCaseSensitive(): boolean {
if (this._caseSensitive === undefined) {
this._caseSensitive = this.exists(togglePathCase(__filename));
}
return this._caseSensitive;
}
resolve(...paths: string[]): AbsoluteFsPath {
return this.normalize(p.resolve(...paths)) as AbsoluteFsPath;
}
dirname<T extends string>(file: T): T { return this.normalize(p.dirname(file)) as T; }
join<T extends string>(basePath: T, ...paths: string[]): T {
return this.normalize(p.join(basePath, ...paths)) as T;
}
isRoot(path: AbsoluteFsPath): boolean { return this.dirname(path) === this.normalize(path); }
isRooted(path: string): boolean { return p.isAbsolute(path); }
relative<T extends PathString>(from: T, to: T): PathSegment {
return relativeFrom(this.normalize(p.relative(from, to)));
}
basename(filePath: string, extension?: string): PathSegment {
return p.basename(filePath, extension) as PathSegment;
}
extname(path: AbsoluteFsPath|PathSegment): string { return p.extname(path); }
realpath(path: AbsoluteFsPath): AbsoluteFsPath { return this.resolve(fs.realpathSync(path)); }
getDefaultLibLocation(): AbsoluteFsPath {
return this.resolve(require.resolve('typescript'), '..');
}
normalize<T extends string>(path: T): T {
// Convert backslashes to forward slashes
return path.replace(/\\/g, '/') as T;
}
}
/**
* Toggle the case of each character in a file path.
*/
function togglePathCase(str: string): AbsoluteFsPath {
return absoluteFrom(
str.replace(/\w/g, ch => ch.toUpperCase() === ch ? ch.toLowerCase() : ch.toUpperCase()));
}

View File

@ -0,0 +1,74 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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
*/
/**
* A `string` representing a specific type of path, with a particular brand `B`.
*
* A `string` is not assignable to a `BrandedPath`, but a `BrandedPath` is assignable to a `string`.
* Two `BrandedPath`s with different brands are not mutually assignable.
*/
export type BrandedPath<B extends string> = string & {
_brand: B;
};
/**
* A fully qualified path in the file system, in POSIX form.
*/
export type AbsoluteFsPath = BrandedPath<'AbsoluteFsPath'>;
/**
* A path that's relative to another (unspecified) root.
*
* This does not necessarily have to refer to a physical file.
*/
export type PathSegment = BrandedPath<'PathSegment'>;
/**
* A basic interface to abstract the underlying file-system.
*
* This makes it easier to provide mock file-systems in unit tests,
* but also to create clever file-systems that have features such as caching.
*/
export interface FileSystem {
exists(path: AbsoluteFsPath): boolean;
readFile(path: AbsoluteFsPath): string;
writeFile(path: AbsoluteFsPath, data: string): void;
symlink(target: AbsoluteFsPath, path: AbsoluteFsPath): void;
readdir(path: AbsoluteFsPath): PathSegment[];
lstat(path: AbsoluteFsPath): FileStats;
stat(path: AbsoluteFsPath): FileStats;
pwd(): AbsoluteFsPath;
extname(path: AbsoluteFsPath|PathSegment): string;
copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void;
moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void;
mkdir(path: AbsoluteFsPath): void;
ensureDir(path: AbsoluteFsPath): void;
isCaseSensitive(): boolean;
isRoot(path: AbsoluteFsPath): boolean;
isRooted(path: string): boolean;
resolve(...paths: string[]): AbsoluteFsPath;
dirname<T extends PathString>(file: T): T;
join<T extends PathString>(basePath: T, ...paths: string[]): T;
relative<T extends PathString>(from: T, to: T): PathSegment;
basename(filePath: string, extension?: string): PathSegment;
realpath(filePath: AbsoluteFsPath): AbsoluteFsPath;
getDefaultLibLocation(): AbsoluteFsPath;
normalize<T extends PathString>(path: T): T;
}
export type PathString = string | AbsoluteFsPath | PathSegment;
/**
* Information about an object in the FileSystem.
* This is analogous to the `fs.Stats` class in Node.js.
*/
export interface FileStats {
isFile(): boolean;
isDirectory(): boolean;
isSymbolicLink(): boolean;
}

View File

@ -5,11 +5,10 @@
* 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
*/
// TODO(alxhub): Unify this file with `util/src/path`.
import * as ts from 'typescript';
import {AbsoluteFsPath} from './types';
const TS_DTS_JS_EXTENSION = /(?:\.d)?\.ts$|\.js$/;
const ABSOLUTE_PATH = /^([a-zA-Z]:\/|\/)/;
/**
* Convert Windows-style separators to POSIX separators.
@ -26,10 +25,11 @@ export function stripExtension(path: string): string {
return path.replace(TS_DTS_JS_EXTENSION, '');
}
/**
* Returns true if the normalized path is an absolute path.
*/
export function isAbsolutePath(path: string): boolean {
// TODO: use regExp based on OS in the future
return ABSOLUTE_PATH.test(path);
export function getSourceFileOrError(program: ts.Program, fileName: AbsoluteFsPath): ts.SourceFile {
const sf = program.getSourceFile(fileName);
if (sf === undefined) {
throw new Error(
`Program does not contain "${fileName}" - available files are ${program.getSourceFiles().map(sf => sf.fileName).join(', ')}`);
}
return sf;
}

View File

@ -10,7 +10,8 @@ ts_library(
]),
deps = [
"//packages:types",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
],
)

View File

@ -0,0 +1,47 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 os from 'os';
import {absoluteFrom, relativeFrom, setFileSystem} from '../src/helpers';
import {NodeJSFileSystem} from '../src/node_js_file_system';
describe('path types', () => {
beforeEach(() => { setFileSystem(new NodeJSFileSystem()); });
describe('absoluteFrom', () => {
it('should not throw when creating one from an absolute path',
() => { expect(() => absoluteFrom('/test.txt')).not.toThrow(); });
if (os.platform() === 'win32') {
it('should not throw when creating one from a windows absolute path',
() => { expect(absoluteFrom('C:\\test.txt')).toEqual('C:/test.txt'); });
it('should not throw when creating one from a windows absolute path with POSIX separators',
() => { expect(absoluteFrom('C:/test.txt')).toEqual('C:/test.txt'); });
it('should support windows drive letters',
() => { expect(absoluteFrom('D:\\foo\\test.txt')).toEqual('D:/foo/test.txt'); });
it('should convert Windows path separators to POSIX separators',
() => { expect(absoluteFrom('C:\\foo\\test.txt')).toEqual('C:/foo/test.txt'); });
}
it('should throw when creating one from a non-absolute path',
() => { expect(() => absoluteFrom('test.txt')).toThrow(); });
});
describe('relativeFrom', () => {
it('should not throw when creating one from a relative path',
() => { expect(() => relativeFrom('a/b/c.txt')).not.toThrow(); });
it('should throw when creating one from an absolute path',
() => { expect(() => relativeFrom('/a/b/c.txt')).toThrow(); });
if (os.platform() === 'win32') {
it('should throw when creating one from a Windows absolute path',
() => { expect(() => relativeFrom('C:/a/b/c.txt')).toThrow(); });
}
});
});

View File

@ -0,0 +1,65 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {absoluteFrom} from '../src/helpers';
import {LogicalFileSystem, LogicalProjectPath} from '../src/logical';
import {runInEachFileSystem} from '../testing';
runInEachFileSystem(() => {
describe('logical paths', () => {
let _: typeof absoluteFrom;
beforeEach(() => _ = absoluteFrom);
describe('LogicalFileSystem', () => {
it('should determine logical paths in a single root file system', () => {
const fs = new LogicalFileSystem([_('/test')]);
expect(fs.logicalPathOfFile(_('/test/foo/foo.ts')))
.toEqual('/foo/foo' as LogicalProjectPath);
expect(fs.logicalPathOfFile(_('/test/bar/bar.ts')))
.toEqual('/bar/bar' as LogicalProjectPath);
expect(fs.logicalPathOfFile(_('/not-test/bar.ts'))).toBeNull();
});
it('should determine logical paths in a multi-root file system', () => {
const fs = new LogicalFileSystem([_('/test/foo'), _('/test/bar')]);
expect(fs.logicalPathOfFile(_('/test/foo/foo.ts'))).toEqual('/foo' as LogicalProjectPath);
expect(fs.logicalPathOfFile(_('/test/bar/bar.ts'))).toEqual('/bar' as LogicalProjectPath);
});
it('should continue to work when one root is a child of another', () => {
const fs = new LogicalFileSystem([_('/test'), _('/test/dist')]);
expect(fs.logicalPathOfFile(_('/test/foo.ts'))).toEqual('/foo' as LogicalProjectPath);
expect(fs.logicalPathOfFile(_('/test/dist/foo.ts'))).toEqual('/foo' as LogicalProjectPath);
});
it('should always return `/` prefixed logical paths', () => {
const rootFs = new LogicalFileSystem([_('/')]);
expect(rootFs.logicalPathOfFile(_('/foo/foo.ts')))
.toEqual('/foo/foo' as LogicalProjectPath);
const nonRootFs = new LogicalFileSystem([_('/test/')]);
expect(nonRootFs.logicalPathOfFile(_('/test/foo/foo.ts')))
.toEqual('/foo/foo' as LogicalProjectPath);
});
});
describe('utilities', () => {
it('should give a relative path between two adjacent logical files', () => {
const res = LogicalProjectPath.relativePathBetween(
'/foo' as LogicalProjectPath, '/bar' as LogicalProjectPath);
expect(res).toEqual('./bar');
});
it('should give a relative path between two non-adjacent logical files', () => {
const res = LogicalProjectPath.relativePathBetween(
'/foo/index' as LogicalProjectPath, '/bar/index' as LogicalProjectPath);
expect(res).toEqual('../bar/index');
});
});
});
});

View File

@ -0,0 +1,148 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 realFs from 'fs';
import {absoluteFrom, relativeFrom, setFileSystem} from '../src/helpers';
import {NodeJSFileSystem} from '../src/node_js_file_system';
import {AbsoluteFsPath} from '../src/types';
describe('NodeJSFileSystem', () => {
let fs: NodeJSFileSystem;
let abcPath: AbsoluteFsPath;
let xyzPath: AbsoluteFsPath;
beforeEach(() => {
fs = new NodeJSFileSystem();
// Set the file-system so that calls like `absoluteFrom()`
// and `relativeFrom()` work correctly.
setFileSystem(fs);
abcPath = absoluteFrom('/a/b/c');
xyzPath = absoluteFrom('/x/y/z');
});
describe('exists()', () => {
it('should delegate to fs.existsSync()', () => {
const spy = spyOn(realFs, 'existsSync').and.returnValues(true, false);
expect(fs.exists(abcPath)).toBe(true);
expect(spy).toHaveBeenCalledWith(abcPath);
expect(fs.exists(xyzPath)).toBe(false);
expect(spy).toHaveBeenCalledWith(xyzPath);
});
});
describe('readFile()', () => {
it('should delegate to fs.readFileSync()', () => {
const spy = spyOn(realFs, 'readFileSync').and.returnValue('Some contents');
const result = fs.readFile(abcPath);
expect(result).toBe('Some contents');
expect(spy).toHaveBeenCalledWith(abcPath, 'utf8');
});
});
describe('writeFile()', () => {
it('should delegate to fs.writeFileSync()', () => {
const spy = spyOn(realFs, 'writeFileSync');
fs.writeFile(abcPath, 'Some contents');
expect(spy).toHaveBeenCalledWith(abcPath, 'Some contents', 'utf8');
});
});
describe('readdir()', () => {
it('should delegate to fs.readdirSync()', () => {
const spy = spyOn(realFs, 'readdirSync').and.returnValue(['x', 'y/z']);
const result = fs.readdir(abcPath);
expect(result).toEqual([relativeFrom('x'), relativeFrom('y/z')]);
expect(spy).toHaveBeenCalledWith(abcPath);
});
});
describe('lstat()', () => {
it('should delegate to fs.lstatSync()', () => {
const stats = new realFs.Stats();
const spy = spyOn(realFs, 'lstatSync').and.returnValue(stats);
const result = fs.lstat(abcPath);
expect(result).toBe(stats);
expect(spy).toHaveBeenCalledWith(abcPath);
});
});
describe('stat()', () => {
it('should delegate to fs.statSync()', () => {
const stats = new realFs.Stats();
const spy = spyOn(realFs, 'statSync').and.returnValue(stats);
const result = fs.stat(abcPath);
expect(result).toBe(stats);
expect(spy).toHaveBeenCalledWith(abcPath);
});
});
describe('pwd()', () => {
it('should delegate to process.cwd()', () => {
const spy = spyOn(process, 'cwd').and.returnValue(abcPath);
const result = fs.pwd();
expect(result).toEqual(abcPath);
expect(spy).toHaveBeenCalledWith();
});
});
describe('copyFile()', () => {
it('should delegate to fs.copyFileSync()', () => {
const spy = spyOn(realFs, 'copyFileSync');
fs.copyFile(abcPath, xyzPath);
expect(spy).toHaveBeenCalledWith(abcPath, xyzPath);
});
});
describe('moveFile()', () => {
it('should delegate to fs.renameSync()', () => {
const spy = spyOn(realFs, 'renameSync');
fs.moveFile(abcPath, xyzPath);
expect(spy).toHaveBeenCalledWith(abcPath, xyzPath);
});
});
describe('mkdir()', () => {
it('should delegate to fs.mkdirSync()', () => {
const spy = spyOn(realFs, 'mkdirSync');
fs.mkdir(xyzPath);
expect(spy).toHaveBeenCalledWith(xyzPath);
});
});
describe('ensureDir()', () => {
it('should call exists() and fs.mkdir()', () => {
const aPath = absoluteFrom('/a');
const abPath = absoluteFrom('/a/b');
const xPath = absoluteFrom('/x');
const xyPath = absoluteFrom('/x/y');
const mkdirCalls: string[] = [];
const existsCalls: string[] = [];
spyOn(realFs, 'mkdirSync').and.callFake((path: string) => mkdirCalls.push(path));
spyOn(fs, 'exists').and.callFake((path: AbsoluteFsPath) => {
existsCalls.push(path);
switch (path) {
case aPath:
return true;
case abPath:
return true;
default:
return false;
}
});
fs.ensureDir(abcPath);
expect(existsCalls).toEqual([abcPath, abPath]);
expect(mkdirCalls).toEqual([abcPath]);
mkdirCalls.length = 0;
existsCalls.length = 0;
fs.ensureDir(xyzPath);
expect(existsCalls).toEqual([xyzPath, xyPath, xPath]);
expect(mkdirCalls).toEqual([xPath, xyPath, xyzPath]);
});
});
});

View File

@ -0,0 +1,16 @@
load("//tools:defaults.bzl", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "testing",
testonly = True,
srcs = glob([
"**/*.ts",
]),
deps = [
"//packages:types",
"//packages/compiler-cli/src/ngtsc/file_system",
"@npm//typescript",
],
)

View File

@ -0,0 +1,13 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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
*/
export {Folder, MockFileSystem} from './src/mock_file_system';
export {MockFileSystemNative} from './src/mock_file_system_native';
export {MockFileSystemPosix} from './src/mock_file_system_posix';
export {MockFileSystemWindows} from './src/mock_file_system_windows';
export {TestFile, initMockFileSystem, runInEachFileSystem} from './src/test_helper';

View File

@ -0,0 +1,239 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {basename, dirname, resolve} from '../../src/helpers';
import {AbsoluteFsPath, FileStats, FileSystem, PathSegment, PathString} from '../../src/types';
/**
* An in-memory file system that can be used in unit tests.
*/
export abstract class MockFileSystem implements FileSystem {
private _fileTree: Folder = {};
private _cwd: AbsoluteFsPath;
constructor(private _isCaseSensitive = false, cwd: AbsoluteFsPath = '/' as AbsoluteFsPath) {
this._cwd = this.normalize(cwd);
}
isCaseSensitive() { return this._isCaseSensitive; }
exists(path: AbsoluteFsPath): boolean { return this.findFromPath(path).entity !== null; }
readFile(path: AbsoluteFsPath): string {
const {entity} = this.findFromPath(path);
if (isFile(entity)) {
return entity;
} else {
throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`);
}
}
writeFile(path: AbsoluteFsPath, data: string): void {
const [folderPath, basename] = this.splitIntoFolderAndFile(path);
const {entity} = this.findFromPath(folderPath);
if (entity === null || !isFolder(entity)) {
throw new MockFileSystemError(
'ENOENT', path, `Unable to write file "${path}". The containing folder does not exist.`);
}
entity[basename] = data;
}
symlink(target: AbsoluteFsPath, path: AbsoluteFsPath): void {
const [folderPath, basename] = this.splitIntoFolderAndFile(path);
const {entity} = this.findFromPath(folderPath);
if (entity === null || !isFolder(entity)) {
throw new MockFileSystemError(
'ENOENT', path,
`Unable to create symlink at "${path}". The containing folder does not exist.`);
}
entity[basename] = new SymLink(target);
}
readdir(path: AbsoluteFsPath): PathSegment[] {
const {entity} = this.findFromPath(path);
if (entity === null) {
throw new MockFileSystemError(
'ENOENT', path, `Unable to read directory "${path}". It does not exist.`);
}
if (isFile(entity)) {
throw new MockFileSystemError(
'ENOTDIR', path, `Unable to read directory "${path}". It is a file.`);
}
return Object.keys(entity) as PathSegment[];
}
lstat(path: AbsoluteFsPath): FileStats {
const {entity} = this.findFromPath(path);
if (entity === null) {
throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`);
}
return new MockFileStats(entity);
}
stat(path: AbsoluteFsPath): FileStats {
const {entity} = this.findFromPath(path, {followSymLinks: true});
if (entity === null) {
throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`);
}
return new MockFileStats(entity);
}
copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void {
this.writeFile(to, this.readFile(from));
}
moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void {
this.writeFile(to, this.readFile(from));
const result = this.findFromPath(dirname(from));
const folder = result.entity as Folder;
const name = basename(from);
delete folder[name];
}
mkdir(path: AbsoluteFsPath): void { this.ensureFolders(this._fileTree, this.splitPath(path)); }
ensureDir(path: AbsoluteFsPath): void {
this.ensureFolders(this._fileTree, this.splitPath(path));
}
isRoot(path: AbsoluteFsPath): boolean { return this.dirname(path) === path; }
extname(path: AbsoluteFsPath|PathSegment): string {
const match = /.+(\.[^.]*)$/.exec(path);
return match !== null ? match[1] : '';
}
realpath(filePath: AbsoluteFsPath): AbsoluteFsPath {
const result = this.findFromPath(filePath, {followSymLinks: true});
if (result.entity === null) {
throw new MockFileSystemError(
'ENOENT', filePath, `Unable to find the real path of "${filePath}". It does not exist.`);
} else {
return result.path;
}
}
pwd(): AbsoluteFsPath { return this._cwd; }
getDefaultLibLocation(): AbsoluteFsPath { return this.resolve('node_modules/typescript/lib'); }
abstract resolve(...paths: string[]): AbsoluteFsPath;
abstract dirname<T extends string>(file: T): T;
abstract join<T extends string>(basePath: T, ...paths: string[]): T;
abstract relative<T extends PathString>(from: T, to: T): PathSegment;
abstract basename(filePath: string, extension?: string): PathSegment;
abstract isRooted(path: string): boolean;
abstract normalize<T extends PathString>(path: T): T;
protected abstract splitPath<T extends PathString>(path: T): string[];
dump(): Folder { return cloneFolder(this._fileTree); }
init(folder: Folder): void { this._fileTree = cloneFolder(folder); }
protected findFromPath(path: AbsoluteFsPath, options?: {followSymLinks: boolean}): FindResult {
const followSymLinks = !!options && options.followSymLinks;
const segments = this.splitPath(path);
if (segments.length > 1 && segments[segments.length - 1] === '') {
// Remove a trailing slash (unless the path was only `/`)
segments.pop();
}
// Convert the root folder to a canonical empty string `""` (on Windows it would be `C:`).
segments[0] = '';
let current: Entity|null = this._fileTree;
while (segments.length) {
current = current[segments.shift() !];
if (current === undefined) {
return {path, entity: null};
}
if (segments.length > 0 && (!isFolder(current))) {
current = null;
break;
}
if (isFile(current)) {
break;
}
if (isSymLink(current)) {
if (followSymLinks) {
return this.findFromPath(resolve(current.path, ...segments), {followSymLinks});
} else {
break;
}
}
}
return {path, entity: current};
}
protected splitIntoFolderAndFile(path: AbsoluteFsPath): [AbsoluteFsPath, string] {
const segments = this.splitPath(path);
const file = segments.pop() !;
return [path.substring(0, path.length - file.length - 1) as AbsoluteFsPath, file];
}
protected ensureFolders(current: Folder, segments: string[]): Folder {
// Convert the root folder to a canonical empty string `""` (on Windows it would be `C:`).
segments[0] = '';
for (const segment of segments) {
if (isFile(current[segment])) {
throw new Error(`Folder already exists as a file.`);
}
if (!current[segment]) {
current[segment] = {};
}
current = current[segment] as Folder;
}
return current;
}
}
export interface FindResult {
path: AbsoluteFsPath;
entity: Entity|null;
}
export type Entity = Folder | File | SymLink;
export interface Folder { [pathSegments: string]: Entity; }
export type File = string;
export class SymLink {
constructor(public path: AbsoluteFsPath) {}
}
class MockFileStats implements FileStats {
constructor(private entity: Entity) {}
isFile(): boolean { return isFile(this.entity); }
isDirectory(): boolean { return isFolder(this.entity); }
isSymbolicLink(): boolean { return isSymLink(this.entity); }
}
class MockFileSystemError extends Error {
constructor(public code: string, public path: string, message: string) { super(message); }
}
export function isFile(item: Entity | null): item is File {
return typeof item === 'string';
}
export function isSymLink(item: Entity | null): item is SymLink {
return item instanceof SymLink;
}
export function isFolder(item: Entity | null): item is Folder {
return item !== null && !isFile(item) && !isSymLink(item);
}
function cloneFolder(folder: Folder): Folder {
const clone: Folder = {};
for (const path in folder) {
const item = folder[path];
if (isSymLink(item)) {
clone[path] = new SymLink(item.path);
} else if (isFolder(item)) {
clone[path] = cloneFolder(item);
} else {
clone[path] = folder[path];
}
}
return clone;
}

View File

@ -0,0 +1,48 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {NodeJSFileSystem} from '../../src/node_js_file_system';
import {AbsoluteFsPath, PathSegment, PathString} from '../../src/types';
import {MockFileSystem} from './mock_file_system';
export class MockFileSystemNative extends MockFileSystem {
constructor(cwd: AbsoluteFsPath = '/' as AbsoluteFsPath) { super(undefined, cwd); }
// Delegate to the real NodeJSFileSystem for these path related methods
resolve(...paths: string[]): AbsoluteFsPath {
return NodeJSFileSystem.prototype.resolve.call(this, this.pwd(), ...paths);
}
dirname<T extends string>(file: T): T {
return NodeJSFileSystem.prototype.dirname.call(this, file) as T;
}
join<T extends string>(basePath: T, ...paths: string[]): T {
return NodeJSFileSystem.prototype.join.call(this, basePath, ...paths) as T;
}
relative<T extends PathString>(from: T, to: T): PathSegment {
return NodeJSFileSystem.prototype.relative.call(this, from, to);
}
basename(filePath: string, extension?: string): PathSegment {
return NodeJSFileSystem.prototype.basename.call(this, filePath, extension);
}
isCaseSensitive() { return NodeJSFileSystem.prototype.isCaseSensitive.call(this); }
isRooted(path: string): boolean { return NodeJSFileSystem.prototype.isRooted.call(this, path); }
isRoot(path: AbsoluteFsPath): boolean {
return NodeJSFileSystem.prototype.isRoot.call(this, path);
}
normalize<T extends PathString>(path: T): T {
return NodeJSFileSystem.prototype.normalize.call(this, path) as T;
}
protected splitPath<T>(path: string): string[] { return path.split(/[\\\/]/); }
}

View File

@ -0,0 +1,41 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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
*/
/// <reference types="node" />
import * as p from 'path';
import {AbsoluteFsPath, PathSegment, PathString} from '../../src/types';
import {MockFileSystem} from './mock_file_system';
export class MockFileSystemPosix extends MockFileSystem {
resolve(...paths: string[]): AbsoluteFsPath {
const resolved = p.posix.resolve(this.pwd(), ...paths);
return this.normalize(resolved) as AbsoluteFsPath;
}
dirname<T extends string>(file: T): T { return this.normalize(p.posix.dirname(file)) as T; }
join<T extends string>(basePath: T, ...paths: string[]): T {
return this.normalize(p.posix.join(basePath, ...paths)) as T;
}
relative<T extends PathString>(from: T, to: T): PathSegment {
return this.normalize(p.posix.relative(from, to)) as PathSegment;
}
basename(filePath: string, extension?: string): PathSegment {
return p.posix.basename(filePath, extension) as PathSegment;
}
isRooted(path: string): boolean { return path.startsWith('/'); }
protected splitPath<T extends PathString>(path: T): string[] { return path.split('/'); }
normalize<T extends PathString>(path: T): T {
return path.replace(/^[a-z]:\//i, '/').replace(/\\/g, '/') as T;
}
}

View File

@ -0,0 +1,41 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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
*/
/// <reference types="node" />
import * as p from 'path';
import {AbsoluteFsPath, PathSegment, PathString} from '../../src/types';
import {MockFileSystem} from './mock_file_system';
export class MockFileSystemWindows extends MockFileSystem {
resolve(...paths: string[]): AbsoluteFsPath {
const resolved = p.win32.resolve(this.pwd(), ...paths);
return this.normalize(resolved as AbsoluteFsPath);
}
dirname<T extends string>(path: T): T { return this.normalize(p.win32.dirname(path) as T); }
join<T extends string>(basePath: T, ...paths: string[]): T {
return this.normalize(p.win32.join(basePath, ...paths)) as T;
}
relative<T extends PathString>(from: T, to: T): PathSegment {
return this.normalize(p.win32.relative(from, to)) as PathSegment;
}
basename(filePath: string, extension?: string): PathSegment {
return p.win32.basename(filePath, extension) as PathSegment;
}
isRooted(path: string): boolean { return /^([A-Z]:)?([\\\/]|$)/i.test(path); }
protected splitPath<T extends PathString>(path: T): string[] { return path.split(/[\\\/]/); }
normalize<T extends PathString>(path: T): T {
return path.replace(/^[\/\\]/i, 'C:/').replace(/\\/g, '/') as T;
}
}

View File

@ -0,0 +1,149 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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
*/
/// <reference types="jasmine"/>
import * as ts from 'typescript';
import {absoluteFrom, setFileSystem} from '../../src/helpers';
import {AbsoluteFsPath} from '../../src/types';
import {MockFileSystem} from './mock_file_system';
import {MockFileSystemNative} from './mock_file_system_native';
import {MockFileSystemPosix} from './mock_file_system_posix';
import {MockFileSystemWindows} from './mock_file_system_windows';
export interface TestFile {
name: AbsoluteFsPath;
contents: string;
isRoot?: boolean|undefined;
}
export interface RunInEachFileSystemFn {
(callback: (os: string) => void): void;
windows(callback: (os: string) => void): void;
unix(callback: (os: string) => void): void;
native(callback: (os: string) => void): void;
osX(callback: (os: string) => void): void;
}
const FS_NATIVE = 'Native';
const FS_OS_X = 'OS/X';
const FS_UNIX = 'Unix';
const FS_WINDOWS = 'Windows';
const FS_ALL = [FS_OS_X, FS_WINDOWS, FS_UNIX, FS_NATIVE];
function runInEachFileSystemFn(callback: (os: string) => void) {
FS_ALL.forEach(os => runInFileSystem(os, callback, false));
}
function runInFileSystem(os: string, callback: (os: string) => void, error: boolean) {
describe(`<<FileSystem: ${os}>>`, () => {
beforeEach(() => initMockFileSystem(os));
callback(os);
if (error) {
afterAll(() => { throw new Error(`runInFileSystem limited to ${os}, cannot pass`); });
}
});
}
export const runInEachFileSystem: RunInEachFileSystemFn =
runInEachFileSystemFn as RunInEachFileSystemFn;
runInEachFileSystem.native = (callback: (os: string) => void) =>
runInFileSystem(FS_NATIVE, callback, true);
runInEachFileSystem.osX = (callback: (os: string) => void) =>
runInFileSystem(FS_OS_X, callback, true);
runInEachFileSystem.unix = (callback: (os: string) => void) =>
runInFileSystem(FS_UNIX, callback, true);
runInEachFileSystem.windows = (callback: (os: string) => void) =>
runInFileSystem(FS_WINDOWS, callback, true);
export function initMockFileSystem(os: string, cwd?: AbsoluteFsPath): void {
const fs = createMockFileSystem(os, cwd);
setFileSystem(fs);
monkeyPatchTypeScript(os, fs);
}
function createMockFileSystem(os: string, cwd?: AbsoluteFsPath): MockFileSystem {
switch (os) {
case 'OS/X':
return new MockFileSystemPosix(/* isCaseSensitive */ false, cwd);
case 'Unix':
return new MockFileSystemPosix(/* isCaseSensitive */ true, cwd);
case 'Windows':
return new MockFileSystemWindows(/* isCaseSensitive*/ false, cwd);
case 'Native':
return new MockFileSystemNative(cwd);
default:
throw new Error('FileSystem not supported');
}
}
function monkeyPatchTypeScript(os: string, fs: MockFileSystem) {
ts.sys.directoryExists = path => {
const absPath = fs.resolve(path);
return fs.exists(absPath) && fs.stat(absPath).isDirectory();
};
ts.sys.fileExists = path => {
const absPath = fs.resolve(path);
return fs.exists(absPath) && fs.stat(absPath).isFile();
};
ts.sys.getCurrentDirectory = () => fs.pwd();
ts.sys.getDirectories = getDirectories;
ts.sys.readFile = fs.readFile.bind(fs);
ts.sys.resolvePath = fs.resolve.bind(fs);
ts.sys.writeFile = fs.writeFile.bind(fs);
ts.sys.readDirectory = readDirectory;
function getDirectories(path: string): string[] {
return fs.readdir(absoluteFrom(path)).filter(p => fs.stat(fs.resolve(path, p)).isDirectory());
}
function getFileSystemEntries(path: string): FileSystemEntries {
const files: string[] = [];
const directories: string[] = [];
const absPath = fs.resolve(path);
const entries = fs.readdir(absPath);
for (const entry of entries) {
if (entry == '.' || entry === '..') {
continue;
}
const absPath = fs.resolve(path, entry);
const stat = fs.stat(absPath);
if (stat.isDirectory()) {
directories.push(absPath);
} else if (stat.isFile()) {
files.push(absPath);
}
}
return {files, directories};
}
function realPath(path: string): string { return fs.realpath(fs.resolve(path)); }
// Rather than completely re-implementing we are using the `ts.matchFiles` function,
// which is internal to the `ts` namespace.
const tsMatchFiles: (
path: string, extensions: ReadonlyArray<string>| undefined,
excludes: ReadonlyArray<string>| undefined, includes: ReadonlyArray<string>| undefined,
useCaseSensitiveFileNames: boolean, currentDirectory: string, depth: number | undefined,
getFileSystemEntries: (path: string) => FileSystemEntries,
realpath: (path: string) => string) => string[] = (ts as any).matchFiles;
function readDirectory(
path: string, extensions?: ReadonlyArray<string>, excludes?: ReadonlyArray<string>,
includes?: ReadonlyArray<string>, depth?: number): string[] {
return tsMatchFiles(
path, extensions, excludes, includes, fs.isCaseSensitive(), fs.pwd(), depth,
getFileSystemEntries, realPath);
}
}
interface FileSystemEntries {
readonly files: ReadonlyArray<string>;
readonly directories: ReadonlyArray<string>;
}

View File

@ -10,7 +10,7 @@ ts_library(
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//@types/node",

View File

@ -5,19 +5,15 @@
* 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 {Expression, ExternalExpr, WrappedNodeExpr} from '@angular/compiler';
import {ExternalReference} from '@angular/compiler/src/compiler';
import * as ts from 'typescript';
import {LogicalFileSystem, LogicalProjectPath} from '../../path';
import {LogicalFileSystem, LogicalProjectPath, absoluteFrom} from '../../file_system';
import {ReflectionHost} from '../../reflection';
import {getSourceFile, isDeclaration, nodeNameForError, resolveModuleName} from '../../util/src/typescript';
import {getSourceFile, getSourceFileOrNull, isDeclaration, nodeNameForError, resolveModuleName} from '../../util/src/typescript';
import {findExportedNameOfNode} from './find_export';
import {ImportMode, Reference} from './references';
/**
* A host which supports an operation to convert a file name into a module name.
*
@ -170,8 +166,9 @@ export class AbsoluteModuleStrategy implements ReferenceEmitStrategy {
return null;
}
const entryPointFile = this.program.getSourceFile(resolvedModule.resolvedFileName);
if (entryPointFile === undefined) {
const entryPointFile =
getSourceFileOrNull(this.program, absoluteFrom(resolvedModule.resolvedFileName));
if (entryPointFile === null) {
return null;
}

View File

@ -5,10 +5,9 @@
* 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 {resolveModuleName} from '../../util/src/typescript';
import {absoluteFrom} from '../../file_system';
import {getSourceFileOrNull, resolveModuleName} from '../../util/src/typescript';
import {Reference} from './references';
export interface ReferenceResolver {
@ -33,6 +32,6 @@ export class ModuleResolver {
if (resolved === undefined) {
return null;
}
return this.program.getSourceFile(resolved.resolvedFileName) || null;
return getSourceFileOrNull(this.program, absoluteFrom(resolved.resolvedFileName));
}
}

View File

@ -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/imports",
"//packages/compiler-cli/src/ngtsc/testing",
"@npm//typescript",

View File

@ -5,86 +5,101 @@
* 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 {DefaultImportTracker} from '../src/default';
describe('DefaultImportTracker', () => {
it('should prevent a default import from being elided if used', () => {
const {program, host} = makeProgram(
[
{name: 'dep.ts', contents: `export default class Foo {}`},
{name: 'test.ts', contents: `import Foo from './dep'; export function test(f: Foo) {}`},
runInEachFileSystem(() => {
describe('DefaultImportTracker', () => {
let _: typeof absoluteFrom;
beforeEach(() => _ = absoluteFrom);
// This control file is identical to the test file, but will not have its import marked
// for preservation. It exists to verify that it is in fact the action of
// DefaultImportTracker and not some other artifact of the test setup which causes the
// import to be preserved. It will also verify that DefaultImportTracker does not preserve
// imports which are not marked for preservation.
{name: 'ctrl.ts', contents: `import Foo from './dep'; export function test(f: Foo) {}`},
],
{
module: ts.ModuleKind.ES2015,
});
const fooClause = getDeclaration(program, 'test.ts', 'Foo', ts.isImportClause);
const fooId = fooClause.name !;
const fooDecl = fooClause.parent;
it('should prevent a default import from being elided if used', () => {
const {program, host} = makeProgram(
[
{name: _('/dep.ts'), contents: `export default class Foo {}`},
{
name: _('/test.ts'),
contents: `import Foo from './dep'; export function test(f: Foo) {}`
},
const tracker = new DefaultImportTracker();
tracker.recordImportedIdentifier(fooId, fooDecl);
tracker.recordUsedIdentifier(fooId);
program.emit(undefined, undefined, undefined, undefined, {
before: [tracker.importPreservingTransformer()],
// This control file is identical to the test file, but will not have its import marked
// for preservation. It exists to verify that it is in fact the action of
// DefaultImportTracker and not some other artifact of the test setup which causes the
// import to be preserved. It will also verify that DefaultImportTracker does not
// preserve imports which are not marked for preservation.
{
name: _('/ctrl.ts'),
contents: `import Foo from './dep'; export function test(f: Foo) {}`
},
],
{
module: ts.ModuleKind.ES2015,
});
const fooClause = getDeclaration(program, _('/test.ts'), 'Foo', ts.isImportClause);
const fooId = fooClause.name !;
const fooDecl = fooClause.parent;
const tracker = new DefaultImportTracker();
tracker.recordImportedIdentifier(fooId, fooDecl);
tracker.recordUsedIdentifier(fooId);
program.emit(undefined, undefined, undefined, undefined, {
before: [tracker.importPreservingTransformer()],
});
const testContents = host.readFile('/test.js') !;
expect(testContents).toContain(`import Foo from './dep';`);
// The control should have the import elided.
const ctrlContents = host.readFile('/ctrl.js');
expect(ctrlContents).not.toContain(`import Foo from './dep';`);
});
const testContents = host.readFile('/test.js') !;
expect(testContents).toContain(`import Foo from './dep';`);
// The control should have the import elided.
const ctrlContents = host.readFile('/ctrl.js');
expect(ctrlContents).not.toContain(`import Foo from './dep';`);
it('should transpile imports correctly into commonjs', () => {
const {program, host} = makeProgram(
[
{name: _('/dep.ts'), contents: `export default class Foo {}`},
{
name: _('/test.ts'),
contents: `import Foo from './dep'; export function test(f: Foo) {}`
},
],
{
module: ts.ModuleKind.CommonJS,
});
const fooClause = getDeclaration(program, _('/test.ts'), 'Foo', ts.isImportClause);
const fooId = ts.updateIdentifier(fooClause.name !);
const fooDecl = fooClause.parent;
const tracker = new DefaultImportTracker();
tracker.recordImportedIdentifier(fooId, fooDecl);
tracker.recordUsedIdentifier(fooId);
program.emit(undefined, undefined, undefined, undefined, {
before: [
addReferenceTransformer(fooId),
tracker.importPreservingTransformer(),
],
});
const testContents = host.readFile('/test.js') !;
expect(testContents).toContain(`var dep_1 = require("./dep");`);
expect(testContents).toContain(`var ref = dep_1["default"];`);
});
});
it('should transpile imports correctly into commonjs', () => {
const {program, host} = makeProgram(
[
{name: 'dep.ts', contents: `export default class Foo {}`},
{name: 'test.ts', contents: `import Foo from './dep'; export function test(f: Foo) {}`},
],
{
module: ts.ModuleKind.CommonJS,
});
const fooClause = getDeclaration(program, 'test.ts', 'Foo', ts.isImportClause);
const fooId = ts.updateIdentifier(fooClause.name !);
const fooDecl = fooClause.parent;
const tracker = new DefaultImportTracker();
tracker.recordImportedIdentifier(fooId, fooDecl);
tracker.recordUsedIdentifier(fooId);
program.emit(undefined, undefined, undefined, undefined, {
before: [
addReferenceTransformer(fooId),
tracker.importPreservingTransformer(),
],
});
const testContents = host.readFile('/test.js') !;
expect(testContents).toContain(`var dep_1 = require("./dep");`);
expect(testContents).toContain(`var ref = dep_1["default"];`);
});
});
function addReferenceTransformer(id: ts.Identifier): ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext) => {
return (sf: ts.SourceFile) => {
if (id.getSourceFile().fileName === sf.fileName) {
return ts.updateSourceFileNode(sf, [
...sf.statements, ts.createVariableStatement(undefined, ts.createVariableDeclarationList([
ts.createVariableDeclaration('ref', undefined, id),
]))
]);
}
return sf;
function addReferenceTransformer(id: ts.Identifier): ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext) => {
return (sf: ts.SourceFile) => {
if (id.getSourceFile().fileName === sf.fileName) {
return ts.updateSourceFileNode(sf, [
...sf.statements,
ts.createVariableStatement(undefined, ts.createVariableDeclarationList([
ts.createVariableDeclaration('ref', undefined, id),
]))
]);
}
return sf;
};
};
};
}
}
});

View File

@ -10,6 +10,8 @@ ts_library(
]),
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/indexer",
"//packages/compiler-cli/src/ngtsc/metadata",

View File

@ -5,35 +5,37 @@
* 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 {ParseSourceFile} from '@angular/compiler';
import {runInEachFileSystem} from '../../file_system/testing';
import {IndexingContext} from '../src/context';
import * as util from './util';
describe('ComponentAnalysisContext', () => {
it('should store and return information about components', () => {
const context = new IndexingContext();
const declaration = util.getComponentDeclaration('class C {};', 'C');
const boundTemplate = util.getBoundTemplate('<div></div>');
runInEachFileSystem(() => {
describe('ComponentAnalysisContext', () => {
it('should store and return information about components', () => {
const context = new IndexingContext();
const declaration = util.getComponentDeclaration('class C {};', 'C');
const boundTemplate = util.getBoundTemplate('<div></div>');
context.addComponent({
declaration,
selector: 'c-selector', boundTemplate,
templateMeta: {
isInline: false,
file: new ParseSourceFile('<div></div>', util.TESTFILE),
},
});
expect(context.components).toEqual(new Set([
{
context.addComponent({
declaration,
selector: 'c-selector', boundTemplate,
templateMeta: {
isInline: false,
file: new ParseSourceFile('<div></div>', util.TESTFILE),
file: new ParseSourceFile('<div></div>', util.getTestFilePath()),
},
},
]));
});
expect(context.components).toEqual(new Set([
{
declaration,
selector: 'c-selector', boundTemplate,
templateMeta: {
isInline: false,
file: new ParseSourceFile('<div></div>', util.getTestFilePath()),
},
},
]));
});
});
});

View File

@ -5,8 +5,8 @@
* 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 {BoundTarget, ParseSourceFile} from '@angular/compiler';
import {runInEachFileSystem} from '../../file_system/testing';
import {DirectiveMeta} from '../../metadata';
import {ClassDeclaration} from '../../reflection';
import {IndexingContext} from '../src/context';
@ -26,92 +26,96 @@ function populateContext(
boundTemplate,
templateMeta: {
isInline,
file: new ParseSourceFile(template, util.TESTFILE),
file: new ParseSourceFile(template, util.getTestFilePath()),
},
});
}
describe('generateAnalysis', () => {
it('should emit component and template analysis information', () => {
const context = new IndexingContext();
const decl = util.getComponentDeclaration('class C {}', 'C');
const template = '<div>{{foo}}</div>';
populateContext(context, decl, 'c-selector', template, util.getBoundTemplate(template));
const analysis = generateAnalysis(context);
runInEachFileSystem(() => {
describe('generateAnalysis', () => {
it('should emit component and template analysis information', () => {
const context = new IndexingContext();
const decl = util.getComponentDeclaration('class C {}', 'C');
const template = '<div>{{foo}}</div>';
populateContext(context, decl, 'c-selector', template, util.getBoundTemplate(template));
const analysis = generateAnalysis(context);
expect(analysis.size).toBe(1);
expect(analysis.size).toBe(1);
const info = analysis.get(decl);
expect(info).toEqual({
name: 'C',
selector: 'c-selector',
file: new ParseSourceFile('class C {}', util.TESTFILE),
template: {
identifiers: getTemplateIdentifiers(util.getBoundTemplate('<div>{{foo}}</div>')),
usedComponents: new Set(),
isInline: false,
file: new ParseSourceFile('<div>{{foo}}</div>', util.TESTFILE),
}
const info = analysis.get(decl);
expect(info).toEqual({
name: 'C',
selector: 'c-selector',
file: new ParseSourceFile('class C {}', util.getTestFilePath()),
template: {
identifiers: getTemplateIdentifiers(util.getBoundTemplate('<div>{{foo}}</div>')),
usedComponents: new Set(),
isInline: false,
file: new ParseSourceFile('<div>{{foo}}</div>', util.getTestFilePath()),
}
});
});
it('should give inline templates the component source file', () => {
const context = new IndexingContext();
const decl = util.getComponentDeclaration('class C {}', 'C');
const template = '<div>{{foo}}</div>';
populateContext(
context, decl, 'c-selector', '<div>{{foo}}</div>', util.getBoundTemplate(template),
/* inline template */ true);
const analysis = generateAnalysis(context);
expect(analysis.size).toBe(1);
const info = analysis.get(decl);
expect(info).toBeDefined();
expect(info !.template.file)
.toEqual(new ParseSourceFile('class C {}', util.getTestFilePath()));
});
it('should give external templates their own source file', () => {
const context = new IndexingContext();
const decl = util.getComponentDeclaration('class C {}', 'C');
const template = '<div>{{foo}}</div>';
populateContext(context, decl, 'c-selector', template, util.getBoundTemplate(template));
const analysis = generateAnalysis(context);
expect(analysis.size).toBe(1);
const info = analysis.get(decl);
expect(info).toBeDefined();
expect(info !.template.file)
.toEqual(new ParseSourceFile('<div>{{foo}}</div>', util.getTestFilePath()));
});
it('should emit used components', () => {
const context = new IndexingContext();
const templateA = '<b-selector></b-selector>';
const declA = util.getComponentDeclaration('class A {}', 'A');
const templateB = '<a-selector></a-selector>';
const declB = util.getComponentDeclaration('class B {}', 'B');
const boundA =
util.getBoundTemplate(templateA, {}, [{selector: 'b-selector', declaration: declB}]);
const boundB =
util.getBoundTemplate(templateB, {}, [{selector: 'a-selector', declaration: declA}]);
populateContext(context, declA, 'a-selector', templateA, boundA);
populateContext(context, declB, 'b-selector', templateB, boundB);
const analysis = generateAnalysis(context);
expect(analysis.size).toBe(2);
const infoA = analysis.get(declA);
expect(infoA).toBeDefined();
expect(infoA !.template.usedComponents).toEqual(new Set([declB]));
const infoB = analysis.get(declB);
expect(infoB).toBeDefined();
expect(infoB !.template.usedComponents).toEqual(new Set([declA]));
});
});
it('should give inline templates the component source file', () => {
const context = new IndexingContext();
const decl = util.getComponentDeclaration('class C {}', 'C');
const template = '<div>{{foo}}</div>';
populateContext(
context, decl, 'c-selector', '<div>{{foo}}</div>', util.getBoundTemplate(template),
/* inline template */ true);
const analysis = generateAnalysis(context);
expect(analysis.size).toBe(1);
const info = analysis.get(decl);
expect(info).toBeDefined();
expect(info !.template.file).toEqual(new ParseSourceFile('class C {}', util.TESTFILE));
});
it('should give external templates their own source file', () => {
const context = new IndexingContext();
const decl = util.getComponentDeclaration('class C {}', 'C');
const template = '<div>{{foo}}</div>';
populateContext(context, decl, 'c-selector', template, util.getBoundTemplate(template));
const analysis = generateAnalysis(context);
expect(analysis.size).toBe(1);
const info = analysis.get(decl);
expect(info).toBeDefined();
expect(info !.template.file).toEqual(new ParseSourceFile('<div>{{foo}}</div>', util.TESTFILE));
});
it('should emit used components', () => {
const context = new IndexingContext();
const templateA = '<b-selector></b-selector>';
const declA = util.getComponentDeclaration('class A {}', 'A');
const templateB = '<a-selector></a-selector>';
const declB = util.getComponentDeclaration('class B {}', 'B');
const boundA =
util.getBoundTemplate(templateA, {}, [{selector: 'b-selector', declaration: declB}]);
const boundB =
util.getBoundTemplate(templateB, {}, [{selector: 'a-selector', declaration: declA}]);
populateContext(context, declA, 'a-selector', templateA, boundA);
populateContext(context, declB, 'b-selector', templateB, boundB);
const analysis = generateAnalysis(context);
expect(analysis.size).toBe(2);
const infoA = analysis.get(declA);
expect(infoA).toBeDefined();
expect(infoA !.template.usedComponents).toEqual(new Set([declB]));
const infoB = analysis.get(declB);
expect(infoB).toBeDefined();
expect(infoB !.template.usedComponents).toEqual(new Set([declA]));
});
});

View File

@ -8,22 +8,25 @@
import {BoundTarget, CssSelector, ParseTemplateOptions, R3TargetBinder, SelectorMatcher, parseTemplate} from '@angular/compiler';
import * as ts from 'typescript';
import {AbsoluteFsPath, absoluteFrom} from '../../file_system';
import {Reference} from '../../imports';
import {DirectiveMeta} from '../../metadata';
import {ClassDeclaration} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {getDeclaration, makeProgram} from '../../testing';
/** Dummy file URL */
export const TESTFILE = '/TESTFILE.ts';
export function getTestFilePath(): AbsoluteFsPath {
return absoluteFrom('/TEST_FILE.ts');
}
/**
* Creates a class declaration from a component source code.
*/
export function getComponentDeclaration(componentStr: string, className: string): ClassDeclaration {
const program = makeProgram([{name: TESTFILE, contents: componentStr}]);
const program = makeProgram([{name: getTestFilePath(), contents: componentStr}]);
return getDeclaration(
program.program, TESTFILE, className,
program.program, getTestFilePath(), className,
(value: ts.Declaration): value is ClassDeclaration => ts.isClassDeclaration(value));
}
@ -57,5 +60,5 @@ export function getBoundTemplate(
});
const binder = new R3TargetBinder(matcher);
return binder.bind({template: parseTemplate(template, TESTFILE, options).nodes});
return binder.bind({template: parseTemplate(template, getTestFilePath(), options).nodes});
}

View File

@ -11,11 +11,12 @@ ts_library(
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/testing",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//typescript",
],
)

View File

@ -5,293 +5,300 @@
* 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 {absoluteFrom, getSourceFileOrError} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {Reference} from '../../imports';
import {FunctionDefinition, TsHelperFn, TypeScriptReflectionHost} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {getDeclaration, makeProgram} from '../../testing';
import {DynamicValue} from '../src/dynamic';
import {PartialEvaluator} from '../src/interface';
import {EnumValue} from '../src/result';
import {evaluate, firstArgFfr, makeEvaluator, makeExpression, owningModuleOf} from './utils';
describe('ngtsc metadata', () => {
it('reads a file correctly', () => {
const value = evaluate(
`
runInEachFileSystem(() => {
describe('ngtsc metadata', () => {
let _: typeof absoluteFrom;
beforeEach(() => _ = absoluteFrom);
it('reads a file correctly', () => {
const value = evaluate(
`
import {Y} from './other';
const A = Y;
`,
'A', [
{
name: 'other.ts',
contents: `
'A', [
{
name: _('/other.ts'),
contents: `
export const Y = 'test';
`
},
]);
},
]);
expect(value).toEqual('test');
});
expect(value).toEqual('test');
});
it('map access works',
() => { expect(evaluate('const obj = {a: "test"};', 'obj.a')).toEqual('test'); });
it('map access works',
() => { expect(evaluate('const obj = {a: "test"};', 'obj.a')).toEqual('test'); });
it('resolves undefined property access',
() => { expect(evaluate('const obj: any = {}', 'obj.bar')).toEqual(undefined); });
it('resolves undefined property access',
() => { expect(evaluate('const obj: any = {}', 'obj.bar')).toEqual(undefined); });
it('function calls work', () => {
expect(evaluate(`function foo(bar) { return bar; }`, 'foo("test")')).toEqual('test');
});
it('function calls work', () => {
expect(evaluate(`function foo(bar) { return bar; }`, 'foo("test")')).toEqual('test');
});
it('function call default value works', () => {
expect(evaluate(`function foo(bar = 1) { return bar; }`, 'foo()')).toEqual(1);
expect(evaluate(`function foo(bar = 1) { return bar; }`, 'foo(2)')).toEqual(2);
expect(evaluate(`function foo(a, c = a) { return c; }; const a = 1;`, 'foo(2)')).toEqual(2);
});
it('function call default value works', () => {
expect(evaluate(`function foo(bar = 1) { return bar; }`, 'foo()')).toEqual(1);
expect(evaluate(`function foo(bar = 1) { return bar; }`, 'foo(2)')).toEqual(2);
expect(evaluate(`function foo(a, c = a) { return c; }; const a = 1;`, 'foo(2)')).toEqual(2);
});
it('function call spread works', () => {
expect(evaluate(`function foo(a, ...b) { return [a, b]; }`, 'foo(1, ...[2, 3])')).toEqual([
1, [2, 3]
]);
});
it('function call spread works', () => {
expect(evaluate(`function foo(a, ...b) { return [a, b]; }`, 'foo(1, ...[2, 3])')).toEqual([
1, [2, 3]
]);
});
it('conditionals work', () => {
expect(evaluate(`const x = false; const y = x ? 'true' : 'false';`, 'y')).toEqual('false');
});
it('conditionals work', () => {
expect(evaluate(`const x = false; const y = x ? 'true' : 'false';`, 'y')).toEqual('false');
});
it('addition works', () => { expect(evaluate(`const x = 1 + 2;`, 'x')).toEqual(3); });
it('addition works', () => { expect(evaluate(`const x = 1 + 2;`, 'x')).toEqual(3); });
it('static property on class works',
() => { expect(evaluate(`class Foo { static bar = 'test'; }`, 'Foo.bar')).toEqual('test'); });
it('static property on class works', () => {
expect(evaluate(`class Foo { static bar = 'test'; }`, 'Foo.bar')).toEqual('test');
});
it('static property call works', () => {
expect(evaluate(`class Foo { static bar(test) { return test; } }`, 'Foo.bar("test")'))
.toEqual('test');
});
it('static property call works', () => {
expect(evaluate(`class Foo { static bar(test) { return test; } }`, 'Foo.bar("test")'))
.toEqual('test');
});
it('indirected static property call works', () => {
expect(
evaluate(
`class Foo { static bar(test) { return test; } }; const fn = Foo.bar;`, 'fn("test")'))
.toEqual('test');
});
it('indirected static property call works', () => {
expect(
evaluate(
`class Foo { static bar(test) { return test; } }; const fn = Foo.bar;`, 'fn("test")'))
.toEqual('test');
});
it('array works', () => {
expect(evaluate(`const x = 'test'; const y = [1, x, 2];`, 'y')).toEqual([1, 'test', 2]);
});
it('array works', () => {
expect(evaluate(`const x = 'test'; const y = [1, x, 2];`, 'y')).toEqual([1, 'test', 2]);
});
it('array spread works', () => {
expect(evaluate(`const a = [1, 2]; const b = [4, 5]; const c = [...a, 3, ...b];`, 'c'))
.toEqual([1, 2, 3, 4, 5]);
});
it('array spread works', () => {
expect(evaluate(`const a = [1, 2]; const b = [4, 5]; const c = [...a, 3, ...b];`, 'c'))
.toEqual([1, 2, 3, 4, 5]);
});
it('&& operations work', () => {
expect(evaluate(`const a = 'hello', b = 'world';`, 'a && b')).toEqual('world');
expect(evaluate(`const a = false, b = 'world';`, 'a && b')).toEqual(false);
expect(evaluate(`const a = 'hello', b = 0;`, 'a && b')).toEqual(0);
});
it('&& operations work', () => {
expect(evaluate(`const a = 'hello', b = 'world';`, 'a && b')).toEqual('world');
expect(evaluate(`const a = false, b = 'world';`, 'a && b')).toEqual(false);
expect(evaluate(`const a = 'hello', b = 0;`, 'a && b')).toEqual(0);
});
it('|| operations work', () => {
expect(evaluate(`const a = 'hello', b = 'world';`, 'a || b')).toEqual('hello');
expect(evaluate(`const a = false, b = 'world';`, 'a || b')).toEqual('world');
expect(evaluate(`const a = 'hello', b = 0;`, 'a || b')).toEqual('hello');
});
it('|| operations work', () => {
expect(evaluate(`const a = 'hello', b = 'world';`, 'a || b')).toEqual('hello');
expect(evaluate(`const a = false, b = 'world';`, 'a || b')).toEqual('world');
expect(evaluate(`const a = 'hello', b = 0;`, 'a || b')).toEqual('hello');
});
it('evaluates arithmetic operators', () => {
expect(evaluate('const a = 6, b = 3;', 'a + b')).toEqual(9);
expect(evaluate('const a = 6, b = 3;', 'a - b')).toEqual(3);
expect(evaluate('const a = 6, b = 3;', 'a * b')).toEqual(18);
expect(evaluate('const a = 6, b = 3;', 'a / b')).toEqual(2);
expect(evaluate('const a = 6, b = 3;', 'a % b')).toEqual(0);
expect(evaluate('const a = 6, b = 3;', 'a & b')).toEqual(2);
expect(evaluate('const a = 6, b = 3;', 'a | b')).toEqual(7);
expect(evaluate('const a = 6, b = 3;', 'a ^ b')).toEqual(5);
expect(evaluate('const a = 6, b = 3;', 'a ** b')).toEqual(216);
expect(evaluate('const a = 6, b = 3;', 'a << b')).toEqual(48);
expect(evaluate('const a = -6, b = 2;', 'a >> b')).toEqual(-2);
expect(evaluate('const a = -6, b = 2;', 'a >>> b')).toEqual(1073741822);
});
it('evaluates arithmetic operators', () => {
expect(evaluate('const a = 6, b = 3;', 'a + b')).toEqual(9);
expect(evaluate('const a = 6, b = 3;', 'a - b')).toEqual(3);
expect(evaluate('const a = 6, b = 3;', 'a * b')).toEqual(18);
expect(evaluate('const a = 6, b = 3;', 'a / b')).toEqual(2);
expect(evaluate('const a = 6, b = 3;', 'a % b')).toEqual(0);
expect(evaluate('const a = 6, b = 3;', 'a & b')).toEqual(2);
expect(evaluate('const a = 6, b = 3;', 'a | b')).toEqual(7);
expect(evaluate('const a = 6, b = 3;', 'a ^ b')).toEqual(5);
expect(evaluate('const a = 6, b = 3;', 'a ** b')).toEqual(216);
expect(evaluate('const a = 6, b = 3;', 'a << b')).toEqual(48);
expect(evaluate('const a = -6, b = 2;', 'a >> b')).toEqual(-2);
expect(evaluate('const a = -6, b = 2;', 'a >>> b')).toEqual(1073741822);
});
it('evaluates comparison operators', () => {
expect(evaluate('const a = 2, b = 3;', 'a < b')).toEqual(true);
expect(evaluate('const a = 3, b = 3;', 'a < b')).toEqual(false);
it('evaluates comparison operators', () => {
expect(evaluate('const a = 2, b = 3;', 'a < b')).toEqual(true);
expect(evaluate('const a = 3, b = 3;', 'a < b')).toEqual(false);
expect(evaluate('const a = 3, b = 3;', 'a <= b')).toEqual(true);
expect(evaluate('const a = 4, b = 3;', 'a <= b')).toEqual(false);
expect(evaluate('const a = 3, b = 3;', 'a <= b')).toEqual(true);
expect(evaluate('const a = 4, b = 3;', 'a <= b')).toEqual(false);
expect(evaluate('const a = 4, b = 3;', 'a > b')).toEqual(true);
expect(evaluate('const a = 3, b = 3;', 'a > b')).toEqual(false);
expect(evaluate('const a = 4, b = 3;', 'a > b')).toEqual(true);
expect(evaluate('const a = 3, b = 3;', 'a > b')).toEqual(false);
expect(evaluate('const a = 3, b = 3;', 'a >= b')).toEqual(true);
expect(evaluate('const a = 2, b = 3;', 'a >= b')).toEqual(false);
expect(evaluate('const a = 3, b = 3;', 'a >= b')).toEqual(true);
expect(evaluate('const a = 2, b = 3;', 'a >= b')).toEqual(false);
expect(evaluate('const a: any = 3, b = "3";', 'a == b')).toEqual(true);
expect(evaluate('const a: any = 2, b = "3";', 'a == b')).toEqual(false);
expect(evaluate('const a: any = 3, b = "3";', 'a == b')).toEqual(true);
expect(evaluate('const a: any = 2, b = "3";', 'a == b')).toEqual(false);
expect(evaluate('const a: any = 2, b = "3";', 'a != b')).toEqual(true);
expect(evaluate('const a: any = 3, b = "3";', 'a != b')).toEqual(false);
expect(evaluate('const a: any = 2, b = "3";', 'a != b')).toEqual(true);
expect(evaluate('const a: any = 3, b = "3";', 'a != b')).toEqual(false);
expect(evaluate('const a: any = 3, b = 3;', 'a === b')).toEqual(true);
expect(evaluate('const a: any = 3, b = "3";', 'a === b')).toEqual(false);
expect(evaluate('const a: any = 3, b = 3;', 'a === b')).toEqual(true);
expect(evaluate('const a: any = 3, b = "3";', 'a === b')).toEqual(false);
expect(evaluate('const a: any = 3, b = "3";', 'a !== b')).toEqual(true);
expect(evaluate('const a: any = 3, b = 3;', 'a !== b')).toEqual(false);
});
expect(evaluate('const a: any = 3, b = "3";', 'a !== b')).toEqual(true);
expect(evaluate('const a: any = 3, b = 3;', 'a !== b')).toEqual(false);
});
it('parentheticals work',
() => { expect(evaluate(`const a = 3, b = 4;`, 'a * (a + b)')).toEqual(21); });
it('parentheticals work',
() => { expect(evaluate(`const a = 3, b = 4;`, 'a * (a + b)')).toEqual(21); });
it('array access works',
() => { expect(evaluate(`const a = [1, 2, 3];`, 'a[1] + a[0]')).toEqual(3); });
it('array access works',
() => { expect(evaluate(`const a = [1, 2, 3];`, 'a[1] + a[0]')).toEqual(3); });
it('array `length` property access works',
() => { expect(evaluate(`const a = [1, 2, 3];`, 'a[\'length\'] + 1')).toEqual(4); });
it('array `length` property access works',
() => { expect(evaluate(`const a = [1, 2, 3];`, 'a[\'length\'] + 1')).toEqual(4); });
it('array `slice` function works', () => {
expect(evaluate(`const a = [1, 2, 3];`, 'a[\'slice\']()')).toEqual([1, 2, 3]);
});
it('array `slice` function works', () => {
expect(evaluate(`const a = [1, 2, 3];`, 'a[\'slice\']()')).toEqual([1, 2, 3]);
});
it('array `concat` function works', () => {
expect(evaluate(`const a = [1, 2], b = [3, 4];`, 'a[\'concat\'](b)')).toEqual([1, 2, 3, 4]);
expect(evaluate(`const a = [1, 2], b = 3;`, 'a[\'concat\'](b)')).toEqual([1, 2, 3]);
expect(evaluate(`const a = [1, 2], b = 3, c = [4, 5];`, 'a[\'concat\'](b, c)')).toEqual([
1, 2, 3, 4, 5
]);
expect(evaluate(`const a = [1, 2], b = [3, 4]`, 'a[\'concat\'](...b)')).toEqual([1, 2, 3, 4]);
});
it('array `concat` function works', () => {
expect(evaluate(`const a = [1, 2], b = [3, 4];`, 'a[\'concat\'](b)')).toEqual([1, 2, 3, 4]);
expect(evaluate(`const a = [1, 2], b = 3;`, 'a[\'concat\'](b)')).toEqual([1, 2, 3]);
expect(evaluate(`const a = [1, 2], b = 3, c = [4, 5];`, 'a[\'concat\'](b, c)')).toEqual([
1, 2, 3, 4, 5
]);
expect(evaluate(`const a = [1, 2], b = [3, 4]`, 'a[\'concat\'](...b)')).toEqual([1, 2, 3, 4]);
});
it('negation works', () => {
expect(evaluate(`const x = 3;`, '!x')).toEqual(false);
expect(evaluate(`const x = 3;`, '!!x')).toEqual(true);
});
it('negation works', () => {
expect(evaluate(`const x = 3;`, '!x')).toEqual(false);
expect(evaluate(`const x = 3;`, '!!x')).toEqual(true);
});
it('resolves access from external variable declarations as dynamic value', () => {
const value = evaluate('declare const window: any;', 'window.location');
if (!(value instanceof DynamicValue)) {
return fail(`Should have resolved to a DynamicValue`);
}
expect(value.isFromDynamicInput()).toEqual(true);
expect(value.node.getText()).toEqual('window.location');
if (!(value.reason instanceof DynamicValue)) {
return fail(`Should have a DynamicValue as reason`);
}
expect(value.reason.isFromExternalReference()).toEqual(true);
expect(value.reason.node.getText()).toEqual('window: any');
});
it('resolves access from external variable declarations as dynamic value', () => {
const value = evaluate('declare const window: any;', 'window.location');
if (!(value instanceof DynamicValue)) {
return fail(`Should have resolved to a DynamicValue`);
}
expect(value.isFromDynamicInput()).toEqual(true);
expect(value.node.getText()).toEqual('window.location');
if (!(value.reason instanceof DynamicValue)) {
return fail(`Should have a DynamicValue as reason`);
}
expect(value.reason.isFromExternalReference()).toEqual(true);
expect(value.reason.node.getText()).toEqual('window: any');
});
it('imports work', () => {
const {program} = makeProgram([
{name: 'second.ts', contents: 'export function foo(bar) { return bar; }'},
{
name: 'entry.ts',
contents: `
it('imports work', () => {
const {program} = makeProgram([
{name: _('/second.ts'), contents: 'export function foo(bar) { return bar; }'},
{
name: _('/entry.ts'),
contents: `
import {foo} from './second';
const target$ = foo;
`
},
]);
const checker = program.getTypeChecker();
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !;
const evaluator = makeEvaluator(checker);
const resolved = evaluator.evaluate(expr);
if (!(resolved instanceof Reference)) {
return fail('Expected expression to resolve to a reference');
}
expect(ts.isFunctionDeclaration(resolved.node)).toBe(true);
const reference = resolved.getIdentityIn(program.getSourceFile('entry.ts') !);
if (reference === null) {
return fail('Expected to get an identifier');
}
expect(reference.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !);
});
},
]);
const checker = program.getTypeChecker();
const result = getDeclaration(program, _('/entry.ts'), 'target$', ts.isVariableDeclaration);
const expr = result.initializer !;
const evaluator = makeEvaluator(checker);
const resolved = evaluator.evaluate(expr);
if (!(resolved instanceof Reference)) {
return fail('Expected expression to resolve to a reference');
}
expect(ts.isFunctionDeclaration(resolved.node)).toBe(true);
const reference = resolved.getIdentityIn(getSourceFileOrError(program, _('/entry.ts')));
if (reference === null) {
return fail('Expected to get an identifier');
}
expect(reference.getSourceFile()).toEqual(getSourceFileOrError(program, _('/entry.ts')));
});
it('absolute imports work', () => {
const {program} = makeProgram([
{name: 'node_modules/some_library/index.d.ts', contents: 'export declare function foo(bar);'},
{
name: 'entry.ts',
contents: `
it('absolute imports work', () => {
const {program} = makeProgram([
{
name: _('/node_modules/some_library/index.d.ts'),
contents: 'export declare function foo(bar);'
},
{
name: _('/entry.ts'),
contents: `
import {foo} from 'some_library';
const target$ = foo;
`
},
]);
const checker = program.getTypeChecker();
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !;
const evaluator = makeEvaluator(checker);
const resolved = evaluator.evaluate(expr);
if (!(resolved instanceof Reference)) {
return fail('Expected expression to resolve to an absolute reference');
}
expect(owningModuleOf(resolved)).toBe('some_library');
expect(ts.isFunctionDeclaration(resolved.node)).toBe(true);
const reference = resolved.getIdentityIn(program.getSourceFile('entry.ts') !);
expect(reference).not.toBeNull();
expect(reference !.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !);
});
},
]);
const checker = program.getTypeChecker();
const result = getDeclaration(program, _('/entry.ts'), 'target$', ts.isVariableDeclaration);
const expr = result.initializer !;
const evaluator = makeEvaluator(checker);
const resolved = evaluator.evaluate(expr);
if (!(resolved instanceof Reference)) {
return fail('Expected expression to resolve to an absolute reference');
}
expect(owningModuleOf(resolved)).toBe('some_library');
expect(ts.isFunctionDeclaration(resolved.node)).toBe(true);
const reference = resolved.getIdentityIn(getSourceFileOrError(program, _('/entry.ts')));
expect(reference).not.toBeNull();
expect(reference !.getSourceFile()).toEqual(getSourceFileOrError(program, _('/entry.ts')));
});
it('reads values from default exports', () => {
const value = evaluate(
`
it('reads values from default exports', () => {
const value = evaluate(
`
import mod from './second';
`,
'mod.property', [
{name: 'second.ts', contents: 'export default {property: "test"}'},
]);
expect(value).toEqual('test');
});
it('reads values from named exports', () => {
const value = evaluate(`import * as mod from './second';`, 'mod.a.property', [
{name: 'second.ts', contents: 'export const a = {property: "test"};'},
]);
expect(value).toEqual('test');
});
it('chain of re-exports works', () => {
const value = evaluate(`import * as mod from './direct-reexport';`, 'mod.value.property', [
{name: 'const.ts', contents: 'export const value = {property: "test"};'},
{name: 'def.ts', contents: `import {value} from './const'; export default value;`},
{name: 'indirect-reexport.ts', contents: `import value from './def'; export {value};`},
{name: 'direct-reexport.ts', contents: `export {value} from './indirect-reexport';`},
]);
expect(value).toEqual('test');
});
it('map spread works', () => {
const map: Map<string, number> = evaluate<Map<string, number>>(
`const a = {a: 1}; const b = {b: 2, c: 1}; const c = {...a, ...b, c: 3};`, 'c');
const obj: {[key: string]: number} = {};
map.forEach((value, key) => obj[key] = value);
expect(obj).toEqual({
a: 1,
b: 2,
c: 3,
'mod.property', [
{name: _('/second.ts'), contents: 'export default {property: "test"}'},
]);
expect(value).toEqual('test');
});
});
it('indirected-via-object function call works', () => {
expect(evaluate(
`
it('reads values from named exports', () => {
const value = evaluate(`import * as mod from './second';`, 'mod.a.property', [
{name: _('/second.ts'), contents: 'export const a = {property: "test"};'},
]);
expect(value).toEqual('test');
});
it('chain of re-exports works', () => {
const value = evaluate(`import * as mod from './direct-reexport';`, 'mod.value.property', [
{name: _('/const.ts'), contents: 'export const value = {property: "test"};'},
{name: _('/def.ts'), contents: `import {value} from './const'; export default value;`},
{name: _('/indirect-reexport.ts'), contents: `import value from './def'; export {value};`},
{name: _('/direct-reexport.ts'), contents: `export {value} from './indirect-reexport';`},
]);
expect(value).toEqual('test');
});
it('map spread works', () => {
const map: Map<string, number> = evaluate<Map<string, number>>(
`const a = {a: 1}; const b = {b: 2, c: 1}; const c = {...a, ...b, c: 3};`, 'c');
const obj: {[key: string]: number} = {};
map.forEach((value, key) => obj[key] = value);
expect(obj).toEqual({
a: 1,
b: 2,
c: 3,
});
});
it('indirected-via-object function call works', () => {
expect(evaluate(
`
function fn(res) { return res; }
const obj = {fn};
`,
'obj.fn("test")'))
.toEqual('test');
});
'obj.fn("test")'))
.toEqual('test');
});
it('template expressions work',
() => { expect(evaluate('const a = 2, b = 4;', '`1${a}3${b}5`')).toEqual('12345'); });
it('template expressions work',
() => { expect(evaluate('const a = 2, b = 4;', '`1${a}3${b}5`')).toEqual('12345'); });
it('enum resolution works', () => {
const result = evaluate(
`
it('enum resolution works', () => {
const result = evaluate(
`
enum Foo {
A,
B,
@ -300,194 +307,209 @@ describe('ngtsc metadata', () => {
const r = Foo.B;
`,
'r');
if (!(result instanceof EnumValue)) {
return fail(`result is not an EnumValue`);
}
expect(result.enumRef.node.name.text).toBe('Foo');
expect(result.name).toBe('B');
});
'r');
if (!(result instanceof EnumValue)) {
return fail(`result is not an EnumValue`);
}
expect(result.enumRef.node.name.text).toBe('Foo');
expect(result.name).toBe('B');
});
it('variable declaration resolution works', () => {
const value = evaluate(`import {value} from './decl';`, 'value', [
{name: 'decl.d.ts', contents: 'export declare let value: number;'},
]);
expect(value instanceof Reference).toBe(true);
});
it('variable declaration resolution works', () => {
const value = evaluate(`import {value} from './decl';`, 'value', [
{name: _('/decl.d.ts'), contents: 'export declare let value: number;'},
]);
expect(value instanceof Reference).toBe(true);
});
it('should resolve shorthand properties to values', () => {
const {program} = makeProgram([
{name: 'entry.ts', contents: `const prop = 42; const target$ = {prop};`},
]);
const checker = program.getTypeChecker();
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !as ts.ObjectLiteralExpression;
const prop = expr.properties[0] as ts.ShorthandPropertyAssignment;
const evaluator = makeEvaluator(checker);
const resolved = evaluator.evaluate(prop.name);
expect(resolved).toBe(42);
});
it('should resolve shorthand properties to values', () => {
const {program} = makeProgram([
{name: _('/entry.ts'), contents: `const prop = 42; const target$ = {prop};`},
]);
const checker = program.getTypeChecker();
const result = getDeclaration(program, _('/entry.ts'), 'target$', ts.isVariableDeclaration);
const expr = result.initializer !as ts.ObjectLiteralExpression;
const prop = expr.properties[0] as ts.ShorthandPropertyAssignment;
const evaluator = makeEvaluator(checker);
const resolved = evaluator.evaluate(prop.name);
expect(resolved).toBe(42);
});
it('should resolve dynamic values in object literals', () => {
const {program} = makeProgram([
{name: 'decl.d.ts', contents: 'export declare const fn: any;'},
{
name: 'entry.ts',
contents: `import {fn} from './decl'; const prop = fn.foo(); const target$ = {value: prop};`
},
]);
const checker = program.getTypeChecker();
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !as ts.ObjectLiteralExpression;
const evaluator = makeEvaluator(checker);
const resolved = evaluator.evaluate(expr);
if (!(resolved instanceof Map)) {
return fail('Should have resolved to a Map');
}
const value = resolved.get('value') !;
if (!(value instanceof DynamicValue)) {
return fail(`Should have resolved 'value' to a DynamicValue`);
}
const prop = expr.properties[0] as ts.PropertyAssignment;
expect(value.node).toBe(prop.initializer);
});
it('should resolve dynamic values in object literals', () => {
const {program} = makeProgram([
{name: _('/decl.d.ts'), contents: 'export declare const fn: any;'},
{
name: _('/entry.ts'),
contents:
`import {fn} from './decl'; const prop = fn.foo(); const target$ = {value: prop};`
},
]);
const checker = program.getTypeChecker();
const result = getDeclaration(program, _('/entry.ts'), 'target$', ts.isVariableDeclaration);
const expr = result.initializer !as ts.ObjectLiteralExpression;
const evaluator = makeEvaluator(checker);
const resolved = evaluator.evaluate(expr);
if (!(resolved instanceof Map)) {
return fail('Should have resolved to a Map');
}
const value = resolved.get('value') !;
if (!(value instanceof DynamicValue)) {
return fail(`Should have resolved 'value' to a DynamicValue`);
}
const prop = expr.properties[0] as ts.PropertyAssignment;
expect(value.node).toBe(prop.initializer);
});
it('should resolve enums in template expressions', () => {
const value =
evaluate(`enum Test { VALUE = 'test', } const value = \`a.\${Test.VALUE}.b\`;`, 'value');
expect(value).toBe('a.test.b');
});
it('should resolve enums in template expressions', () => {
const value =
evaluate(`enum Test { VALUE = 'test', } const value = \`a.\${Test.VALUE}.b\`;`, 'value');
expect(value).toBe('a.test.b');
});
it('should not attach identifiers to FFR-resolved values', () => {
const value = evaluate(
`
it('should not attach identifiers to FFR-resolved values', () => {
const value = evaluate(
`
declare function foo(arg: any): any;
class Target {}
const indir = foo(Target);
const value = indir;
`,
'value', [], firstArgFfr);
if (!(value instanceof Reference)) {
return fail('Expected value to be a Reference');
}
const id = value.getIdentityIn(value.node.getSourceFile());
if (id === null) {
return fail('Expected value to have an identity');
}
expect(id.text).toEqual('Target');
});
'value', [], firstArgFfr);
if (!(value instanceof Reference)) {
return fail('Expected value to be a Reference');
}
const id = value.getIdentityIn(value.node.getSourceFile());
if (id === null) {
return fail('Expected value to have an identity');
}
expect(id.text).toEqual('Target');
});
it('should resolve functions with more than one statement to an unknown value', () => {
const value = evaluate(`function foo(bar) { const b = bar; return b; }`, 'foo("test")');
it('should resolve functions with more than one statement to an unknown value', () => {
const value = evaluate(`function foo(bar) { const b = bar; return b; }`, 'foo("test")');
if (!(value instanceof DynamicValue)) {
return fail(`Should have resolved to a DynamicValue`);
}
if (!(value instanceof DynamicValue)) {
return fail(`Should have resolved to a DynamicValue`);
}
expect(value.isFromUnknown()).toBe(true);
expect((value.node as ts.CallExpression).expression.getText()).toBe('foo');
});
expect(value.isFromUnknown()).toBe(true);
expect((value.node as ts.CallExpression).expression.getText()).toBe('foo');
});
it('should evaluate TypeScript __spread helper', () => {
const {checker, expression} = makeExpression(
`
it('should evaluate TypeScript __spread helper', () => {
const {checker, expression} = makeExpression(
`
import * as tslib from 'tslib';
const a = [1];
const b = [2, 3];
`,
'tslib.__spread(a, b)', [
{
name: 'node_modules/tslib/index.d.ts',
contents: `
'tslib.__spread(a, b)', [
{
name: _('/node_modules/tslib/index.d.ts'),
contents: `
export declare function __spread(...args: any[]): any[];
`
},
]);
const reflectionHost = new TsLibAwareReflectionHost(checker);
const evaluator = new PartialEvaluator(reflectionHost, checker);
const value = evaluator.evaluate(expression);
expect(value).toEqual([1, 2, 3]);
});
describe('(visited file tracking)', () => {
it('should track each time a source file is visited', () => {
const trackFileDependency = jasmine.createSpy('DependencyTracker');
const {expression, checker} =
makeExpression(`class A { static foo = 42; } function bar() { return A.foo; }`, 'bar()');
const evaluator = makeEvaluator(checker, {trackFileDependency});
evaluator.evaluate(expression);
expect(trackFileDependency).toHaveBeenCalledTimes(2); // two declaration visited
expect(trackFileDependency.calls.allArgs().map(args => [args[0].fileName, args[1].fileName]))
.toEqual([['/entry.ts', '/entry.ts'], ['/entry.ts', '/entry.ts']]);
},
]);
const reflectionHost = new TsLibAwareReflectionHost(checker);
const evaluator = new PartialEvaluator(reflectionHost, checker);
const value = evaluator.evaluate(expression);
expect(value).toEqual([1, 2, 3]);
});
it('should track imported source files', () => {
const trackFileDependency = jasmine.createSpy('DependencyTracker');
const {expression, checker} = makeExpression(`import {Y} from './other'; const A = Y;`, 'A', [
{name: 'other.ts', contents: `export const Y = 'test';`},
{name: 'not-visited.ts', contents: `export const Z = 'nope';`}
]);
const evaluator = makeEvaluator(checker, {trackFileDependency});
evaluator.evaluate(expression);
expect(trackFileDependency).toHaveBeenCalledTimes(2);
expect(trackFileDependency.calls.allArgs().map(args => [args[0].fileName, args[1].fileName]))
.toEqual([
['/entry.ts', '/entry.ts'],
['/other.ts', '/entry.ts'],
]);
});
describe('(visited file tracking)', () => {
it('should track each time a source file is visited', () => {
const trackFileDependency = jasmine.createSpy('DependencyTracker');
const {expression, checker} = makeExpression(
`class A { static foo = 42; } function bar() { return A.foo; }`, 'bar()');
const evaluator = makeEvaluator(checker, {trackFileDependency});
evaluator.evaluate(expression);
expect(trackFileDependency).toHaveBeenCalledTimes(2); // two declaration visited
expect(
trackFileDependency.calls.allArgs().map(args => [args[0].fileName, args[1].fileName]))
.toEqual([[_('/entry.ts'), _('/entry.ts')], [_('/entry.ts'), _('/entry.ts')]]);
});
it('should track files passed through during re-exports', () => {
const trackFileDependency = jasmine.createSpy('DependencyTracker');
const {expression, checker} =
makeExpression(`import * as mod from './direct-reexport';`, 'mod.value.property', [
{name: 'const.ts', contents: 'export const value = {property: "test"};'},
{name: 'def.ts', contents: `import {value} from './const'; export default value;`},
{name: 'indirect-reexport.ts', contents: `import value from './def'; export {value};`},
{name: 'direct-reexport.ts', contents: `export {value} from './indirect-reexport';`},
]);
const evaluator = makeEvaluator(checker, {trackFileDependency});
evaluator.evaluate(expression);
expect(trackFileDependency).toHaveBeenCalledTimes(2);
expect(trackFileDependency.calls.allArgs().map(args => [args[0].fileName, args[1].fileName]))
.toEqual([
['/direct-reexport.ts', '/entry.ts'],
// Not '/indirect-reexport.ts' or '/def.ts'.
// TS skips through them when finding the original symbol for `value`
['/const.ts', '/entry.ts'],
]);
it('should track imported source files', () => {
const trackFileDependency = jasmine.createSpy('DependencyTracker');
const {expression, checker} =
makeExpression(`import {Y} from './other'; const A = Y;`, 'A', [
{name: _('/other.ts'), contents: `export const Y = 'test';`},
{name: _('/not-visited.ts'), contents: `export const Z = 'nope';`}
]);
const evaluator = makeEvaluator(checker, {trackFileDependency});
evaluator.evaluate(expression);
expect(trackFileDependency).toHaveBeenCalledTimes(2);
expect(
trackFileDependency.calls.allArgs().map(args => [args[0].fileName, args[1].fileName]))
.toEqual([
[_('/entry.ts'), _('/entry.ts')],
[_('/other.ts'), _('/entry.ts')],
]);
});
it('should track files passed through during re-exports', () => {
const trackFileDependency = jasmine.createSpy('DependencyTracker');
const {expression, checker} =
makeExpression(`import * as mod from './direct-reexport';`, 'mod.value.property', [
{name: _('/const.ts'), contents: 'export const value = {property: "test"};'},
{
name: _('/def.ts'),
contents: `import {value} from './const'; export default value;`
},
{
name: _('/indirect-reexport.ts'),
contents: `import value from './def'; export {value};`
},
{
name: _('/direct-reexport.ts'),
contents: `export {value} from './indirect-reexport';`
},
]);
const evaluator = makeEvaluator(checker, {trackFileDependency});
evaluator.evaluate(expression);
expect(trackFileDependency).toHaveBeenCalledTimes(2);
expect(
trackFileDependency.calls.allArgs().map(args => [args[0].fileName, args[1].fileName]))
.toEqual([
[_('/direct-reexport.ts'), _('/entry.ts')],
// Not '/indirect-reexport.ts' or '/def.ts'.
// TS skips through them when finding the original symbol for `value`
[_('/const.ts'), _('/entry.ts')],
]);
});
});
});
});
/**
* Customizes the resolution of functions to recognize functions from tslib. Such functions are not
* handled specially in the default TypeScript host, as only ngcc's ES5 host will have special
* powers to recognize functions from tslib.
*/
class TsLibAwareReflectionHost extends TypeScriptReflectionHost {
getDefinitionOfFunction(node: ts.Node): FunctionDefinition|null {
if (ts.isFunctionDeclaration(node)) {
const helper = getTsHelperFn(node);
if (helper !== null) {
return {
node,
body: null, helper,
parameters: [],
};
/**
* Customizes the resolution of functions to recognize functions from tslib. Such functions are
* not handled specially in the default TypeScript host, as only ngcc's ES5 host will have special
* powers to recognize functions from tslib.
*/
class TsLibAwareReflectionHost extends TypeScriptReflectionHost {
getDefinitionOfFunction(node: ts.Node): FunctionDefinition|null {
if (ts.isFunctionDeclaration(node)) {
const helper = getTsHelperFn(node);
if (helper !== null) {
return {
node,
body: null, helper,
parameters: [],
};
}
}
return super.getDefinitionOfFunction(node);
}
return super.getDefinitionOfFunction(node);
}
}
function getTsHelperFn(node: ts.FunctionDeclaration): TsHelperFn|null {
const name = node.name !== undefined && ts.isIdentifier(node.name) && node.name.text;
function getTsHelperFn(node: ts.FunctionDeclaration): TsHelperFn|null {
const name = node.name !== undefined && ts.isIdentifier(node.name) && node.name.text;
if (name === '__spread') {
return TsHelperFn.Spread;
} else {
return null;
if (name === '__spread') {
return TsHelperFn.Spread;
} else {
return null;
}
}
}
});

View File

@ -5,27 +5,29 @@
* 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 {absoluteFrom} from '../../file_system';
import {TestFile} from '../../file_system/testing';
import {Reference} from '../../imports';
import {TypeScriptReflectionHost} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {getDeclaration, makeProgram} from '../../testing';
import {DependencyTracker, ForeignFunctionResolver, PartialEvaluator} from '../src/interface';
import {ResolvedValue} from '../src/result';
export function makeExpression(
code: string, expr: string, supportingFiles: {name: string, contents: string}[] = []): {
export function makeExpression(code: string, expr: string, supportingFiles: TestFile[] = []): {
expression: ts.Expression,
host: ts.CompilerHost,
checker: ts.TypeChecker,
program: ts.Program,
options: ts.CompilerOptions
} {
const {program, options, host} = makeProgram(
[{name: 'entry.ts', contents: `${code}; const target$ = ${expr};`}, ...supportingFiles]);
const {program, options, host} = makeProgram([
{name: absoluteFrom('/entry.ts'), contents: `${code}; const target$ = ${expr};`},
...supportingFiles
]);
const checker = program.getTypeChecker();
const decl = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const decl =
getDeclaration(program, absoluteFrom('/entry.ts'), 'target$', ts.isVariableDeclaration);
return {
expression: decl.initializer !,
host,
@ -42,7 +44,7 @@ export function makeEvaluator(
}
export function evaluate<T extends ResolvedValue>(
code: string, expr: string, supportingFiles: {name: string, contents: string}[] = [],
code: string, expr: string, supportingFiles: TestFile[] = [],
foreignFunctionResolver?: ForeignFunctionResolver): T {
const {expression, checker} = makeExpression(code, expr, supportingFiles);
const evaluator = makeEvaluator(checker);

View File

@ -1,45 +0,0 @@
# About paths in ngtsc
Within the compiler, there are a number of different types of file system or URL "paths" which are manipulated as strings. While it's possible to declare the variables and fields which store these different kinds of paths using the 'string' type, this has significant drawbacks:
* When calling a function which accepts a path as an argument, it's not clear what kind of path should be passed.
* It can be expensive to check whether a path is properly formatted, and without types it's easy to fall into the habit of normalizing different kinds of paths repeatedly.
* There is no static check to detect if paths are improperly used in the wrong context (e.g. a relative path passed where an absolute path was required). This can cause subtle bugs.
* When running on Windows, some paths can use different conventions (e.g. forward vs back slashes). It's not always clear when a path needs to be checked for the correct convention.
To address these issues, ngtsc has specific static types for each kind of path in the system. These types are not mutually assignable, nor can they be directly assigned from `string`s (though they can be assigned _to_ `string`s). Conversion between `string`s and these specific path types happens through a narrow API which validates that all typed paths are valid.
# The different path kinds
All paths in the type system use POSIX format (`/` separators).
## `AbsoluteFsPath`
This path type represents an absolute path to a physical directory or file. For example, `/foo/bar.txt`.
## `PathSegment`
This path type represents a relative path to a directory or file. It only makes sense in the context of some directory (e.g. the working directory) or set of directories to search, and does not need to necessarily represent a relative path between two physical files.
## `LogicalProjectPath`
This path type represents a path to a file in TypeScript's logical file system.
TypeScript supports multiple root directories for a given project, which are effectively overlayed to obtain a file layout. For example, if a project has two root directories `foo` and `bar` with the layout:
```text
/foo
/foo/foo.ts
/bar
/bar/bar.ts
```
Then `foo.ts` could theoretically contain:
```typescript
import {Bar} from './bar';
```
This import of `./bar` is not a valid relative path from `foo.ts` to `bar.ts` on the physical filesystem, but is valid in the context of the project because the contents of the `foo` and `bar` directories are overlayed as far as TypeScript is concerned.
In this example, `/foo/foo.ts` has a `LogicalProjectPath` of `/foo.ts` and `/bar/bar.ts` has a `LogicalProjectPath` of `/bar.ts`, allowing the module specifier in the import (`./bar`) to be resolved via standard path operations.

View File

@ -1,122 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 path from 'path';
import * as ts from 'typescript';
import {isAbsolutePath, normalizeSeparators} from './util';
/**
* A `string` representing a specific type of path, with a particular brand `B`.
*
* A `string` is not assignable to a `BrandedPath`, but a `BrandedPath` is assignable to a `string`.
* Two `BrandedPath`s with different brands are not mutually assignable.
*/
export type BrandedPath<B extends string> = string & {
_brand: B;
};
/**
* A fully qualified path in the file system, in POSIX form.
*/
export type AbsoluteFsPath = BrandedPath<'AbsoluteFsPath'>;
/**
* A path that's relative to another (unspecified) root.
*
* This does not necessarily have to refer to a physical file.
*/
export type PathSegment = BrandedPath<'PathSegment'>;
/**
* Contains utility functions for creating and manipulating `AbsoluteFsPath`s.
*/
export const AbsoluteFsPath = {
/**
* Convert the path `str` to an `AbsoluteFsPath`, throwing an error if it's not an absolute path.
*/
from: function(str: string): AbsoluteFsPath {
if (str.startsWith('/') && process.platform === 'win32') {
// in Windows if it's absolute path and starts with `/` we shall
// resolve it and return it including the drive.
str = path.resolve(str);
}
const normalized = normalizeSeparators(str);
if (!isAbsolutePath(normalized)) {
throw new Error(`Internal Error: AbsoluteFsPath.from(${str}): path is not absolute`);
}
return normalized as AbsoluteFsPath;
},
/**
* Assume that the path `str` is an `AbsoluteFsPath` in the correct format already.
*/
fromUnchecked: function(str: string): AbsoluteFsPath { return str as AbsoluteFsPath;},
/**
* Extract an `AbsoluteFsPath` from a `ts.SourceFile`.
*
* This is cheaper than calling `AbsoluteFsPath.from(sf.fileName)`, as source files already have
* their file path in absolute POSIX format.
*/
fromSourceFile: function(sf: ts.SourceFile): AbsoluteFsPath {
// ts.SourceFile paths are always absolute.
return sf.fileName as AbsoluteFsPath;
},
/**
* Wrapper around `path.dirname` that returns an absolute path.
*/
dirname: function(file: AbsoluteFsPath):
AbsoluteFsPath { return AbsoluteFsPath.fromUnchecked(path.dirname(file));},
/**
* Wrapper around `path.join` that returns an absolute path.
*/
join: function(basePath: AbsoluteFsPath, ...paths: string[]):
AbsoluteFsPath { return AbsoluteFsPath.fromUnchecked(path.posix.join(basePath, ...paths));},
/**
* Wrapper around `path.resolve` that returns an absolute paths.
*/
resolve: function(basePath: string, ...paths: string[]):
AbsoluteFsPath { return AbsoluteFsPath.from(path.resolve(basePath, ...paths));},
/** Returns true when the path provided is the root path. */
isRoot: function(path: AbsoluteFsPath): boolean { return AbsoluteFsPath.dirname(path) === path;},
};
/**
* Contains utility functions for creating and manipulating `PathSegment`s.
*/
export const PathSegment = {
/**
* Convert the path `str` to a `PathSegment`, throwing an error if it's not a relative path.
*/
fromFsPath: function(str: string): PathSegment {
const normalized = normalizeSeparators(str);
if (isAbsolutePath(normalized)) {
throw new Error(`Internal Error: PathSegment.fromFsPath(${str}): path is not relative`);
}
return normalized as PathSegment;
},
/**
* Convert the path `str` to a `PathSegment`, while assuming that `str` is already normalized.
*/
fromUnchecked: function(str: string): PathSegment { return str as PathSegment;},
/**
* Wrapper around `path.relative` that returns a `PathSegment`.
*/
relative: function(from: AbsoluteFsPath, to: AbsoluteFsPath):
PathSegment { return PathSegment.fromFsPath(path.relative(from, to));},
basename: function(filePath: string, extension?: string):
PathSegment { return path.basename(filePath, extension) as PathSegment;}
};

View File

@ -1,63 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {LogicalFileSystem, LogicalProjectPath} from '../src/logical';
import {AbsoluteFsPath} from '../src/types';
describe('logical paths', () => {
describe('LogicalFileSystem', () => {
it('should determine logical paths in a single root file system', () => {
const fs = new LogicalFileSystem([abs('/test')]);
expect(fs.logicalPathOfFile(abs('/test/foo/foo.ts')))
.toEqual('/foo/foo' as LogicalProjectPath);
expect(fs.logicalPathOfFile(abs('/test/bar/bar.ts')))
.toEqual('/bar/bar' as LogicalProjectPath);
expect(fs.logicalPathOfFile(abs('/not-test/bar.ts'))).toBeNull();
});
it('should determine logical paths in a multi-root file system', () => {
const fs = new LogicalFileSystem([abs('/test/foo'), abs('/test/bar')]);
expect(fs.logicalPathOfFile(abs('/test/foo/foo.ts'))).toEqual('/foo' as LogicalProjectPath);
expect(fs.logicalPathOfFile(abs('/test/bar/bar.ts'))).toEqual('/bar' as LogicalProjectPath);
});
it('should continue to work when one root is a child of another', () => {
const fs = new LogicalFileSystem([abs('/test'), abs('/test/dist')]);
expect(fs.logicalPathOfFile(abs('/test/foo.ts'))).toEqual('/foo' as LogicalProjectPath);
expect(fs.logicalPathOfFile(abs('/test/dist/foo.ts'))).toEqual('/foo' as LogicalProjectPath);
});
it('should always return `/` prefixed logical paths', () => {
const rootFs = new LogicalFileSystem([abs('/')]);
expect(rootFs.logicalPathOfFile(abs('/foo/foo.ts')))
.toEqual('/foo/foo' as LogicalProjectPath);
const nonRootFs = new LogicalFileSystem([abs('/test/')]);
expect(nonRootFs.logicalPathOfFile(abs('/test/foo/foo.ts')))
.toEqual('/foo/foo' as LogicalProjectPath);
});
});
describe('utilities', () => {
it('should give a relative path between two adjacent logical files', () => {
const res = LogicalProjectPath.relativePathBetween(
'/foo' as LogicalProjectPath, '/bar' as LogicalProjectPath);
expect(res).toEqual('./bar');
});
it('should give a relative path between two non-adjacent logical files', () => {
const res = LogicalProjectPath.relativePathBetween(
'/foo/index' as LogicalProjectPath, '/bar/index' as LogicalProjectPath);
expect(res).toEqual('../bar/index');
});
});
});
function abs(file: string): AbsoluteFsPath {
return AbsoluteFsPath.from(file);
}

View File

@ -1,26 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {AbsoluteFsPath} from '../src/types';
describe('path types', () => {
describe('AbsoluteFsPath', () => {
it('should not throw when creating one from an absolute path',
() => { expect(() => AbsoluteFsPath.from('/test.txt')).not.toThrow(); });
it('should not throw when creating one from a windows absolute path',
() => { expect(AbsoluteFsPath.from('C:\\test.txt')).toEqual('C:/test.txt'); });
it('should not throw when creating one from a windows absolute path with POSIX separators',
() => { expect(AbsoluteFsPath.from('C:/test.txt')).toEqual('C:/test.txt'); });
it('should throw when creating one from a non-absolute path',
() => { expect(() => AbsoluteFsPath.from('test.txt')).toThrow(); });
it('should support windows drive letters',
() => { expect(AbsoluteFsPath.from('D:\\foo\\test.txt')).toEqual('D:/foo/test.txt'); });
it('should convert Windows path separators to POSIX separators',
() => { expect(AbsoluteFsPath.from('\\foo\\test.txt')).toEqual('/foo/test.txt'); });
});
});

View File

@ -9,6 +9,7 @@ ts_library(
]),
deps = [
"//packages:types",
"//packages/compiler-cli/src/ngtsc/file_system",
"@npm//@types/node",
"@npm//typescript",
],

View File

@ -5,13 +5,10 @@
* 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
*/
/// <reference types="node" />
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {resolve} from '../../file_system';
import {PerfRecorder} from './api';
import {HrTime, mark, timeSinceInMicros} from './clock';
@ -83,10 +80,10 @@ export class PerfTracker implements PerfRecorder {
if (target.startsWith('ts:')) {
target = target.substr('ts:'.length);
const outFile = path.posix.resolve(host.getCurrentDirectory(), target);
const outFile = resolve(host.getCurrentDirectory(), target);
host.writeFile(outFile, json, false);
} else {
const outFile = path.posix.resolve(host.getCurrentDirectory(), target);
const outFile = resolve(host.getCurrentDirectory(), target);
fs.writeFileSync(outFile, json);
}
}

View File

@ -17,13 +17,13 @@ import {BaseDefDecoratorHandler} from './annotations/src/base_def';
import {CycleAnalyzer, ImportGraph} from './cycles';
import {ErrorCode, ngErrorCode} from './diagnostics';
import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point';
import {AbsoluteFsPath, LogicalFileSystem, absoluteFrom} from './file_system';
import {AbsoluteModuleStrategy, AliasGenerator, AliasStrategy, DefaultImportTracker, FileToModuleHost, FileToModuleStrategy, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports';
import {IncrementalState} from './incremental';
import {IndexedComponent, IndexingContext} from './indexer';
import {generateAnalysis} from './indexer/src/transform';
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, LocalMetadataRegistry, MetadataReader} from './metadata';
import {PartialEvaluator} from './partial_evaluator';
import {AbsoluteFsPath, LogicalFileSystem} from './path';
import {NOOP_PERF_RECORDER, PerfRecorder, PerfTracker} from './perf';
import {TypeScriptReflectionHost} from './reflection';
import {HostResourceLoader} from './resource_loader';
@ -35,7 +35,7 @@ import {IvyCompilation, declarationTransformFactory, ivyTransformFactory} from '
import {aliasTransformFactory} from './transform/src/alias';
import {TypeCheckContext, TypeCheckingConfig, typeCheckFilePath} from './typecheck';
import {normalizeSeparators} from './util/src/path';
import {getRootDirs, isDtsPath, resolveModuleName} from './util/src/typescript';
import {getRootDirs, getSourceFileOrNull, isDtsPath, resolveModuleName} from './util/src/typescript';
export class NgtscProgram implements api.Program {
private tsProgram: ts.Program;
@ -84,7 +84,7 @@ export class NgtscProgram implements api.Program {
this.closureCompilerEnabled = !!options.annotateForClosureCompiler;
this.resourceManager = new HostResourceLoader(host, options);
const shouldGenerateShims = options.allowEmptyCodegenFiles || false;
const normalizedRootNames = rootNames.map(n => AbsoluteFsPath.from(n));
const normalizedRootNames = rootNames.map(n => absoluteFrom(n));
if (host.fileNameToModuleName !== undefined) {
this.fileToModuleHost = host as FileToModuleHost;
}
@ -115,7 +115,7 @@ export class NgtscProgram implements api.Program {
generators.push(new TypeCheckShimGenerator(this.typeCheckFilePath));
rootFiles.push(this.typeCheckFilePath);
let entryPoint: string|null = null;
let entryPoint: AbsoluteFsPath|null = null;
if (options.flatModuleOutFile !== undefined) {
entryPoint = findFlatIndexEntryPoint(normalizedRootNames);
if (entryPoint === null) {
@ -154,7 +154,7 @@ export class NgtscProgram implements api.Program {
ts.createProgram(rootFiles, options, this.host, oldProgram && oldProgram.reuseTsProgram);
this.reuseTsProgram = this.tsProgram;
this.entryPoint = entryPoint !== null ? this.tsProgram.getSourceFile(entryPoint) || null : null;
this.entryPoint = entryPoint !== null ? getSourceFileOrNull(this.tsProgram, entryPoint) : null;
this.moduleResolver = new ModuleResolver(this.tsProgram, options, this.host);
this.cycleAnalyzer = new CycleAnalyzer(new ImportGraph(this.moduleResolver));
this.defaultImportTracker = new DefaultImportTracker();
@ -345,7 +345,7 @@ export class NgtscProgram implements api.Program {
const emitSpan = this.perfRecorder.start('emit');
const emitResults: ts.EmitResult[] = [];
const typeCheckFile = this.tsProgram.getSourceFile(this.typeCheckFilePath);
const typeCheckFile = getSourceFileOrNull(this.tsProgram, this.typeCheckFilePath);
for (const targetSourceFile of this.tsProgram.getSourceFiles()) {
if (targetSourceFile.isDeclarationFile || targetSourceFile === typeCheckFile) {

View File

@ -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",

View File

@ -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]}.`);
}
}
}
});

View File

@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as fs from 'fs';
import * as ts from 'typescript';
import {CompilerHost} from '../transformers/api';
import {ResourceLoader} from './annotations/src/api';
@ -99,7 +98,7 @@ export class HostResourceLoader implements ResourceLoader {
}
const result = this.host.readResource ? this.host.readResource(resolvedUrl) :
fs.readFileSync(resolvedUrl, 'utf8');
this.host.readFile(resolvedUrl);
if (typeof result !== 'string') {
throw new Error(`HostResourceLoader: loader(${resolvedUrl}) returned a Promise`);
}
@ -126,7 +125,7 @@ export class HostResourceLoader implements ResourceLoader {
const candidateLocations = this.getCandidateLocations(url, fromFile);
for (const candidate of candidateLocations) {
if (fs.existsSync(candidate)) {
if (this.host.fileExists(candidate)) {
return candidate;
} else if (CSS_PREPROCESSOR_EXT.test(candidate)) {
/**
@ -135,7 +134,7 @@ export class HostResourceLoader implements ResourceLoader {
* again.
*/
const cssFallbackUrl = candidate.replace(CSS_PREPROCESSOR_EXT, '.css');
if (fs.existsSync(cssFallbackUrl)) {
if (this.host.fileExists(cssFallbackUrl)) {
return cssFallbackUrl;
}
}

View File

@ -11,6 +11,8 @@ ts_library(
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/reflection",

View File

@ -5,14 +5,14 @@
* 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 {ExternalExpr, ExternalReference} from '@angular/compiler';
import * as ts from 'typescript';
import {absoluteFrom} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {AliasGenerator, FileToModuleHost, Reference} from '../../imports';
import {DtsMetadataReader} from '../../metadata';
import {ClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
import {makeProgram} from '../../testing/in_memory_typescript';
import {makeProgram} from '../../testing';
import {ExportScope} from '../src/api';
import {MetadataDtsModuleScopeResolver} from '../src/dependency';
@ -49,7 +49,7 @@ function makeTestEnv(
// Map the modules object to an array of files for `makeProgram`.
const files = Object.keys(modules).map(moduleName => {
return {
name: `node_modules/${moduleName}/index.d.ts`,
name: absoluteFrom(`/node_modules/${moduleName}/index.d.ts`),
contents: PROLOG + (modules as any)[moduleName],
};
});
@ -79,10 +79,11 @@ function makeTestEnv(
};
}
describe('MetadataDtsModuleScopeResolver', () => {
it('should produce an accurate scope for a basic NgModule', () => {
const {resolver, refs} = makeTestEnv({
'test': `
runInEachFileSystem(() => {
describe('MetadataDtsModuleScopeResolver', () => {
it('should produce an accurate scope for a basic NgModule', () => {
const {resolver, refs} = makeTestEnv({
'test': `
export declare class Dir {
static ngDirectiveDef: DirectiveMeta<Dir, '[dir]', ['exportAs'], {'input': 'input2'},
{'output': 'output2'}, ['query']>;
@ -92,15 +93,15 @@ describe('MetadataDtsModuleScopeResolver', () => {
static ngModuleDef: ModuleMeta<Module, [typeof Dir], never, [typeof Dir]>;
}
`
});
const {Dir, Module} = refs;
const scope = resolver.resolve(Module) !;
expect(scopeToRefs(scope)).toEqual([Dir]);
});
const {Dir, Module} = refs;
const scope = resolver.resolve(Module) !;
expect(scopeToRefs(scope)).toEqual([Dir]);
});
it('should produce an accurate scope when a module is exported', () => {
const {resolver, refs} = makeTestEnv({
'test': `
it('should produce an accurate scope when a module is exported', () => {
const {resolver, refs} = makeTestEnv({
'test': `
export declare class Dir {
static ngDirectiveDef: DirectiveMeta<Dir, '[dir]', never, never, never, never>;
}
@ -113,15 +114,15 @@ describe('MetadataDtsModuleScopeResolver', () => {
static ngModuleDef: ModuleMeta<ModuleB, never, never, [typeof ModuleA]>;
}
`
});
const {Dir, ModuleB} = refs;
const scope = resolver.resolve(ModuleB) !;
expect(scopeToRefs(scope)).toEqual([Dir]);
});
const {Dir, ModuleB} = refs;
const scope = resolver.resolve(ModuleB) !;
expect(scopeToRefs(scope)).toEqual([Dir]);
});
it('should resolve correctly across modules', () => {
const {resolver, refs} = makeTestEnv({
'declaration': `
it('should resolve correctly across modules', () => {
const {resolver, refs} = makeTestEnv({
'declaration': `
export declare class Dir {
static ngDirectiveDef: DirectiveMeta<Dir, '[dir]', never, never, never, never>;
}
@ -130,26 +131,26 @@ describe('MetadataDtsModuleScopeResolver', () => {
static ngModuleDef: ModuleMeta<ModuleA, [typeof Dir], never, [typeof Dir]>;
}
`,
'exported': `
'exported': `
import * as d from 'declaration';
export declare class ModuleB {
static ngModuleDef: ModuleMeta<ModuleB, never, never, [typeof d.ModuleA]>;
}
`
});
const {Dir, ModuleB} = refs;
const scope = resolver.resolve(ModuleB) !;
expect(scopeToRefs(scope)).toEqual([Dir]);
// Explicitly verify that the directive has the correct owning module.
expect(scope.exported.directives[0].ref.ownedByModuleGuess).toBe('declaration');
});
const {Dir, ModuleB} = refs;
const scope = resolver.resolve(ModuleB) !;
expect(scopeToRefs(scope)).toEqual([Dir]);
// Explicitly verify that the directive has the correct owning module.
expect(scope.exported.directives[0].ref.ownedByModuleGuess).toBe('declaration');
});
it('should write correct aliases for deep dependencies', () => {
const {resolver, refs} = makeTestEnv(
{
'deep': `
it('should write correct aliases for deep dependencies', () => {
const {resolver, refs} = makeTestEnv(
{
'deep': `
export declare class DeepDir {
static ngDirectiveDef: DirectiveMeta<DeepDir, '[deep]', never, never, never, never>;
}
@ -158,7 +159,7 @@ describe('MetadataDtsModuleScopeResolver', () => {
static ngModuleDef: ModuleMeta<DeepModule, [typeof DeepDir], never, [typeof DeepDir]>;
}
`,
'middle': `
'middle': `
import * as deep from 'deep';
export declare class MiddleDir {
@ -169,7 +170,7 @@ describe('MetadataDtsModuleScopeResolver', () => {
static ngModuleDef: ModuleMeta<MiddleModule, [typeof MiddleDir], never, [typeof MiddleDir, typeof deep.DeepModule]>;
}
`,
'shallow': `
'shallow': `
import * as middle from 'middle';
export declare class ShallowDir {
@ -180,26 +181,26 @@ describe('MetadataDtsModuleScopeResolver', () => {
static ngModuleDef: ModuleMeta<ShallowModule, [typeof ShallowDir], never, [typeof ShallowDir, typeof middle.MiddleModule]>;
}
`,
},
new AliasGenerator(testHost));
const {ShallowModule} = refs;
const scope = resolver.resolve(ShallowModule) !;
const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope);
expect(getAlias(DeepDir)).toEqual({
moduleName: 'root/shallow',
name: 'ɵng$root$deep$$DeepDir',
},
new AliasGenerator(testHost));
const {ShallowModule} = refs;
const scope = resolver.resolve(ShallowModule) !;
const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope);
expect(getAlias(DeepDir)).toEqual({
moduleName: 'root/shallow',
name: 'ɵng$root$deep$$DeepDir',
});
expect(getAlias(MiddleDir)).toEqual({
moduleName: 'root/shallow',
name: 'ɵng$root$middle$$MiddleDir',
});
expect(getAlias(ShallowDir)).toBeNull();
});
expect(getAlias(MiddleDir)).toEqual({
moduleName: 'root/shallow',
name: 'ɵng$root$middle$$MiddleDir',
});
expect(getAlias(ShallowDir)).toBeNull();
});
it('should write correct aliases for bare directives in exports', () => {
const {resolver, refs} = makeTestEnv(
{
'deep': `
it('should write correct aliases for bare directives in exports', () => {
const {resolver, refs} = makeTestEnv(
{
'deep': `
export declare class DeepDir {
static ngDirectiveDef: DirectiveMeta<DeepDir, '[deep]', never, never, never, never>;
}
@ -208,7 +209,7 @@ describe('MetadataDtsModuleScopeResolver', () => {
static ngModuleDef: ModuleMeta<DeepModule, [typeof DeepDir], never, [typeof DeepDir]>;
}
`,
'middle': `
'middle': `
import * as deep from 'deep';
export declare class MiddleDir {
@ -219,7 +220,7 @@ describe('MetadataDtsModuleScopeResolver', () => {
static ngModuleDef: ModuleMeta<MiddleModule, [typeof MiddleDir], [typeof deep.DeepModule], [typeof MiddleDir, typeof deep.DeepDir]>;
}
`,
'shallow': `
'shallow': `
import * as middle from 'middle';
export declare class ShallowDir {
@ -230,27 +231,27 @@ describe('MetadataDtsModuleScopeResolver', () => {
static ngModuleDef: ModuleMeta<ShallowModule, [typeof ShallowDir], never, [typeof ShallowDir, typeof middle.MiddleModule]>;
}
`,
},
new AliasGenerator(testHost));
const {ShallowModule} = refs;
const scope = resolver.resolve(ShallowModule) !;
const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope);
expect(getAlias(DeepDir)).toEqual({
moduleName: 'root/shallow',
name: 'ɵng$root$deep$$DeepDir',
},
new AliasGenerator(testHost));
const {ShallowModule} = refs;
const scope = resolver.resolve(ShallowModule) !;
const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope);
expect(getAlias(DeepDir)).toEqual({
moduleName: 'root/shallow',
name: 'ɵng$root$deep$$DeepDir',
});
expect(getAlias(MiddleDir)).toEqual({
moduleName: 'root/shallow',
name: 'ɵng$root$middle$$MiddleDir',
});
expect(getAlias(ShallowDir)).toBeNull();
});
expect(getAlias(MiddleDir)).toEqual({
moduleName: 'root/shallow',
name: 'ɵng$root$middle$$MiddleDir',
});
expect(getAlias(ShallowDir)).toBeNull();
});
it('should not use an alias if a directive is declared in the same file as the re-exporting module',
() => {
const {resolver, refs} = makeTestEnv(
{
'module': `
it('should not use an alias if a directive is declared in the same file as the re-exporting module',
() => {
const {resolver, refs} = makeTestEnv(
{
'module': `
export declare class DeepDir {
static ngDirectiveDef: DirectiveMeta<DeepDir, '[deep]', never, never, never, never>;
}
@ -263,25 +264,26 @@ describe('MetadataDtsModuleScopeResolver', () => {
static ngModuleDef: ModuleMeta<DeepExportModule, never, never, [typeof DeepModule]>;
}
`,
},
new AliasGenerator(testHost));
const {DeepExportModule} = refs;
const scope = resolver.resolve(DeepExportModule) !;
const [DeepDir] = scopeToRefs(scope);
expect(getAlias(DeepDir)).toBeNull();
});
});
},
new AliasGenerator(testHost));
const {DeepExportModule} = refs;
const scope = resolver.resolve(DeepExportModule) !;
const [DeepDir] = scopeToRefs(scope);
expect(getAlias(DeepDir)).toBeNull();
});
});
function scopeToRefs(scope: ExportScope): Reference<ClassDeclaration>[] {
const directives = scope.exported.directives.map(dir => dir.ref);
const pipes = scope.exported.pipes.map(pipe => pipe.ref);
return [...directives, ...pipes].sort((a, b) => a.debugName !.localeCompare(b.debugName !));
}
function getAlias(ref: Reference<ClassDeclaration>): ExternalReference|null {
if (ref.alias === null) {
return null;
} else {
return (ref.alias as ExternalExpr).value;
function scopeToRefs(scope: ExportScope): Reference<ClassDeclaration>[] {
const directives = scope.exported.directives.map(dir => dir.ref);
const pipes = scope.exported.pipes.map(pipe => pipe.ref);
return [...directives, ...pipes].sort((a, b) => a.debugName !.localeCompare(b.debugName !));
}
}
function getAlias(ref: Reference<ClassDeclaration>): ExternalReference|null {
if (ref.alias === null) {
return null;
} else {
return (ref.alias as ExternalExpr).value;
}
}
});

View File

@ -9,8 +9,8 @@ ts_library(
]),
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//@types/node",
"@npm//typescript",

View File

@ -5,12 +5,10 @@
* 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 path from 'path';
import * as ts from 'typescript';
import {AbsoluteFsPath, absoluteFrom, basename} from '../../file_system';
import {ImportRewriter} from '../../imports';
import {AbsoluteFsPath} from '../../path/src/types';
import {isNonDeclarationTsPath} from '../../util/src/typescript';
import {ShimGenerator} from './host';
@ -38,8 +36,7 @@ export class FactoryGenerator implements ShimGenerator {
return null;
}
const relativePathToSource =
'./' + path.posix.basename(original.fileName).replace(TS_DTS_SUFFIX, '');
const relativePathToSource = './' + basename(original.fileName).replace(TS_DTS_SUFFIX, '');
// Collect a list of classes that need to have factory types emitted for them. This list is
// overly broad as at this point the ts.TypeChecker hasn't been created, and can't be used to
// semantically understand which decorated types are actually decorated with Angular decorators.
@ -103,9 +100,8 @@ export class FactoryGenerator implements ShimGenerator {
const map = new Map<AbsoluteFsPath, string>();
files.filter(sourceFile => isNonDeclarationTsPath(sourceFile))
.forEach(
sourceFile => map.set(
AbsoluteFsPath.fromUnchecked(sourceFile.replace(/\.ts$/, '.ngfactory.ts')),
sourceFile));
sourceFile =>
map.set(absoluteFrom(sourceFile.replace(/\.ts$/, '.ngfactory.ts')), sourceFile));
return new FactoryGenerator(map);
}
}

View File

@ -5,9 +5,8 @@
* 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 {AbsoluteFsPath} from '../../path/src/types';
import {AbsoluteFsPath, absoluteFrom, resolve} from '../../file_system';
export interface ShimGenerator {
/**
@ -73,7 +72,7 @@ export class GeneratedShimsHostWrapper implements ts.CompilerHost {
for (let i = 0; i < this.shimGenerators.length; i++) {
const generator = this.shimGenerators[i];
// TypeScript internal paths are guaranteed to be POSIX-like absolute file paths.
const absoluteFsPath = AbsoluteFsPath.fromUnchecked(fileName);
const absoluteFsPath = resolve(fileName);
if (generator.recognize(absoluteFsPath)) {
const readFile = (originalFile: string) => {
return this.delegate.getSourceFile(
@ -118,7 +117,7 @@ export class GeneratedShimsHostWrapper implements ts.CompilerHost {
// Note that we can pass the file name as branded absolute fs path because TypeScript
// internally only passes POSIX-like paths.
return this.delegate.fileExists(fileName) ||
this.shimGenerators.some(gen => gen.recognize(AbsoluteFsPath.fromUnchecked(fileName)));
this.shimGenerators.some(gen => gen.recognize(absoluteFrom(fileName)));
}
readFile(fileName: string): string|undefined { return this.delegate.readFile(fileName); }

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../path/src/types';
import {AbsoluteFsPath, absoluteFrom} from '../../file_system';
import {isNonDeclarationTsPath} from '../../util/src/typescript';
import {ShimGenerator} from './host';
@ -81,9 +81,8 @@ export class SummaryGenerator implements ShimGenerator {
const map = new Map<AbsoluteFsPath, string>();
files.filter(sourceFile => isNonDeclarationTsPath(sourceFile))
.forEach(
sourceFile => map.set(
AbsoluteFsPath.fromUnchecked(sourceFile.replace(/\.ts$/, '.ngsummary.ts')),
sourceFile));
sourceFile =>
map.set(absoluteFrom(sourceFile.replace(/\.ts$/, '.ngsummary.ts')), sourceFile));
return new SummaryGenerator(map);
}
}

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../path';
import {AbsoluteFsPath} from '../../file_system';
import {ShimGenerator} from './host';

View File

@ -10,6 +10,7 @@ ts_library(
]),
deps = [
"//packages:types",
"//packages/compiler-cli/src/ngtsc/file_system",
"@npm//typescript",
],
)

View File

@ -1,157 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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
*/
///<reference types="jasmine"/>
import * as path from 'path';
import * as ts from 'typescript';
export function makeProgram(
files: {name: string, contents: string, isRoot?: boolean}[], options?: ts.CompilerOptions,
host: ts.CompilerHost = new InMemoryHost(), checkForErrors: boolean = true):
{program: ts.Program, host: ts.CompilerHost, options: ts.CompilerOptions} {
files.forEach(file => host.writeFile(file.name, file.contents, false, undefined, []));
const rootNames =
files.filter(file => file.isRoot !== false).map(file => host.getCanonicalFileName(file.name));
const compilerOptions = {
noLib: true,
experimentalDecorators: true,
moduleResolution: ts.ModuleResolutionKind.NodeJs, ...options
};
const program = ts.createProgram(rootNames, compilerOptions, host);
if (checkForErrors) {
const diags = [...program.getSyntacticDiagnostics(), ...program.getSemanticDiagnostics()];
if (diags.length > 0) {
const errors = diags.map(diagnostic => {
let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
if (diagnostic.file) {
const {line, character} =
diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start !);
message = `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`;
}
return `Error: ${message}`;
});
throw new Error(`Typescript diagnostics failed! ${errors.join(', ')}`);
}
}
return {program, host, options: compilerOptions};
}
export class InMemoryHost implements ts.CompilerHost {
private fileSystem = new Map<string, string>();
getSourceFile(
fileName: string, languageVersion: ts.ScriptTarget,
onError?: ((message: string) => void)|undefined,
shouldCreateNewSourceFile?: boolean|undefined): ts.SourceFile|undefined {
const contents = this.fileSystem.get(this.getCanonicalFileName(fileName));
if (contents === undefined) {
onError && onError(`File does not exist: ${this.getCanonicalFileName(fileName)})`);
return undefined;
}
return ts.createSourceFile(fileName, contents, languageVersion);
}
getDefaultLibFileName(options: ts.CompilerOptions): string { return '/lib.d.ts'; }
writeFile(
fileName: string, data: string, writeByteOrderMark?: boolean,
onError?: ((message: string) => void)|undefined,
sourceFiles?: ReadonlyArray<ts.SourceFile>): void {
this.fileSystem.set(this.getCanonicalFileName(fileName), data);
}
getCurrentDirectory(): string { return '/'; }
getDirectories(dir: string): string[] {
const fullDir = this.getCanonicalFileName(dir) + '/';
const dirSet = new Set(Array
// Look at all paths known to the host.
.from(this.fileSystem.keys())
// Filter out those that aren't under the requested directory.
.filter(candidate => candidate.startsWith(fullDir))
// Relativize the rest by the requested directory.
.map(candidate => candidate.substr(fullDir.length))
// What's left are dir/.../file.txt entries, and file.txt entries.
// Get the dirname, which
// yields '.' for the latter and dir/... for the former.
.map(candidate => path.dirname(candidate))
// Filter out the '.' entries, which were files.
.filter(candidate => candidate !== '.')
// Finally, split on / and grab the first entry.
.map(candidate => candidate.split('/', 1)[0]));
// Get the resulting values out of the Set.
return Array.from(dirSet);
}
getCanonicalFileName(fileName: string): string {
return path.posix.normalize(`${this.getCurrentDirectory()}/${fileName}`);
}
useCaseSensitiveFileNames(): boolean { return true; }
getNewLine(): string { return '\n'; }
fileExists(fileName: string): boolean { return this.fileSystem.has(fileName); }
readFile(fileName: string): string|undefined { return this.fileSystem.get(fileName); }
}
function bindingNameEquals(node: ts.BindingName, name: string): boolean {
if (ts.isIdentifier(node)) {
return node.text === name;
}
return false;
}
export function getDeclaration<T extends ts.Declaration>(
program: ts.Program, fileName: string, name: string, assert: (value: any) => value is T): T {
const sf = program.getSourceFile(fileName);
if (!sf) {
throw new Error(`No such file: ${fileName}`);
}
const chosenDecl = walkForDeclaration(sf);
if (chosenDecl === null) {
throw new Error(`No such symbol: ${name} in ${fileName}`);
}
if (!assert(chosenDecl)) {
throw new Error(`Symbol ${name} from ${fileName} is a ${ts.SyntaxKind[chosenDecl.kind]}`);
}
return chosenDecl;
// We walk the AST tree looking for a declaration that matches
function walkForDeclaration(rootNode: ts.Node): ts.Declaration|null {
let chosenDecl: ts.Declaration|null = null;
rootNode.forEachChild(node => {
if (chosenDecl !== null) {
return;
}
if (ts.isVariableStatement(node)) {
node.declarationList.declarations.forEach(decl => {
if (bindingNameEquals(decl.name, name)) {
chosenDecl = decl;
}
});
} else if (ts.isClassDeclaration(node) || ts.isFunctionDeclaration(node)) {
if (node.name !== undefined && node.name.text === name) {
chosenDecl = node;
}
} else if (
ts.isImportDeclaration(node) && node.importClause !== undefined &&
node.importClause.name !== undefined && node.importClause.name.text === name) {
chosenDecl = node.importClause;
} else {
chosenDecl = walkForDeclaration(node);
}
});
return chosenDecl;
}
}

View File

@ -5,6 +5,4 @@
* 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
*/
export {LogicalFileSystem, LogicalProjectPath} from './src/logical';
export {AbsoluteFsPath, PathSegment} from './src/types';
export {getDeclaration, makeProgram} from './src/utils';

View File

@ -0,0 +1,100 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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
*/
///<reference types="jasmine"/>
import * as ts from 'typescript';
import {AbsoluteFsPath, NgtscCompilerHost, dirname, getFileSystem, getSourceFileOrError} from '../../file_system';
export function makeProgram(
files: {name: AbsoluteFsPath, contents: string, isRoot?: boolean}[],
options?: ts.CompilerOptions, host?: ts.CompilerHost, checkForErrors: boolean = true):
{program: ts.Program, host: ts.CompilerHost, options: ts.CompilerOptions} {
const fs = getFileSystem();
files.forEach(file => {
fs.ensureDir(dirname(file.name));
fs.writeFile(file.name, file.contents);
});
const compilerOptions = {
noLib: true,
experimentalDecorators: true,
moduleResolution: ts.ModuleResolutionKind.NodeJs, ...options
};
const compilerHost = new NgtscCompilerHost(fs, compilerOptions);
const rootNames = files.filter(file => file.isRoot !== false)
.map(file => compilerHost.getCanonicalFileName(file.name));
const program = ts.createProgram(rootNames, compilerOptions, compilerHost);
if (checkForErrors) {
const diags = [...program.getSyntacticDiagnostics(), ...program.getSemanticDiagnostics()];
if (diags.length > 0) {
const errors = diags.map(diagnostic => {
let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
if (diagnostic.file) {
const {line, character} =
diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start !);
message = `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`;
}
return `Error: ${message}`;
});
throw new Error(`Typescript diagnostics failed! ${errors.join(', ')}`);
}
}
return {program, host: compilerHost, options: compilerOptions};
}
export function getDeclaration<T extends ts.Declaration>(
program: ts.Program, fileName: AbsoluteFsPath, name: string,
assert: (value: any) => value is T): T {
const sf = getSourceFileOrError(program, fileName);
const chosenDecl = walkForDeclaration(sf);
if (chosenDecl === null) {
throw new Error(`No such symbol: ${name} in ${fileName}`);
}
if (!assert(chosenDecl)) {
throw new Error(`Symbol ${name} from ${fileName} is a ${ts.SyntaxKind[chosenDecl.kind]}`);
}
return chosenDecl;
// We walk the AST tree looking for a declaration that matches
function walkForDeclaration(rootNode: ts.Node): ts.Declaration|null {
let chosenDecl: ts.Declaration|null = null;
rootNode.forEachChild(node => {
if (chosenDecl !== null) {
return;
}
if (ts.isVariableStatement(node)) {
node.declarationList.declarations.forEach(decl => {
if (bindingNameEquals(decl.name, name)) {
chosenDecl = decl;
}
});
} else if (ts.isClassDeclaration(node) || ts.isFunctionDeclaration(node)) {
if (node.name !== undefined && node.name.text === name) {
chosenDecl = node;
}
} else if (
ts.isImportDeclaration(node) && node.importClause !== undefined &&
node.importClause.name !== undefined && node.importClause.name.text === name) {
chosenDecl = node.importClause;
} else {
chosenDecl = walkForDeclaration(node);
}
});
return chosenDecl;
}
}
function bindingNameEquals(node: ts.BindingName, name: string): boolean {
if (ts.isIdentifier(node)) {
return node.text === name;
}
return false;
}

View File

@ -8,9 +8,9 @@ ts_library(
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/translator",
"//packages/compiler-cli/src/ngtsc/util",

View File

@ -9,8 +9,8 @@
import {BoundTarget} from '@angular/compiler';
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../file_system';
import {NoopImportRewriter, Reference, ReferenceEmitter} from '../../imports';
import {AbsoluteFsPath} from '../../path';
import {ClassDeclaration} from '../../reflection';
import {ImportManager} from '../../translator';

View File

@ -5,13 +5,10 @@
* 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
*/
/// <reference types="node" />
import * as path from 'path';
import * as ts from 'typescript';
import {AbsoluteFsPath, join} from '../../file_system';
import {NoopImportRewriter, Reference, ReferenceEmitter} from '../../imports';
import {AbsoluteFsPath} from '../../path';
import {ClassDeclaration} from '../../reflection';
import {ImportManager} from '../../translator';
@ -71,5 +68,5 @@ export class TypeCheckFile extends Environment {
export function typeCheckFilePath(rootDirs: AbsoluteFsPath[]): AbsoluteFsPath {
const shortest = rootDirs.concat([]).sort((a, b) => a.length - b.length)[0];
return AbsoluteFsPath.fromUnchecked(path.posix.join(shortest, '__ng_typecheck__.ts'));
return join(shortest, '__ng_typecheck__.ts');
}

View File

@ -11,11 +11,11 @@ ts_library(
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/testing",
"//packages/compiler-cli/src/ngtsc/translator",
"//packages/compiler-cli/src/ngtsc/typecheck",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//typescript",

View File

@ -5,25 +5,15 @@
* 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 {LogicalFileSystem, absoluteFrom, getSourceFileOrError} from '../../file_system';
import {TestFile, runInEachFileSystem} from '../../file_system/testing';
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, Reference, ReferenceEmitter} from '../../imports';
import {AbsoluteFsPath, LogicalFileSystem} from '../../path';
import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {getDeclaration, makeProgram} from '../../testing';
import {getRootDirs} from '../../util/src/typescript';
import {TypeCheckingConfig} from '../src/api';
import {TypeCheckContext} from '../src/context';
import {TypeCheckProgramHost} from '../src/host';
const LIB_D_TS = {
name: 'lib.d.ts',
contents: `
type Partial<T> = { [P in keyof T]?: T[P]; };
type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
type NonNullable<T> = T extends null | undefined ? never : T;`
};
const ALL_ENABLED_CONFIG: TypeCheckingConfig = {
applyTemplateContextGuards: true,
@ -34,82 +24,100 @@ const ALL_ENABLED_CONFIG: TypeCheckingConfig = {
strictSafeNavigationTypes: true,
};
describe('ngtsc typechecking', () => {
describe('ctors', () => {
it('compiles a basic type constructor', () => {
const files = [
LIB_D_TS, {
name: 'main.ts',
contents: `
runInEachFileSystem(() => {
describe('ngtsc typechecking', () => {
let _: typeof absoluteFrom;
let LIB_D_TS: TestFile;
beforeEach(() => {
_ = absoluteFrom;
LIB_D_TS = {
name: _('/lib.d.ts'),
contents: `
type Partial<T> = { [P in keyof T]?: T[P]; };
type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
type NonNullable<T> = T extends null | undefined ? never : T;`
};
});
describe('ctors', () => {
it('compiles a basic type constructor', () => {
const files: TestFile[] = [
LIB_D_TS, {
name: _('/main.ts'),
contents: `
class TestClass<T extends string> {
value: T;
}
TestClass.ngTypeCtor({value: 'test'});
`
}
];
const {program, host, options} = makeProgram(files, undefined, undefined, false);
const checker = program.getTypeChecker();
const logicalFs = new LogicalFileSystem(getRootDirs(host, options));
const emitter = new ReferenceEmitter([
new LocalIdentifierStrategy(),
new AbsoluteModuleStrategy(
program, checker, options, host, new TypeScriptReflectionHost(checker)),
new LogicalProjectStrategy(checker, logicalFs),
]);
const ctx = new TypeCheckContext(
ALL_ENABLED_CONFIG, emitter, AbsoluteFsPath.fromUnchecked('/_typecheck_.ts'));
const TestClass = getDeclaration(program, 'main.ts', 'TestClass', isNamedClassDeclaration);
ctx.addInlineTypeCtor(program.getSourceFile('main.ts') !, new Reference(TestClass), {
fnName: 'ngTypeCtor',
body: true,
fields: {
inputs: ['value'],
outputs: [],
queries: [],
},
}
];
const {program, host, options} = makeProgram(files, undefined, undefined, false);
const checker = program.getTypeChecker();
const logicalFs = new LogicalFileSystem(getRootDirs(host, options));
const emitter = new ReferenceEmitter([
new LocalIdentifierStrategy(),
new AbsoluteModuleStrategy(
program, checker, options, host, new TypeScriptReflectionHost(checker)),
new LogicalProjectStrategy(checker, logicalFs),
]);
const ctx = new TypeCheckContext(ALL_ENABLED_CONFIG, emitter, _('/_typecheck_.ts'));
const TestClass =
getDeclaration(program, _('/main.ts'), 'TestClass', isNamedClassDeclaration);
ctx.addInlineTypeCtor(
getSourceFileOrError(program, _('/main.ts')), new Reference(TestClass), {
fnName: 'ngTypeCtor',
body: true,
fields: {
inputs: ['value'],
outputs: [],
queries: [],
},
});
ctx.calculateTemplateDiagnostics(program, host, options);
});
ctx.calculateTemplateDiagnostics(program, host, options);
});
it('should not consider query fields', () => {
const files = [
LIB_D_TS, {
name: 'main.ts',
contents: `class TestClass { value: any; }`,
}
];
const {program, host, options} = makeProgram(files, undefined, undefined, false);
const checker = program.getTypeChecker();
const logicalFs = new LogicalFileSystem(getRootDirs(host, options));
const emitter = new ReferenceEmitter([
new LocalIdentifierStrategy(),
new AbsoluteModuleStrategy(
program, checker, options, host, new TypeScriptReflectionHost(checker)),
new LogicalProjectStrategy(checker, logicalFs),
]);
const ctx = new TypeCheckContext(
ALL_ENABLED_CONFIG, emitter, AbsoluteFsPath.fromUnchecked('/_typecheck_.ts'));
const TestClass = getDeclaration(program, 'main.ts', 'TestClass', isNamedClassDeclaration);
ctx.addInlineTypeCtor(program.getSourceFile('main.ts') !, new Reference(TestClass), {
fnName: 'ngTypeCtor',
body: true,
fields: {
inputs: ['value'],
outputs: [],
queries: ['queryField'],
},
it('should not consider query fields', () => {
const files: TestFile[] = [
LIB_D_TS, {
name: _('/main.ts'),
contents: `class TestClass { value: any; }`,
}
];
const {program, host, options} = makeProgram(files, undefined, undefined, false);
const checker = program.getTypeChecker();
const logicalFs = new LogicalFileSystem(getRootDirs(host, options));
const emitter = new ReferenceEmitter([
new LocalIdentifierStrategy(),
new AbsoluteModuleStrategy(
program, checker, options, host, new TypeScriptReflectionHost(checker)),
new LogicalProjectStrategy(checker, logicalFs),
]);
const ctx = new TypeCheckContext(ALL_ENABLED_CONFIG, emitter, _('/_typecheck_.ts'));
const TestClass =
getDeclaration(program, _('/main.ts'), 'TestClass', isNamedClassDeclaration);
ctx.addInlineTypeCtor(
getSourceFileOrError(program, _('/main.ts')), new Reference(TestClass), {
fnName: 'ngTypeCtor',
body: true,
fields: {
inputs: ['value'],
outputs: [],
queries: ['queryField'],
},
});
const res = ctx.calculateTemplateDiagnostics(program, host, options);
const TestClassWithCtor =
getDeclaration(res.program, _('/main.ts'), 'TestClass', isNamedClassDeclaration);
const typeCtor = TestClassWithCtor.members.find(isTypeCtor) !;
expect(typeCtor.getText()).not.toContain('queryField');
});
const res = ctx.calculateTemplateDiagnostics(program, host, options);
const TestClassWithCtor =
getDeclaration(res.program, 'main.ts', 'TestClass', isNamedClassDeclaration);
const typeCtor = TestClassWithCtor.members.find(isTypeCtor) !;
expect(typeCtor.getText()).not.toContain('queryField');
});
});
});
function isTypeCtor(el: ts.ClassElement): el is ts.MethodDeclaration {
return ts.isMethodDeclaration(el) && ts.isIdentifier(el.name) && el.name.text === 'ngTypeCtor';
}
function isTypeCtor(el: ts.ClassElement): el is ts.MethodDeclaration {
return ts.isMethodDeclaration(el) && ts.isIdentifier(el.name) && el.name.text === 'ngTypeCtor';
}
});

View File

@ -9,7 +9,7 @@ ts_library(
]),
deps = [
"//packages:types",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/file_system",
"@npm//@types/node",
"@npm//typescript",
],

View File

@ -5,26 +5,23 @@
* 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
*/
/// <reference types="node" />
import * as path from 'path';
import {dirname, relative, resolve} from '../../file_system';
const TS_DTS_JS_EXTENSION = /(?:\.d)?\.ts$|\.js$/;
export function relativePathBetween(from: string, to: string): string|null {
let relative = path.posix.relative(path.dirname(from), to).replace(TS_DTS_JS_EXTENSION, '');
let relativePath = relative(dirname(resolve(from)), resolve(to)).replace(TS_DTS_JS_EXTENSION, '');
if (relative === '') {
if (relativePath === '') {
return null;
}
// path.relative() does not include the leading './'.
if (!relative.startsWith('.')) {
relative = `./${relative}`;
if (!relativePath.startsWith('.')) {
relativePath = `./${relativePath}`;
}
return relative;
return relativePath;
}
export function normalizeSeparators(path: string): string {

View File

@ -10,7 +10,7 @@ const TS = /\.tsx?$/i;
const D_TS = /\.d\.ts$/i;
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../path';
import {AbsoluteFsPath, absoluteFrom} from '../../file_system';
export function isDtsPath(filePath: string): boolean {
return D_TS.test(filePath);
@ -47,6 +47,12 @@ export function getSourceFile(node: ts.Node): ts.SourceFile {
return directSf !== undefined ? directSf : ts.getOriginalNode(node).getSourceFile();
}
export function getSourceFileOrNull(program: ts.Program, fileName: AbsoluteFsPath): ts.SourceFile|
null {
return program.getSourceFile(fileName) || null;
}
export function identifierOfNode(decl: ts.Node & {name?: ts.Node}): ts.Identifier|null {
if (decl.name !== undefined && ts.isIdentifier(decl.name)) {
return decl.name;
@ -83,7 +89,7 @@ export function getRootDirs(host: ts.CompilerHost, options: ts.CompilerOptions):
// See:
// https://github.com/Microsoft/TypeScript/blob/3f7357d37f66c842d70d835bc925ec2a873ecfec/src/compiler/sys.ts#L650
// Also compiler options might be set via an API which doesn't normalize paths
return rootDirs.map(rootDir => AbsoluteFsPath.from(rootDir));
return rootDirs.map(rootDir => absoluteFrom(rootDir));
}
export function nodeDebugInfo(node: ts.Node): string {

View File

@ -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/testing",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//typescript",

View File

@ -5,10 +5,10 @@
* 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 {makeProgram} from '../../testing/in_memory_typescript';
import {absoluteFrom, getSourceFileOrError} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {makeProgram} from '../../testing';
import {VisitListEntryResult, Visitor, visit} from '../src/visitor';
class TestAstVisitor extends Visitor {
@ -43,37 +43,41 @@ function testTransformerFactory(context: ts.TransformationContext): ts.Transform
return (file: ts.SourceFile) => visit(file, new TestAstVisitor(), context);
}
describe('AST Visitor', () => {
it('should add a statement before class in plain file', () => {
const {program, host} =
makeProgram([{name: 'main.ts', contents: `class A { static id = 3; }`}]);
const sf = program.getSourceFile('main.ts') !;
program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]});
const main = host.readFile('/main.js');
expect(main).toMatch(/^var A_id = 3;/);
});
runInEachFileSystem(() => {
describe('AST Visitor', () => {
let _: typeof absoluteFrom;
beforeEach(() => _ = absoluteFrom);
it('should add a statement before class inside function definition', () => {
const {program, host} = makeProgram([{
name: 'main.ts',
contents: `
it('should add a statement before class in plain file', () => {
const {program, host} =
makeProgram([{name: _('/main.ts'), contents: `class A { static id = 3; }`}]);
const sf = getSourceFileOrError(program, _('/main.ts'));
program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]});
const main = host.readFile('/main.js');
expect(main).toMatch(/^var A_id = 3;/);
});
it('should add a statement before class inside function definition', () => {
const {program, host} = makeProgram([{
name: _('/main.ts'),
contents: `
export function foo() {
var x = 3;
class A { static id = 2; }
return A;
}
`
}]);
const sf = program.getSourceFile('main.ts') !;
program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]});
const main = host.readFile('/main.js');
expect(main).toMatch(/var x = 3;\s+var A_id = 2;\s+var A =/);
});
}]);
const sf = getSourceFileOrError(program, _('/main.ts'));
program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]});
const main = host.readFile(_('/main.js'));
expect(main).toMatch(/var x = 3;\s+var A_id = 2;\s+var A =/);
});
it('handles nested statements', () => {
const {program, host} = makeProgram([{
name: 'main.ts',
contents: `
it('handles nested statements', () => {
const {program, host} = makeProgram([{
name: _('/main.ts'),
contents: `
export class A {
static id = 3;
@ -84,11 +88,12 @@ describe('AST Visitor', () => {
return B;
}
}`
}]);
const sf = program.getSourceFile('main.ts') !;
program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]});
const main = host.readFile('/main.js');
expect(main).toMatch(/var A_id = 3;\s+var A = /);
expect(main).toMatch(/var B_id = 4;\s+var B = /);
}]);
const sf = getSourceFileOrError(program, _('/main.ts'));
program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]});
const main = host.readFile(_('/main.js'));
expect(main).toMatch(/var A_id = 3;\s+var A = /);
expect(main).toMatch(/var B_id = 4;\s+var B = /);
});
});
});

View File

@ -6,17 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Position, isSyntaxError, syntaxError} from '@angular/compiler';
import * as fs from 'fs';
import * as path from 'path';
import {Position, isSyntaxError} from '@angular/compiler';
import * as ts from 'typescript';
import {AbsoluteFsPath, absoluteFrom, getFileSystem, relative, resolve} from '../src/ngtsc/file_system';
import * as api from './transformers/api';
import * as ng from './transformers/entry_points';
import {createMessageDiagnostic} from './transformers/util';
const TS_EXT = /\.ts$/;
export type Diagnostics = ReadonlyArray<ts.Diagnostic|api.Diagnostic>;
export function filterErrorsAndWarnings(diagnostics: Diagnostics): Diagnostics {
@ -30,7 +26,8 @@ const defaultFormatHost: ts.FormatDiagnosticsHost = {
};
function displayFileName(fileName: string, host: ts.FormatDiagnosticsHost): string {
return path.relative(host.getCurrentDirectory(), host.getCanonicalFileName(fileName));
return relative(
resolve(host.getCurrentDirectory()), resolve(host.getCanonicalFileName(fileName)));
}
export function formatDiagnosticPosition(
@ -110,11 +107,13 @@ export interface ParsedConfiguration {
}
export function calcProjectFileAndBasePath(project: string):
{projectFile: string, basePath: string} {
const projectIsDir = fs.lstatSync(project).isDirectory();
const projectFile = projectIsDir ? path.join(project, 'tsconfig.json') : project;
const projectDir = projectIsDir ? project : path.dirname(project);
const basePath = path.resolve(process.cwd(), projectDir);
{projectFile: AbsoluteFsPath, basePath: AbsoluteFsPath} {
const fs = getFileSystem();
const absProject = fs.resolve(project);
const projectIsDir = fs.lstat(absProject).isDirectory();
const projectFile = projectIsDir ? fs.join(absProject, 'tsconfig.json') : absProject;
const projectDir = projectIsDir ? absProject : fs.dirname(absProject);
const basePath = fs.resolve(projectDir);
return {projectFile, basePath};
}
@ -130,6 +129,7 @@ export function createNgCompilerOptions(
export function readConfiguration(
project: string, existingOptions?: ts.CompilerOptions): ParsedConfiguration {
try {
const fs = getFileSystem();
const {projectFile, basePath} = calcProjectFileAndBasePath(project);
const readExtendedConfigFile =
@ -149,11 +149,12 @@ export function readConfiguration(
}
if (config.extends) {
let extendedConfigPath = path.resolve(path.dirname(configFile), config.extends);
extendedConfigPath = path.extname(extendedConfigPath) ? extendedConfigPath :
`${extendedConfigPath}.json`;
let extendedConfigPath = fs.resolve(fs.dirname(configFile), config.extends);
extendedConfigPath = fs.extname(extendedConfigPath) ?
extendedConfigPath :
absoluteFrom(`${extendedConfigPath}.json`);
if (fs.existsSync(extendedConfigPath)) {
if (fs.exists(extendedConfigPath)) {
// Call read config recursively as TypeScript only merges CompilerOptions
return readExtendedConfigFile(extendedConfigPath, baseConfig);
}
@ -175,14 +176,14 @@ export function readConfiguration(
}
const parseConfigHost = {
useCaseSensitiveFileNames: true,
fileExists: fs.existsSync,
fileExists: fs.exists.bind(fs),
readDirectory: ts.sys.readDirectory,
readFile: ts.sys.readFile
};
const configFileName = path.resolve(process.cwd(), projectFile);
const configFileName = fs.resolve(fs.pwd(), projectFile);
const parsed = ts.parseJsonConfigFileContent(
config, parseConfigHost, basePath, existingOptions, configFileName);
const rootNames = parsed.fileNames.map(f => path.normalize(f));
const rootNames = parsed.fileNames;
const options = createNgCompilerOptions(basePath, config, parsed.options);
let emitFlags = api.EmitFlags.Default;

View File

@ -6,12 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AotCompilerHost, EmitterVisitorContext, ExternalReference, GeneratedFile, ParseSourceSpan, TypeScriptEmitter, collectExternalReferences, syntaxError} from '@angular/compiler';
import {AotCompilerHost, EmitterVisitorContext, GeneratedFile, ParseSourceSpan, TypeScriptEmitter, collectExternalReferences, syntaxError} from '@angular/compiler';
import * as path from 'path';
import * as ts from 'typescript';
import {TypeCheckHost} from '../diagnostics/translate_diagnostics';
import {METADATA_VERSION, ModuleMetadata} from '../metadata/index';
import {ModuleMetadata} from '../metadata/index';
import {join} from '../ngtsc/file_system';
import {CompilerHost, CompilerOptions, LibrarySummary} from './api';
import {MetadataReaderHost, createMetadataReaderCache, readMetadata} from './metadata_reader';
@ -253,7 +254,7 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter implements ts.CompilerHos
const modulePath = importedFile.substring(0, importedFile.length - moduleName.length) +
importedFilePackageName;
const packageJson = require(modulePath + '/package.json');
const packageTypings = path.posix.join(modulePath, packageJson.typings);
const packageTypings = join(modulePath, packageJson.typings);
if (packageTypings === originalImportedFile) {
moduleName = importedFilePackageName;
}