Skip to content

Commit

Permalink
fix(ivy): correctly write cross-file references
Browse files Browse the repository at this point in the history
There is a bug in the existing handling for cross-file references.
Suppose there are two files, module.ts and component.ts.

component.ts declares two components, one of which uses the other.
In the Ivy model, this means the component will get a directives:
reference to the other in its defineComponent call.

That reference is generated by looking at the declared components
of the module (in module.ts). However, the way ngtsc tracks this
reference, it ends up comparing the identifier of the component
in module.ts with the component.ts file, detecting they're not in
the same file, and generating a relative import.

This commit changes ngtsc to track all identifiers of a reference,
including the one by which it is declared. This allows toExpression()
to correctly decide that a local reference is okay in component.ts.
  • Loading branch information
alxhub committed Jul 25, 2018
1 parent 87f72d7 commit 6d97ae0
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 40 deletions.
74 changes: 34 additions & 40 deletions packages/compiler-cli/src/ngtsc/metadata/src/resolver.ts
Expand Up @@ -106,7 +106,7 @@ export abstract class Reference<T extends ts.Node = ts.Node> {
*/
abstract toExpression(context: ts.SourceFile, importMode: ImportMode): Expression|null;

abstract withIdentifier(identifier: ts.Identifier): Reference;
abstract addIdentifier(identifier: ts.Identifier): void;
}

/**
Expand All @@ -120,7 +120,7 @@ export class NodeReference<T extends ts.Node = ts.Node> extends Reference<T> {

toExpression(context: ts.SourceFile): null { return null; }

withIdentifier(identifier: ts.Identifier): NodeReference<T> { return this; }
addIdentifier(identifier: ts.Identifier): void {}
}

/**
Expand All @@ -129,25 +129,18 @@ export class NodeReference<T extends ts.Node = ts.Node> extends Reference<T> {
* Imports generated by `ResolvedReference`s are always relative.
*/
export class ResolvedReference<T extends ts.Node = ts.Node> extends Reference<T> {
constructor(node: T, protected identifier: ts.Identifier) { super(node); }
protected identifiers: ts.Identifier[] = [];

constructor(node: T, protected primaryIdentifier: ts.Identifier) { super(node); }

readonly expressable = true;

toExpression(context: ts.SourceFile, importMode: ImportMode = ImportMode.USE_EXISTING_IMPORT):
Expression {
let compareCtx: ts.Node|null = null;
switch (importMode) {
case ImportMode.USE_EXISTING_IMPORT:
compareCtx = this.identifier;
break;
case ImportMode.FORCE_NEW_IMPORT:
compareCtx = this.node;
break;
default:
throw new Error(`Unsupported ImportMode: ${ImportMode[importMode]}`);
}
if (ts.getOriginalNode(context) === ts.getOriginalNode(compareCtx).getSourceFile()) {
return new WrappedNodeExpr(this.identifier);
const localIdentifier =
pickIdentifier(context, this.primaryIdentifier, this.identifiers, importMode);
if (localIdentifier !== null) {
return new WrappedNodeExpr(localIdentifier);
} else {
// Relative import from context -> this.node.getSourceFile().
// TODO(alxhub): investigate the impact of multiple source roots here.
Expand All @@ -165,16 +158,14 @@ export class ResolvedReference<T extends ts.Node = ts.Node> extends Reference<T>
// same.
if (relative === './') {
// Same file after all.
return new WrappedNodeExpr(this.identifier);
return new WrappedNodeExpr(this.primaryIdentifier);
} else {
return new ExternalExpr(new ExternalReference(relative, this.identifier.text));
return new ExternalExpr(new ExternalReference(relative, this.primaryIdentifier.text));
}
}
}

withIdentifier(identifier: ts.Identifier): ResolvedReference {
return new ResolvedReference(this.node, identifier);
}
addIdentifier(identifier: ts.Identifier): void { this.identifiers.push(identifier); }
}

/**
Expand All @@ -184,8 +175,9 @@ export class ResolvedReference<T extends ts.Node = ts.Node> extends Reference<T>
* the module specifier will be an absolute module name, not a relative path.
*/
export class AbsoluteReference extends Reference {
private identifiers: ts.Identifier[] = [];
constructor(
node: ts.Node, private identifier: ts.Identifier, readonly moduleName: string,
node: ts.Node, private primaryIdentifier: ts.Identifier, readonly moduleName: string,
readonly symbolName: string) {
super(node);
}
Expand All @@ -194,26 +186,29 @@ export class AbsoluteReference extends Reference {

toExpression(context: ts.SourceFile, importMode: ImportMode = ImportMode.USE_EXISTING_IMPORT):
Expression {
let compareCtx: ts.Node|null = null;
switch (importMode) {
case ImportMode.USE_EXISTING_IMPORT:
compareCtx = this.identifier;
break;
case ImportMode.FORCE_NEW_IMPORT:
compareCtx = this.node;
break;
default:
throw new Error(`Unsupported ImportMode: ${ImportMode[importMode]}`);
}
if (ts.getOriginalNode(context) === ts.getOriginalNode(compareCtx).getSourceFile()) {
return new WrappedNodeExpr(this.identifier);
const localIdentifier =
pickIdentifier(context, this.primaryIdentifier, this.identifiers, importMode);
if (localIdentifier !== null) {
return new WrappedNodeExpr(localIdentifier);
} else {
return new ExternalExpr(new ExternalReference(this.moduleName, this.symbolName));
}
}

withIdentifier(identifier: ts.Identifier): AbsoluteReference {
return new AbsoluteReference(this.node, identifier, this.moduleName, this.symbolName);
addIdentifier(identifier: ts.Identifier): void { this.identifiers.push(identifier); }
}

function pickIdentifier(
context: ts.SourceFile, primary: ts.Identifier, secondaries: ts.Identifier[],
mode: ImportMode): ts.Identifier|null {
context = ts.getOriginalNode(context) as ts.SourceFile;
let localIdentifier: ts.Identifier|null = null;
if (ts.getOriginalNode(primary).getSourceFile() === context) {
return primary;
} else if (mode === ImportMode.USE_EXISTING_IMPORT) {
return secondaries.find(id => ts.getOriginalNode(id) === context) || null;
} else {
return null;
}
}

Expand Down Expand Up @@ -426,10 +421,9 @@ class StaticInterpreter {
const result = this.visitDeclaration(
decl.node, {...context, absoluteModuleName: decl.viaModule || context.absoluteModuleName});
if (result instanceof Reference) {
return result.withIdentifier(node);
} else {
return result;
result.addIdentifier(node);
}
return result;
}

private visitDeclaration(node: ts.Declaration, context: Context): ResolvedValue {
Expand Down
34 changes: 34 additions & 0 deletions packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
Expand Up @@ -565,4 +565,38 @@ describe('ngtsc behavioral tests', () => {
expect(jsContents).toContain('i0.ɵd(dirIndex).onClick($event)');
expect(jsContents).toContain('i0.ɵd(dirIndex).onChange(i0.ɵd(dirIndex).arg)');
});

it('should correctly recognize local symbols', () => {
writeConfig();
write('module.ts', `
import {NgModule} from '@angular/core';
import {Dir, Comp} from './test';
@NgModule({
declarations: [Dir, Comp],
exports: [Dir, Comp],
})
class Module {}
`);
write(`test.ts`, `
import {Component, Directive} from '@angular/core';
@Directive({
selector: '[dir]',
})
export class Dir {}
@Component({
selector: 'test',
template: '<div dir>Test</div>',
})
export class Comp {}
`);

const exitCode = main(['-p', basePath], errorSpy);
expect(errorSpy).not.toHaveBeenCalled();
expect(exitCode).toBe(0);
const jsContents = getContents('test.js');
expect(jsContents).not.toMatch(/import \* as i[0-9] from ['"].\/test['"]/);
});
});

0 comments on commit 6d97ae0

Please sign in to comment.