Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ngcc - use inner/outer class names as appropriate #33533

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/compiler-cli/ngcc/src/host/esm5_host.ts
Expand Up @@ -86,6 +86,23 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
return iife.parent.arguments[0];
}

getInternalNameOfClass(clazz: ClassDeclaration): ts.Identifier {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this method be called with a TS ClassDeclaration (e.g. from a .d.ts file)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was debating this with myself. I left it as throwing and ran all the ngcc tests and it never threw.

The fact that this never returns null means that we can't try calling the super first. So we possibly could try calling it if the declaration is not in ES5 format.

I think the point is (at least as things stand right now) that ngtsc only ever calls this method when it is working on the source, not typings.

So at the moment I feel confident leaving it as it is.

const innerClass = this.getInnerFunctionDeclarationFromClassDeclaration(clazz);
if (innerClass === undefined) {
throw new Error(
`getInternalNameOfClass() called on a non-ES5 class: expected ${clazz.name.text} to have an inner class declaration`);
}
if (innerClass.name === undefined) {
throw new Error(
`getInternalNameOfClass() called on a class with an anonymous inner declaration: expected a name on:\n${innerClass.getText()}`);
}
return innerClass.name;
}

getAdjacentNameOfClass(clazz: ClassDeclaration): ts.Identifier {
return this.getInternalNameOfClass(clazz);
}

/**
* In ES5, the implementation of a class is a function expression that is hidden inside an IIFE,
* whose value is assigned to a variable (which represents the class to the rest of the program).
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-cli/ngcc/src/packages/transformer.ts
Expand Up @@ -83,7 +83,7 @@ export class Transformer {
// Transform the source files and source maps.
const srcFormatter = this.getRenderingFormatter(reflectionHost, bundle);

const renderer = new Renderer(srcFormatter, this.fs, this.logger, bundle);
const renderer = new Renderer(reflectionHost, srcFormatter, this.fs, this.logger, bundle);
let renderedFiles = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);

Expand Down
Expand Up @@ -96,7 +96,7 @@ export class EsmRenderingFormatter implements RenderingFormatter {
if (!classSymbol) {
throw new Error(`Compiled class does not have a valid symbol: ${compiledClass.name}`);
}
const insertionPoint = classSymbol.declaration.valueDeclaration !.getEnd();
const insertionPoint = classSymbol.declaration.valueDeclaration.getEnd();
output.appendLeft(insertionPoint, '\n' + definitions);
}

Expand Down
56 changes: 29 additions & 27 deletions packages/compiler-cli/ngcc/src/rendering/renderer.ts
Expand Up @@ -20,6 +20,7 @@ import {Logger} from '../logging/logger';
import {FileToWrite, getImportRewriter, stripExtension} from './utils';
import {RenderingFormatter, RedundantDecoratorMap} from './rendering_formatter';
import {extractSourceMap, renderSourceAndMap} from './source_maps';
import {NgccReflectionHost} from '../host/ngcc_host';

/**
* A base-class for rendering an `AnalyzedFile`.
Expand All @@ -29,8 +30,8 @@ import {extractSourceMap, renderSourceAndMap} from './source_maps';
*/
export class Renderer {
constructor(
private srcFormatter: RenderingFormatter, private fs: FileSystem, private logger: Logger,
private bundle: EntryPointBundle) {}
private host: NgccReflectionHost, private srcFormatter: RenderingFormatter,
private fs: FileSystem, private logger: Logger, private bundle: EntryPointBundle) {}

renderProgram(
decorationAnalyses: DecorationAnalyses, switchMarkerAnalyses: SwitchMarkerAnalyses,
Expand Down Expand Up @@ -81,7 +82,8 @@ export class Renderer {
this.srcFormatter.removeDecorators(outputText, decoratorsToRemove);

compiledFile.compiledClasses.forEach(clazz => {
const renderedDefinition = renderDefinitions(compiledFile.sourceFile, clazz, importManager);
const renderedDefinition =
this.renderDefinitions(compiledFile.sourceFile, clazz, importManager);
this.srcFormatter.addDefinitions(outputText, clazz, renderedDefinition);

if (!isEntryPoint && clazz.reexports.length > 0) {
Expand Down Expand Up @@ -143,6 +145,30 @@ export class Renderer {
});
return decoratorsToRemove;
}

/**
* Render the definitions as source code for the given class.
* @param sourceFile The file containing the class to process.
* @param clazz The class whose definitions are to be rendered.
* @param compilation The results of analyzing the class - this is used to generate the rendered
* definitions.
* @param imports An object that tracks the imports that are needed by the rendered definitions.
*/
private renderDefinitions(
sourceFile: ts.SourceFile, compiledClass: CompiledClass, imports: ImportManager): string {
const printer = createPrinter();
const name = this.host.getInternalNameOfClass(compiledClass.declaration);
const translate = (stmt: Statement) =>
translateStatement(stmt, imports, NOOP_DEFAULT_IMPORT_RECORDER);
const print = (stmt: Statement) =>
printer.printNode(ts.EmitHint.Unspecified, translate(stmt), sourceFile);
const statements: Statement[] = compiledClass.compilation.map(
c => { return createAssignmentStatement(name, c.name, c.initializer); });
for (const c of compiledClass.compilation) {
statements.push(...c.statements);
}
return statements.map(print).join('\n');
}
}

/**
Expand All @@ -157,30 +183,6 @@ export function renderConstantPool(
.join('\n');
}

/**
* Render the definitions as source code for the given class.
* @param sourceFile The file containing the class to process.
* @param clazz The class whose definitions are to be rendered.
* @param compilation The results of analyzing the class - this is used to generate the rendered
* definitions.
* @param imports An object that tracks the imports that are needed by the rendered definitions.
*/
export function renderDefinitions(
sourceFile: ts.SourceFile, compiledClass: CompiledClass, imports: ImportManager): string {
const printer = createPrinter();
const name = compiledClass.declaration.name;
const translate = (stmt: Statement) =>
translateStatement(stmt, imports, NOOP_DEFAULT_IMPORT_RECORDER);
const print = (stmt: Statement) =>
printer.printNode(ts.EmitHint.Unspecified, translate(stmt), sourceFile);
const statements: Statement[] =
compiledClass.compilation.map(c => createAssignmentStatement(name, c.name, c.initializer));
for (const c of compiledClass.compilation) {
statements.push(...c.statements);
}
return statements.map(print).join('\n');
}

/**
* Create an Angular AST statement node that contains the assignment of the
* compiled decorator to be applied to the class.
Expand Down
66 changes: 66 additions & 0 deletions packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts
Expand Up @@ -147,6 +147,24 @@ var NoDecoratorConstructorClass = (function() {
}
return NoDecoratorConstructorClass;
}());
var OuterClass1 = (function() {
function InnerClass1() {
}
return InnerClass1;
}());
var OuterClass2 = (function() {
function InnerClass2() {
}
InnerClass2_1 = InnerClass12
var InnerClass2_1;
return InnerClass2;
}());
var SuperClass = (function() { function SuperClass() {} return SuperClass; }());
var ChildClass = /** @class */ (function (_super) {
__extends(ChildClass, _super);
function InnerChildClass() {}
return InnerChildClass;
}(SuperClass);
exports.EmptyClass = EmptyClass;
exports.NoDecoratorConstructorClass = NoDecoratorConstructorClass;
`,
Expand Down Expand Up @@ -2212,6 +2230,54 @@ exports.ExternalModule = ExternalModule;
});
});

describe('getInternalNameOfClass()', () => {
it('should return the name of the inner class declaration', () => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);

const emptyClass = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
expect(host.getInternalNameOfClass(emptyClass).text).toEqual('EmptyClass');

const class1 = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'OuterClass1', isNamedVariableDeclaration);
expect(host.getInternalNameOfClass(class1).text).toEqual('InnerClass1');

const class2 = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'OuterClass2', isNamedVariableDeclaration);
expect(host.getInternalNameOfClass(class2).text).toEqual('InnerClass2');

const childClass = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'ChildClass', isNamedVariableDeclaration);
expect(host.getInternalNameOfClass(childClass).text).toEqual('InnerChildClass');
});
});

describe('getAdjacentNameOfClass()', () => {
it('should return the name of the inner class declaration', () => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);

const emptyClass = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
expect(host.getAdjacentNameOfClass(emptyClass).text).toEqual('EmptyClass');

const class1 = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'OuterClass1', isNamedVariableDeclaration);
expect(host.getAdjacentNameOfClass(class1).text).toEqual('InnerClass1');

const class2 = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'OuterClass2', isNamedVariableDeclaration);
expect(host.getAdjacentNameOfClass(class2).text).toEqual('InnerClass2');

const childClass = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'ChildClass', isNamedVariableDeclaration);
expect(host.getAdjacentNameOfClass(childClass).text).toEqual('InnerChildClass');
});
});

describe('getModuleWithProvidersFunctions', () => {
it('should find every exported function that returns an object that looks like a ModuleWithProviders object',
() => {
Expand Down
22 changes: 22 additions & 0 deletions packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts
Expand Up @@ -2088,6 +2088,28 @@ runInEachFileSystem(() => {
});
});

describe('getInternalNameOfClass()', () => {
it('should return the name of the class (there is no separate inner class in ES2015)', () => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const node =
getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration);
expect(host.getInternalNameOfClass(node).text).toEqual('EmptyClass');
});
});

describe('getAdjacentNameOfClass()', () => {
it('should return the name of the class (there is no separate inner class in ES2015)', () => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const node =
getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration);
expect(host.getAdjacentNameOfClass(node).text).toEqual('EmptyClass');
});
});

describe('getModuleWithProvidersFunctions()', () => {
it('should find every exported function that returns an object that looks like a ModuleWithProviders object',
() => {
Expand Down
66 changes: 66 additions & 0 deletions packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts
Expand Up @@ -192,6 +192,24 @@ runInEachFileSystem(() => {
}
return NoDecoratorConstructorClass;
}());
var OuterClass1 = (function() {
function InnerClass1() {
}
return InnerClass1;
}());
var OuterClass2 = (function() {
function InnerClass2() {
}
InnerClass2_1 = InnerClass12
var InnerClass2_1;
return InnerClass2;
}());
var SuperClass = (function() { function SuperClass() {} return SuperClass; }());
var ChildClass = /** @class */ (function (_super) {
__extends(ChildClass, _super);
function InnerChildClass() {}
return InnerChildClass;
}(SuperClass);
`,
};

Expand Down Expand Up @@ -2331,6 +2349,54 @@ runInEachFileSystem(() => {
});
});

describe('getInternalNameOfClass()', () => {
it('should return the name of the inner class declaration', () => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());

const emptyClass = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
expect(host.getInternalNameOfClass(emptyClass).text).toEqual('EmptyClass');

const class1 = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'OuterClass1', isNamedVariableDeclaration);
expect(host.getInternalNameOfClass(class1).text).toEqual('InnerClass1');

const class2 = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'OuterClass2', isNamedVariableDeclaration);
expect(host.getInternalNameOfClass(class2).text).toEqual('InnerClass2');

const childClass = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'ChildClass', isNamedVariableDeclaration);
expect(host.getInternalNameOfClass(childClass).text).toEqual('InnerChildClass');
});
});

describe('getAdjacentNameOfClass()', () => {
it('should return the name of the inner class declaration', () => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());

const emptyClass = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
expect(host.getAdjacentNameOfClass(emptyClass).text).toEqual('EmptyClass');

const class1 = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'OuterClass1', isNamedVariableDeclaration);
expect(host.getAdjacentNameOfClass(class1).text).toEqual('InnerClass1');

const class2 = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'OuterClass2', isNamedVariableDeclaration);
expect(host.getAdjacentNameOfClass(class2).text).toEqual('InnerClass2');

const childClass = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'ChildClass', isNamedVariableDeclaration);
expect(host.getAdjacentNameOfClass(childClass).text).toEqual('InnerChildClass');
});
});

describe('getModuleWithProvidersFunctions', () => {
it('should find every exported function that returns an object that looks like a ModuleWithProviders object',
() => {
Expand Down