Skip to content

Commit

Permalink
fix(ivy): wrap functions from "providers" in parentheses in Closure m…
Browse files Browse the repository at this point in the history
…ode (angular#33609)

Due to the fact that Tsickle runs between analyze and transform phases in Angular, Tsickle may transform nodes (add comments with type annotations for Closure) that we captured during the analyze phase. As a result, some patterns where a function is returned from another function may trigger automatic semicolon insertion, which breaks the code (makes functions return `undefined` instead of a function). In order to avoid the problem, this commit updates the code to wrap all functions in some expression ("privders" and "viewProviders") in parentheses. More info can be found in Tsickle source code here: https://github.com/angular/tsickle/blob/d7974262571c8a17d684e5ba07680e1b1993afdd/src/jsdoc_transformer.ts#L1021

PR Close angular#33609
  • Loading branch information
AndrewKushnir authored and alxhub committed Nov 20, 2019
1 parent 11a35e5 commit fc2f6b8
Show file tree
Hide file tree
Showing 11 changed files with 251 additions and 50 deletions.
Expand Up @@ -89,17 +89,18 @@ export class DecorationAnalyzer {
this.scopeRegistry, this.scopeRegistry, this.isCore, this.resourceManager, this.rootDirs,
/* defaultPreserveWhitespaces */ false,
/* i18nUseExternalIds */ true, /* i18nLegacyMessageIdFormat */ '', this.moduleResolver,
this.cycleAnalyzer, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER),
this.cycleAnalyzer, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER,
/* annotateForClosureCompiler */ false),
new DirectiveDecoratorHandler(
this.reflectionHost, this.evaluator, this.fullRegistry, NOOP_DEFAULT_IMPORT_RECORDER,
this.isCore),
this.isCore, /* annotateForClosureCompiler */ false),
new InjectableDecoratorHandler(
this.reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore,
/* strictCtorDeps */ false),
new NgModuleDecoratorHandler(
this.reflectionHost, this.evaluator, this.fullMetaReader, this.fullRegistry,
this.scopeRegistry, this.referencesRegistry, this.isCore, /* routeAnalyzer */ null,
this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER),
this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER, /* annotateForClosureCompiler */ false),
new PipeDecoratorHandler(
this.reflectionHost, this.evaluator, this.metaRegistry, NOOP_DEFAULT_IMPORT_RECORDER,
this.isCore),
Expand Down
14 changes: 10 additions & 4 deletions packages/compiler-cli/src/ngtsc/annotations/src/component.ts
Expand Up @@ -28,7 +28,7 @@ import {ResourceLoader} from './api';
import {extractDirectiveMetadata, parseFieldArrayValue} from './directive';
import {compileNgFactoryDefField} from './factory';
import {generateSetClassMetadataCall} from './metadata';
import {findAngularDecorator, isAngularCoreReference, isExpressionForwardReference, readBaseClass, unwrapExpression} from './util';
import {findAngularDecorator, isAngularCoreReference, isExpressionForwardReference, readBaseClass, unwrapExpression, wrapFunctionExpressionsInParens} from './util';

const EMPTY_MAP = new Map<string, Expression>();
const EMPTY_ARRAY: any[] = [];
Expand All @@ -55,6 +55,7 @@ export class ComponentDecoratorHandler implements
private i18nLegacyMessageIdFormat: string, private moduleResolver: ModuleResolver,
private cycleAnalyzer: CycleAnalyzer, private refEmitter: ReferenceEmitter,
private defaultImportRecorder: DefaultImportRecorder,
private annotateForClosureCompiler: boolean,
private resourceDependencies:
ResourceDependencyRecorder = new NoopResourceDependencyRecorder()) {}

Expand Down Expand Up @@ -146,7 +147,8 @@ export class ComponentDecoratorHandler implements
// on it.
const directiveResult = extractDirectiveMetadata(
node, decorator, this.reflector, this.evaluator, this.defaultImportRecorder, this.isCore,
flags, this.elementSchemaRegistry.getDefaultComponentElementName());
flags, this.annotateForClosureCompiler,
this.elementSchemaRegistry.getDefaultComponentElementName());
if (directiveResult === undefined) {
// `extractDirectiveMetadata` returns undefined when the @Directive has `jit: true`. In this
// case, compilation of the decorator is skipped. Returning an empty object signifies
Expand All @@ -169,7 +171,10 @@ export class ComponentDecoratorHandler implements
}, undefined) !;

const viewProviders: Expression|null = component.has('viewProviders') ?
new WrappedNodeExpr(component.get('viewProviders') !) :
new WrappedNodeExpr(
this.annotateForClosureCompiler ?
wrapFunctionExpressionsInParens(component.get('viewProviders') !) :
component.get('viewProviders') !) :
null;

// Parse the template.
Expand Down Expand Up @@ -297,7 +302,8 @@ export class ComponentDecoratorHandler implements
i18nUseExternalIds: this.i18nUseExternalIds, relativeContextFilePath
},
metadataStmt: generateSetClassMetadataCall(
node, this.reflector, this.defaultImportRecorder, this.isCore),
node, this.reflector, this.defaultImportRecorder, this.isCore,
this.annotateForClosureCompiler),
parsedTemplate: template, parseTemplate, templateSourceMapping,
},
typeCheck: true,
Expand Down
20 changes: 13 additions & 7 deletions packages/compiler-cli/src/ngtsc/annotations/src/directive.ts
Expand Up @@ -19,7 +19,7 @@ import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerFl

import {compileNgFactoryDefField} from './factory';
import {generateSetClassMetadataCall} from './metadata';
import {findAngularDecorator, getConstructorDependencies, isAngularDecorator, readBaseClass, unwrapConstructorDependencies, unwrapExpression, unwrapForwardRef, validateConstructorDependencies} from './util';
import {findAngularDecorator, getConstructorDependencies, isAngularDecorator, readBaseClass, unwrapConstructorDependencies, unwrapExpression, unwrapForwardRef, validateConstructorDependencies, wrapFunctionExpressionsInParens} from './util';

const EMPTY_OBJECT: {[key: string]: string} = {};
const FIELD_DECORATORS = [
Expand All @@ -40,7 +40,7 @@ export class DirectiveDecoratorHandler implements
constructor(
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
private metaRegistry: MetadataRegistry, private defaultImportRecorder: DefaultImportRecorder,
private isCore: boolean) {}
private isCore: boolean, private annotateForClosureCompiler: boolean) {}

readonly precedence = HandlerPrecedence.PRIMARY;

Expand Down Expand Up @@ -76,7 +76,7 @@ export class DirectiveDecoratorHandler implements
AnalysisOutput<DirectiveHandlerData> {
const directiveResult = extractDirectiveMetadata(
node, decorator, this.reflector, this.evaluator, this.defaultImportRecorder, this.isCore,
flags);
flags, this.annotateForClosureCompiler);
const analysis = directiveResult && directiveResult.metadata;

if (analysis === undefined) {
Expand All @@ -102,7 +102,8 @@ export class DirectiveDecoratorHandler implements
analysis: {
meta: analysis,
metadataStmt: generateSetClassMetadataCall(
node, this.reflector, this.defaultImportRecorder, this.isCore),
node, this.reflector, this.defaultImportRecorder, this.isCore,
this.annotateForClosureCompiler),
}
};
}
Expand Down Expand Up @@ -136,7 +137,8 @@ export class DirectiveDecoratorHandler implements
export function extractDirectiveMetadata(
clazz: ClassDeclaration, decorator: Decorator | null, reflector: ReflectionHost,
evaluator: PartialEvaluator, defaultImportRecorder: DefaultImportRecorder, isCore: boolean,
flags: HandlerFlags, defaultSelector: string | null = null): {
flags: HandlerFlags, annotateForClosureCompiler: boolean,
defaultSelector: string | null = null): {
decorator: Map<string, ts.Expression>,
metadata: R3DirectiveMetadata,
}|undefined {
Expand Down Expand Up @@ -230,8 +232,12 @@ export function extractDirectiveMetadata(

const host = extractHostBindings(decoratedElements, evaluator, coreModule, directive);

const providers: Expression|null =
directive.has('providers') ? new WrappedNodeExpr(directive.get('providers') !) : null;
const providers: Expression|null = directive.has('providers') ?
new WrappedNodeExpr(
annotateForClosureCompiler ?
wrapFunctionExpressionsInParens(directive.get('providers') !) :
directive.get('providers') !) :
null;

// Determine if `ngOnChanges` is a lifecycle hook defined on the component.
const usesOnChanges = members.some(
Expand Down
24 changes: 15 additions & 9 deletions packages/compiler-cli/src/ngtsc/annotations/src/metadata.ts
Expand Up @@ -12,7 +12,7 @@ import * as ts from 'typescript';
import {DefaultImportRecorder} from '../../imports';
import {CtorParameter, Decorator, ReflectionHost} from '../../reflection';

import {valueReferenceToExpression} from './util';
import {valueReferenceToExpression, wrapFunctionExpressionsInParens} from './util';


/**
Expand All @@ -24,7 +24,7 @@ import {valueReferenceToExpression} from './util';
*/
export function generateSetClassMetadataCall(
clazz: ts.Declaration, reflection: ReflectionHost, defaultImportRecorder: DefaultImportRecorder,
isCore: boolean): Statement|null {
isCore: boolean, annotateForClosureCompiler?: boolean): Statement|null {
if (!reflection.isClass(clazz)) {
return null;
}
Expand All @@ -37,7 +37,9 @@ export function generateSetClassMetadataCall(
return null;
}
const ngClassDecorators =
classDecorators.filter(dec => isAngularDecorator(dec, isCore)).map(decoratorToMetadata);
classDecorators.filter(dec => isAngularDecorator(dec, isCore))
.map(
(decorator: Decorator) => decoratorToMetadata(decorator, annotateForClosureCompiler));
if (ngClassDecorators.length === 0) {
return null;
}
Expand Down Expand Up @@ -113,8 +115,8 @@ function ctorParameterToMetadata(

// If the parameter has decorators, include the ones from Angular.
if (param.decorators !== null) {
const ngDecorators =
param.decorators.filter(dec => isAngularDecorator(dec, isCore)).map(decoratorToMetadata);
const ngDecorators = param.decorators.filter(dec => isAngularDecorator(dec, isCore))
.map((decorator: Decorator) => decoratorToMetadata(decorator));
const value = new WrappedNodeExpr(ts.createArrayLiteral(ngDecorators));
mapEntries.push({key: 'decorators', value, quoted: false});
}
Expand All @@ -126,16 +128,17 @@ function ctorParameterToMetadata(
*/
function classMemberToMetadata(
name: string, decorators: Decorator[], isCore: boolean): ts.PropertyAssignment {
const ngDecorators =
decorators.filter(dec => isAngularDecorator(dec, isCore)).map(decoratorToMetadata);
const ngDecorators = decorators.filter(dec => isAngularDecorator(dec, isCore))
.map((decorator: Decorator) => decoratorToMetadata(decorator));
const decoratorMeta = ts.createArrayLiteral(ngDecorators);
return ts.createPropertyAssignment(name, decoratorMeta);
}

/**
* Convert a reflected decorator to metadata.
*/
function decoratorToMetadata(decorator: Decorator): ts.ObjectLiteralExpression {
function decoratorToMetadata(
decorator: Decorator, wrapFunctionsInParens?: boolean): ts.ObjectLiteralExpression {
if (decorator.identifier === null) {
throw new Error('Illegal state: synthesized decorator cannot be emitted in class metadata.');
}
Expand All @@ -145,7 +148,10 @@ function decoratorToMetadata(decorator: Decorator): ts.ObjectLiteralExpression {
];
// Sometimes they have arguments.
if (decorator.args !== null && decorator.args.length > 0) {
const args = decorator.args.map(arg => ts.getMutableClone(arg));
const args = decorator.args.map(arg => {
const expr = ts.getMutableClone(arg);
return wrapFunctionsInParens ? wrapFunctionExpressionsInParens(expr) : expr;
});
properties.push(ts.createPropertyAssignment('args', ts.createArrayLiteral(args)));
}
return ts.createObjectLiteral(properties, true);
Expand Down
14 changes: 10 additions & 4 deletions packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts
Expand Up @@ -21,7 +21,7 @@ import {getSourceFile} from '../../util/src/typescript';

import {generateSetClassMetadataCall} from './metadata';
import {ReferencesRegistry} from './references_registry';
import {combineResolvers, findAngularDecorator, forwardRefResolver, getValidConstructorDependencies, isExpressionForwardReference, toR3Reference, unwrapExpression} from './util';
import {combineResolvers, findAngularDecorator, forwardRefResolver, getValidConstructorDependencies, isExpressionForwardReference, toR3Reference, unwrapExpression, wrapFunctionExpressionsInParens} from './util';

export interface NgModuleAnalysis {
mod: R3NgModuleMetadata;
Expand All @@ -44,7 +44,8 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
private scopeRegistry: LocalModuleScopeRegistry,
private referencesRegistry: ReferencesRegistry, private isCore: boolean,
private routeAnalyzer: NgModuleRouteAnalyzer|null, private refEmitter: ReferenceEmitter,
private defaultImportRecorder: DefaultImportRecorder, private localeId?: string) {}
private defaultImportRecorder: DefaultImportRecorder,
private annotateForClosureCompiler: boolean, private localeId?: string) {}

readonly precedence = HandlerPrecedence.PRIMARY;

Expand Down Expand Up @@ -212,7 +213,11 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
};

const rawProviders = ngModule.has('providers') ? ngModule.get('providers') ! : null;
const providers = rawProviders !== null ? new WrappedNodeExpr(rawProviders) : null;
const providers = rawProviders !== null ?
new WrappedNodeExpr(
this.annotateForClosureCompiler ? wrapFunctionExpressionsInParens(rawProviders) :
rawProviders) :
null;

// At this point, only add the module's imports as the injectors' imports. Any exported modules
// are added during `resolve`, as we need scope information to be able to filter out directives
Expand Down Expand Up @@ -244,7 +249,8 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
declarations: declarationRefs,
exports: exportRefs,
metadataStmt: generateSetClassMetadataCall(
node, this.reflector, this.defaultImportRecorder, this.isCore),
node, this.reflector, this.defaultImportRecorder, this.isCore,
this.annotateForClosureCompiler),
},
factorySymbolName: node.name.text,
};
Expand Down
25 changes: 25 additions & 0 deletions packages/compiler-cli/src/ngtsc/annotations/src/util.ts
Expand Up @@ -351,3 +351,28 @@ export function readBaseClass(

return null;
}

const parensWrapperTransformerFactory: ts.TransformerFactory<ts.Expression> =
(context: ts.TransformationContext) => {
const visitor: ts.Visitor = (node: ts.Node): ts.Node => {
const visited = ts.visitEachChild(node, visitor, context);
if (ts.isArrowFunction(visited) || ts.isFunctionExpression(visited)) {
return ts.createParen(visited);
}
return visited;
};
return (node: ts.Expression) => ts.visitEachChild(node, visitor, context);
};

/**
* Wraps all functions in a given expression in parentheses. This is needed to avoid problems
* where Tsickle annotations added between analyse and transform phases in Angular may trigger
* automatic semicolon insertion, e.g. if a function is the expression in a `return` statement. More
* info can be found in Tsickle source code here:
* https://github.com/angular/tsickle/blob/d7974262571c8a17d684e5ba07680e1b1993afdd/src/jsdoc_transformer.ts#L1021
*
* @param expression Expression where functions should be wrapped in parentheses
*/
export function wrapFunctionExpressionsInParens(expression: ts.Expression): ts.Expression {
return ts.transform(expression, [parensWrapperTransformerFactory]).transformed[0];
}
Expand Up @@ -60,9 +60,11 @@ runInEachFileSystem(() => {
const refEmitter = new ReferenceEmitter([]);

const handler = new ComponentDecoratorHandler(
reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, scopeRegistry, false,
new NoopResourceLoader(), [''], false, true, '', moduleResolver, cycleAnalyzer,
refEmitter, NOOP_DEFAULT_IMPORT_RECORDER);
reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, scopeRegistry,
/* isCore */ false, new NoopResourceLoader(), /* rootDirs */[''],
/* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true,
/* i18nLegacyMessageIdFormat */ '', moduleResolver, cycleAnalyzer, refEmitter,
NOOP_DEFAULT_IMPORT_RECORDER, /* annotateForClosureCompiler */ false);
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
if (detected === undefined) {
Expand Down
Expand Up @@ -49,7 +49,8 @@ runInEachFileSystem(() => {
metaReader, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]),
null);
const handler = new DirectiveDecoratorHandler(
reflectionHost, evaluator, scopeRegistry, NOOP_DEFAULT_IMPORT_RECORDER, false);
reflectionHost, evaluator, scopeRegistry, NOOP_DEFAULT_IMPORT_RECORDER,
/* isCore */ false, /* annotateForClosureCompiler */ false);

const analyzeDirective = (dirName: string) => {
const DirNode = getDeclaration(program, _('/entry.ts'), dirName, isNamedClassDeclaration);
Expand Down
Expand Up @@ -69,7 +69,8 @@ runInEachFileSystem(() => {

const handler = new NgModuleDecoratorHandler(
reflectionHost, evaluator, metaReader, metaRegistry, scopeRegistry, referencesRegistry,
false, null, refEmitter, NOOP_DEFAULT_IMPORT_RECORDER);
false, null, refEmitter, NOOP_DEFAULT_IMPORT_RECORDER,
/* annotateForClosureCompiler */ false);
const TestModule =
getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration);
const detected =
Expand Down
8 changes: 5 additions & 3 deletions packages/compiler-cli/src/ngtsc/program.ts
Expand Up @@ -604,16 +604,18 @@ export class NgtscProgram implements api.Program {
this.isCore, this.resourceManager, this.rootDirs,
this.options.preserveWhitespaces || false, this.options.i18nUseExternalIds !== false,
this.getI18nLegacyMessageFormat(), this.moduleResolver, this.cycleAnalyzer,
this.refEmitter, this.defaultImportTracker, this.incrementalState),
this.refEmitter, this.defaultImportTracker, this.closureCompilerEnabled,
this.incrementalState),
new DirectiveDecoratorHandler(
this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore),
this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore,
this.closureCompilerEnabled),
new InjectableDecoratorHandler(
this.reflector, this.defaultImportTracker, this.isCore,
this.options.strictInjectionParameters || false),
new NgModuleDecoratorHandler(
this.reflector, evaluator, this.metaReader, metaRegistry, scopeRegistry,
referencesRegistry, this.isCore, this.routeAnalyzer, this.refEmitter,
this.defaultImportTracker, this.options.i18nInLocale),
this.defaultImportTracker, this.closureCompilerEnabled, this.options.i18nInLocale),
new PipeDecoratorHandler(
this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore),
];
Expand Down

0 comments on commit fc2f6b8

Please sign in to comment.