fix(ivy): wrap functions from "providers" in parentheses in Closure mode (#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: d797426257/src/jsdoc_transformer.ts (L1021)
PR Close #33609
This commit is contained in:
parent
11a35e5d14
commit
fc2f6b8456
|
@ -89,17 +89,18 @@ export class DecorationAnalyzer {
|
||||||
this.scopeRegistry, this.scopeRegistry, this.isCore, this.resourceManager, this.rootDirs,
|
this.scopeRegistry, this.scopeRegistry, this.isCore, this.resourceManager, this.rootDirs,
|
||||||
/* defaultPreserveWhitespaces */ false,
|
/* defaultPreserveWhitespaces */ false,
|
||||||
/* i18nUseExternalIds */ true, /* i18nLegacyMessageIdFormat */ '', this.moduleResolver,
|
/* 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(
|
new DirectiveDecoratorHandler(
|
||||||
this.reflectionHost, this.evaluator, this.fullRegistry, NOOP_DEFAULT_IMPORT_RECORDER,
|
this.reflectionHost, this.evaluator, this.fullRegistry, NOOP_DEFAULT_IMPORT_RECORDER,
|
||||||
this.isCore),
|
this.isCore, /* annotateForClosureCompiler */ false),
|
||||||
new InjectableDecoratorHandler(
|
new InjectableDecoratorHandler(
|
||||||
this.reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore,
|
this.reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore,
|
||||||
/* strictCtorDeps */ false),
|
/* strictCtorDeps */ false),
|
||||||
new NgModuleDecoratorHandler(
|
new NgModuleDecoratorHandler(
|
||||||
this.reflectionHost, this.evaluator, this.fullMetaReader, this.fullRegistry,
|
this.reflectionHost, this.evaluator, this.fullMetaReader, this.fullRegistry,
|
||||||
this.scopeRegistry, this.referencesRegistry, this.isCore, /* routeAnalyzer */ null,
|
this.scopeRegistry, this.referencesRegistry, this.isCore, /* routeAnalyzer */ null,
|
||||||
this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER),
|
this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER, /* annotateForClosureCompiler */ false),
|
||||||
new PipeDecoratorHandler(
|
new PipeDecoratorHandler(
|
||||||
this.reflectionHost, this.evaluator, this.metaRegistry, NOOP_DEFAULT_IMPORT_RECORDER,
|
this.reflectionHost, this.evaluator, this.metaRegistry, NOOP_DEFAULT_IMPORT_RECORDER,
|
||||||
this.isCore),
|
this.isCore),
|
||||||
|
|
|
@ -28,7 +28,7 @@ import {ResourceLoader} from './api';
|
||||||
import {extractDirectiveMetadata, parseFieldArrayValue} from './directive';
|
import {extractDirectiveMetadata, parseFieldArrayValue} from './directive';
|
||||||
import {compileNgFactoryDefField} from './factory';
|
import {compileNgFactoryDefField} from './factory';
|
||||||
import {generateSetClassMetadataCall} from './metadata';
|
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_MAP = new Map<string, Expression>();
|
||||||
const EMPTY_ARRAY: any[] = [];
|
const EMPTY_ARRAY: any[] = [];
|
||||||
|
@ -55,6 +55,7 @@ export class ComponentDecoratorHandler implements
|
||||||
private i18nLegacyMessageIdFormat: string, private moduleResolver: ModuleResolver,
|
private i18nLegacyMessageIdFormat: string, private moduleResolver: ModuleResolver,
|
||||||
private cycleAnalyzer: CycleAnalyzer, private refEmitter: ReferenceEmitter,
|
private cycleAnalyzer: CycleAnalyzer, private refEmitter: ReferenceEmitter,
|
||||||
private defaultImportRecorder: DefaultImportRecorder,
|
private defaultImportRecorder: DefaultImportRecorder,
|
||||||
|
private annotateForClosureCompiler: boolean,
|
||||||
private resourceDependencies:
|
private resourceDependencies:
|
||||||
ResourceDependencyRecorder = new NoopResourceDependencyRecorder()) {}
|
ResourceDependencyRecorder = new NoopResourceDependencyRecorder()) {}
|
||||||
|
|
||||||
|
@ -146,7 +147,8 @@ export class ComponentDecoratorHandler implements
|
||||||
// on it.
|
// on it.
|
||||||
const directiveResult = extractDirectiveMetadata(
|
const directiveResult = extractDirectiveMetadata(
|
||||||
node, decorator, this.reflector, this.evaluator, this.defaultImportRecorder, this.isCore,
|
node, decorator, this.reflector, this.evaluator, this.defaultImportRecorder, this.isCore,
|
||||||
flags, this.elementSchemaRegistry.getDefaultComponentElementName());
|
flags, this.annotateForClosureCompiler,
|
||||||
|
this.elementSchemaRegistry.getDefaultComponentElementName());
|
||||||
if (directiveResult === undefined) {
|
if (directiveResult === undefined) {
|
||||||
// `extractDirectiveMetadata` returns undefined when the @Directive has `jit: true`. In this
|
// `extractDirectiveMetadata` returns undefined when the @Directive has `jit: true`. In this
|
||||||
// case, compilation of the decorator is skipped. Returning an empty object signifies
|
// case, compilation of the decorator is skipped. Returning an empty object signifies
|
||||||
|
@ -169,7 +171,10 @@ export class ComponentDecoratorHandler implements
|
||||||
}, undefined) !;
|
}, undefined) !;
|
||||||
|
|
||||||
const viewProviders: Expression|null = component.has('viewProviders') ?
|
const viewProviders: Expression|null = component.has('viewProviders') ?
|
||||||
new WrappedNodeExpr(component.get('viewProviders') !) :
|
new WrappedNodeExpr(
|
||||||
|
this.annotateForClosureCompiler ?
|
||||||
|
wrapFunctionExpressionsInParens(component.get('viewProviders') !) :
|
||||||
|
component.get('viewProviders') !) :
|
||||||
null;
|
null;
|
||||||
|
|
||||||
// Parse the template.
|
// Parse the template.
|
||||||
|
@ -297,7 +302,8 @@ export class ComponentDecoratorHandler implements
|
||||||
i18nUseExternalIds: this.i18nUseExternalIds, relativeContextFilePath
|
i18nUseExternalIds: this.i18nUseExternalIds, relativeContextFilePath
|
||||||
},
|
},
|
||||||
metadataStmt: generateSetClassMetadataCall(
|
metadataStmt: generateSetClassMetadataCall(
|
||||||
node, this.reflector, this.defaultImportRecorder, this.isCore),
|
node, this.reflector, this.defaultImportRecorder, this.isCore,
|
||||||
|
this.annotateForClosureCompiler),
|
||||||
parsedTemplate: template, parseTemplate, templateSourceMapping,
|
parsedTemplate: template, parseTemplate, templateSourceMapping,
|
||||||
},
|
},
|
||||||
typeCheck: true,
|
typeCheck: true,
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerFl
|
||||||
|
|
||||||
import {compileNgFactoryDefField} from './factory';
|
import {compileNgFactoryDefField} from './factory';
|
||||||
import {generateSetClassMetadataCall} from './metadata';
|
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 EMPTY_OBJECT: {[key: string]: string} = {};
|
||||||
const FIELD_DECORATORS = [
|
const FIELD_DECORATORS = [
|
||||||
|
@ -40,7 +40,7 @@ export class DirectiveDecoratorHandler implements
|
||||||
constructor(
|
constructor(
|
||||||
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
|
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
|
||||||
private metaRegistry: MetadataRegistry, private defaultImportRecorder: DefaultImportRecorder,
|
private metaRegistry: MetadataRegistry, private defaultImportRecorder: DefaultImportRecorder,
|
||||||
private isCore: boolean) {}
|
private isCore: boolean, private annotateForClosureCompiler: boolean) {}
|
||||||
|
|
||||||
readonly precedence = HandlerPrecedence.PRIMARY;
|
readonly precedence = HandlerPrecedence.PRIMARY;
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ export class DirectiveDecoratorHandler implements
|
||||||
AnalysisOutput<DirectiveHandlerData> {
|
AnalysisOutput<DirectiveHandlerData> {
|
||||||
const directiveResult = extractDirectiveMetadata(
|
const directiveResult = extractDirectiveMetadata(
|
||||||
node, decorator, this.reflector, this.evaluator, this.defaultImportRecorder, this.isCore,
|
node, decorator, this.reflector, this.evaluator, this.defaultImportRecorder, this.isCore,
|
||||||
flags);
|
flags, this.annotateForClosureCompiler);
|
||||||
const analysis = directiveResult && directiveResult.metadata;
|
const analysis = directiveResult && directiveResult.metadata;
|
||||||
|
|
||||||
if (analysis === undefined) {
|
if (analysis === undefined) {
|
||||||
|
@ -102,7 +102,8 @@ export class DirectiveDecoratorHandler implements
|
||||||
analysis: {
|
analysis: {
|
||||||
meta: analysis,
|
meta: analysis,
|
||||||
metadataStmt: generateSetClassMetadataCall(
|
metadataStmt: generateSetClassMetadataCall(
|
||||||
node, this.reflector, this.defaultImportRecorder, this.isCore),
|
node, this.reflector, this.defaultImportRecorder, this.isCore,
|
||||||
|
this.annotateForClosureCompiler),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -136,7 +137,8 @@ export class DirectiveDecoratorHandler implements
|
||||||
export function extractDirectiveMetadata(
|
export function extractDirectiveMetadata(
|
||||||
clazz: ClassDeclaration, decorator: Decorator | null, reflector: ReflectionHost,
|
clazz: ClassDeclaration, decorator: Decorator | null, reflector: ReflectionHost,
|
||||||
evaluator: PartialEvaluator, defaultImportRecorder: DefaultImportRecorder, isCore: boolean,
|
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>,
|
decorator: Map<string, ts.Expression>,
|
||||||
metadata: R3DirectiveMetadata,
|
metadata: R3DirectiveMetadata,
|
||||||
}|undefined {
|
}|undefined {
|
||||||
|
@ -230,8 +232,12 @@ export function extractDirectiveMetadata(
|
||||||
|
|
||||||
const host = extractHostBindings(decoratedElements, evaluator, coreModule, directive);
|
const host = extractHostBindings(decoratedElements, evaluator, coreModule, directive);
|
||||||
|
|
||||||
const providers: Expression|null =
|
const providers: Expression|null = directive.has('providers') ?
|
||||||
directive.has('providers') ? new WrappedNodeExpr(directive.get('providers') !) : null;
|
new WrappedNodeExpr(
|
||||||
|
annotateForClosureCompiler ?
|
||||||
|
wrapFunctionExpressionsInParens(directive.get('providers') !) :
|
||||||
|
directive.get('providers') !) :
|
||||||
|
null;
|
||||||
|
|
||||||
// Determine if `ngOnChanges` is a lifecycle hook defined on the component.
|
// Determine if `ngOnChanges` is a lifecycle hook defined on the component.
|
||||||
const usesOnChanges = members.some(
|
const usesOnChanges = members.some(
|
||||||
|
|
|
@ -12,7 +12,7 @@ import * as ts from 'typescript';
|
||||||
import {DefaultImportRecorder} from '../../imports';
|
import {DefaultImportRecorder} from '../../imports';
|
||||||
import {CtorParameter, Decorator, ReflectionHost} from '../../reflection';
|
import {CtorParameter, Decorator, ReflectionHost} from '../../reflection';
|
||||||
|
|
||||||
import {valueReferenceToExpression} from './util';
|
import {valueReferenceToExpression, wrapFunctionExpressionsInParens} from './util';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,7 +24,7 @@ import {valueReferenceToExpression} from './util';
|
||||||
*/
|
*/
|
||||||
export function generateSetClassMetadataCall(
|
export function generateSetClassMetadataCall(
|
||||||
clazz: ts.Declaration, reflection: ReflectionHost, defaultImportRecorder: DefaultImportRecorder,
|
clazz: ts.Declaration, reflection: ReflectionHost, defaultImportRecorder: DefaultImportRecorder,
|
||||||
isCore: boolean): Statement|null {
|
isCore: boolean, annotateForClosureCompiler?: boolean): Statement|null {
|
||||||
if (!reflection.isClass(clazz)) {
|
if (!reflection.isClass(clazz)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,9 @@ export function generateSetClassMetadataCall(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const ngClassDecorators =
|
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) {
|
if (ngClassDecorators.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -113,8 +115,8 @@ function ctorParameterToMetadata(
|
||||||
|
|
||||||
// If the parameter has decorators, include the ones from Angular.
|
// If the parameter has decorators, include the ones from Angular.
|
||||||
if (param.decorators !== null) {
|
if (param.decorators !== null) {
|
||||||
const ngDecorators =
|
const ngDecorators = param.decorators.filter(dec => isAngularDecorator(dec, isCore))
|
||||||
param.decorators.filter(dec => isAngularDecorator(dec, isCore)).map(decoratorToMetadata);
|
.map((decorator: Decorator) => decoratorToMetadata(decorator));
|
||||||
const value = new WrappedNodeExpr(ts.createArrayLiteral(ngDecorators));
|
const value = new WrappedNodeExpr(ts.createArrayLiteral(ngDecorators));
|
||||||
mapEntries.push({key: 'decorators', value, quoted: false});
|
mapEntries.push({key: 'decorators', value, quoted: false});
|
||||||
}
|
}
|
||||||
|
@ -126,8 +128,8 @@ function ctorParameterToMetadata(
|
||||||
*/
|
*/
|
||||||
function classMemberToMetadata(
|
function classMemberToMetadata(
|
||||||
name: string, decorators: Decorator[], isCore: boolean): ts.PropertyAssignment {
|
name: string, decorators: Decorator[], isCore: boolean): ts.PropertyAssignment {
|
||||||
const ngDecorators =
|
const ngDecorators = decorators.filter(dec => isAngularDecorator(dec, isCore))
|
||||||
decorators.filter(dec => isAngularDecorator(dec, isCore)).map(decoratorToMetadata);
|
.map((decorator: Decorator) => decoratorToMetadata(decorator));
|
||||||
const decoratorMeta = ts.createArrayLiteral(ngDecorators);
|
const decoratorMeta = ts.createArrayLiteral(ngDecorators);
|
||||||
return ts.createPropertyAssignment(name, decoratorMeta);
|
return ts.createPropertyAssignment(name, decoratorMeta);
|
||||||
}
|
}
|
||||||
|
@ -135,7 +137,8 @@ function classMemberToMetadata(
|
||||||
/**
|
/**
|
||||||
* Convert a reflected decorator to metadata.
|
* Convert a reflected decorator to metadata.
|
||||||
*/
|
*/
|
||||||
function decoratorToMetadata(decorator: Decorator): ts.ObjectLiteralExpression {
|
function decoratorToMetadata(
|
||||||
|
decorator: Decorator, wrapFunctionsInParens?: boolean): ts.ObjectLiteralExpression {
|
||||||
if (decorator.identifier === null) {
|
if (decorator.identifier === null) {
|
||||||
throw new Error('Illegal state: synthesized decorator cannot be emitted in class metadata.');
|
throw new Error('Illegal state: synthesized decorator cannot be emitted in class metadata.');
|
||||||
}
|
}
|
||||||
|
@ -145,7 +148,10 @@ function decoratorToMetadata(decorator: Decorator): ts.ObjectLiteralExpression {
|
||||||
];
|
];
|
||||||
// Sometimes they have arguments.
|
// Sometimes they have arguments.
|
||||||
if (decorator.args !== null && decorator.args.length > 0) {
|
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)));
|
properties.push(ts.createPropertyAssignment('args', ts.createArrayLiteral(args)));
|
||||||
}
|
}
|
||||||
return ts.createObjectLiteral(properties, true);
|
return ts.createObjectLiteral(properties, true);
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {getSourceFile} from '../../util/src/typescript';
|
||||||
|
|
||||||
import {generateSetClassMetadataCall} from './metadata';
|
import {generateSetClassMetadataCall} from './metadata';
|
||||||
import {ReferencesRegistry} from './references_registry';
|
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 {
|
export interface NgModuleAnalysis {
|
||||||
mod: R3NgModuleMetadata;
|
mod: R3NgModuleMetadata;
|
||||||
|
@ -44,7 +44,8 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
|
||||||
private scopeRegistry: LocalModuleScopeRegistry,
|
private scopeRegistry: LocalModuleScopeRegistry,
|
||||||
private referencesRegistry: ReferencesRegistry, private isCore: boolean,
|
private referencesRegistry: ReferencesRegistry, private isCore: boolean,
|
||||||
private routeAnalyzer: NgModuleRouteAnalyzer|null, private refEmitter: ReferenceEmitter,
|
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;
|
readonly precedence = HandlerPrecedence.PRIMARY;
|
||||||
|
|
||||||
|
@ -212,7 +213,11 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
|
||||||
};
|
};
|
||||||
|
|
||||||
const rawProviders = ngModule.has('providers') ? ngModule.get('providers') ! : null;
|
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
|
// 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
|
// are added during `resolve`, as we need scope information to be able to filter out directives
|
||||||
|
@ -244,7 +249,8 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
|
||||||
declarations: declarationRefs,
|
declarations: declarationRefs,
|
||||||
exports: exportRefs,
|
exports: exportRefs,
|
||||||
metadataStmt: generateSetClassMetadataCall(
|
metadataStmt: generateSetClassMetadataCall(
|
||||||
node, this.reflector, this.defaultImportRecorder, this.isCore),
|
node, this.reflector, this.defaultImportRecorder, this.isCore,
|
||||||
|
this.annotateForClosureCompiler),
|
||||||
},
|
},
|
||||||
factorySymbolName: node.name.text,
|
factorySymbolName: node.name.text,
|
||||||
};
|
};
|
||||||
|
|
|
@ -351,3 +351,28 @@ export function readBaseClass(
|
||||||
|
|
||||||
return null;
|
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];
|
||||||
|
}
|
|
@ -60,9 +60,11 @@ runInEachFileSystem(() => {
|
||||||
const refEmitter = new ReferenceEmitter([]);
|
const refEmitter = new ReferenceEmitter([]);
|
||||||
|
|
||||||
const handler = new ComponentDecoratorHandler(
|
const handler = new ComponentDecoratorHandler(
|
||||||
reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, scopeRegistry, false,
|
reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, scopeRegistry,
|
||||||
new NoopResourceLoader(), [''], false, true, '', moduleResolver, cycleAnalyzer,
|
/* isCore */ false, new NoopResourceLoader(), /* rootDirs */[''],
|
||||||
refEmitter, NOOP_DEFAULT_IMPORT_RECORDER);
|
/* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true,
|
||||||
|
/* i18nLegacyMessageIdFormat */ '', moduleResolver, cycleAnalyzer, refEmitter,
|
||||||
|
NOOP_DEFAULT_IMPORT_RECORDER, /* annotateForClosureCompiler */ false);
|
||||||
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
|
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
|
||||||
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
|
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
|
||||||
if (detected === undefined) {
|
if (detected === undefined) {
|
||||||
|
|
|
@ -49,7 +49,8 @@ runInEachFileSystem(() => {
|
||||||
metaReader, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]),
|
metaReader, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]),
|
||||||
null);
|
null);
|
||||||
const handler = new DirectiveDecoratorHandler(
|
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 analyzeDirective = (dirName: string) => {
|
||||||
const DirNode = getDeclaration(program, _('/entry.ts'), dirName, isNamedClassDeclaration);
|
const DirNode = getDeclaration(program, _('/entry.ts'), dirName, isNamedClassDeclaration);
|
||||||
|
|
|
@ -69,7 +69,8 @@ runInEachFileSystem(() => {
|
||||||
|
|
||||||
const handler = new NgModuleDecoratorHandler(
|
const handler = new NgModuleDecoratorHandler(
|
||||||
reflectionHost, evaluator, metaReader, metaRegistry, scopeRegistry, referencesRegistry,
|
reflectionHost, evaluator, metaReader, metaRegistry, scopeRegistry, referencesRegistry,
|
||||||
false, null, refEmitter, NOOP_DEFAULT_IMPORT_RECORDER);
|
false, null, refEmitter, NOOP_DEFAULT_IMPORT_RECORDER,
|
||||||
|
/* annotateForClosureCompiler */ false);
|
||||||
const TestModule =
|
const TestModule =
|
||||||
getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration);
|
getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration);
|
||||||
const detected =
|
const detected =
|
||||||
|
|
|
@ -604,16 +604,18 @@ export class NgtscProgram implements api.Program {
|
||||||
this.isCore, this.resourceManager, this.rootDirs,
|
this.isCore, this.resourceManager, this.rootDirs,
|
||||||
this.options.preserveWhitespaces || false, this.options.i18nUseExternalIds !== false,
|
this.options.preserveWhitespaces || false, this.options.i18nUseExternalIds !== false,
|
||||||
this.getI18nLegacyMessageFormat(), this.moduleResolver, this.cycleAnalyzer,
|
this.getI18nLegacyMessageFormat(), this.moduleResolver, this.cycleAnalyzer,
|
||||||
this.refEmitter, this.defaultImportTracker, this.incrementalState),
|
this.refEmitter, this.defaultImportTracker, this.closureCompilerEnabled,
|
||||||
|
this.incrementalState),
|
||||||
new DirectiveDecoratorHandler(
|
new DirectiveDecoratorHandler(
|
||||||
this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore),
|
this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore,
|
||||||
|
this.closureCompilerEnabled),
|
||||||
new InjectableDecoratorHandler(
|
new InjectableDecoratorHandler(
|
||||||
this.reflector, this.defaultImportTracker, this.isCore,
|
this.reflector, this.defaultImportTracker, this.isCore,
|
||||||
this.options.strictInjectionParameters || false),
|
this.options.strictInjectionParameters || false),
|
||||||
new NgModuleDecoratorHandler(
|
new NgModuleDecoratorHandler(
|
||||||
this.reflector, evaluator, this.metaReader, metaRegistry, scopeRegistry,
|
this.reflector, evaluator, this.metaReader, metaRegistry, scopeRegistry,
|
||||||
referencesRegistry, this.isCore, this.routeAnalyzer, this.refEmitter,
|
referencesRegistry, this.isCore, this.routeAnalyzer, this.refEmitter,
|
||||||
this.defaultImportTracker, this.options.i18nInLocale),
|
this.defaultImportTracker, this.closureCompilerEnabled, this.options.i18nInLocale),
|
||||||
new PipeDecoratorHandler(
|
new PipeDecoratorHandler(
|
||||||
this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore),
|
this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore),
|
||||||
];
|
];
|
||||||
|
|
|
@ -337,25 +337,170 @@ runInEachFileSystem(os => {
|
||||||
// are valid for the real OS. When on non-Windows systems it doesn't like paths
|
// are valid for the real OS. When on non-Windows systems it doesn't like paths
|
||||||
// that start with `C:`.
|
// that start with `C:`.
|
||||||
if (os !== 'Windows' || platform() === 'win32') {
|
if (os !== 'Windows' || platform() === 'win32') {
|
||||||
it('should add @nocollapse to static fields when closure annotations are requested', () => {
|
describe('when closure annotations are requested', () => {
|
||||||
env.tsconfig({
|
|
||||||
'annotateForClosureCompiler': true,
|
it('should add @nocollapse to static fields', () => {
|
||||||
|
env.tsconfig({
|
||||||
|
'annotateForClosureCompiler': true,
|
||||||
|
});
|
||||||
|
env.write('test.ts', `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'test-cmp',
|
||||||
|
templateUrl: './dir/test.html',
|
||||||
|
})
|
||||||
|
export class TestCmp {}
|
||||||
|
`);
|
||||||
|
env.write('dir/test.html', '<p>Hello World</p>');
|
||||||
|
|
||||||
|
env.driveMain();
|
||||||
|
|
||||||
|
const jsContents = env.getContents('test.js');
|
||||||
|
expect(jsContents).toContain('/** @nocollapse */ TestCmp.ɵcmp');
|
||||||
});
|
});
|
||||||
env.write('test.ts', `
|
|
||||||
import {Component} from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
/**
|
||||||
selector: 'test-cmp',
|
* The following set of tests verify that after Tsickle run we do not have cases which
|
||||||
templateUrl: './dir/test.html',
|
* trigger automatic semicolon insertion, which breaks the code. In order to avoid the
|
||||||
})
|
* problem, we wrap all function expressions in certain fields ("providers" and
|
||||||
export class TestCmp {}
|
* "viewProviders") in parentheses. More info on Tsickle processing related to this case can
|
||||||
`);
|
* be found here:
|
||||||
env.write('dir/test.html', '<p>Hello World</p>');
|
* https://github.com/angular/tsickle/blob/d7974262571c8a17d684e5ba07680e1b1993afdd/src/jsdoc_transformer.ts#L1021
|
||||||
|
*/
|
||||||
|
describe('wrap functions in certain fields in parentheses', () => {
|
||||||
|
const providers = `
|
||||||
|
[{
|
||||||
|
provide: 'token-a',
|
||||||
|
useFactory: (service: Service) => {
|
||||||
|
return () => service.id;
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
provide: 'token-b',
|
||||||
|
useFactory: function(service: Service) {
|
||||||
|
return function() {
|
||||||
|
return service.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
`;
|
||||||
|
|
||||||
env.driveMain();
|
const service = `
|
||||||
|
export class Service {
|
||||||
|
id: string = 'service-id';
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const verifyOutput = (jsContents: string) => {
|
||||||
|
// verify that there is no pattern that triggers automatic semicolon insertion
|
||||||
|
// by checking that there are no return statements not wrapped in parentheses
|
||||||
|
expect(trim(jsContents)).not.toContain(trim(`
|
||||||
|
return /**
|
||||||
|
* @return {?}
|
||||||
|
*/
|
||||||
|
`));
|
||||||
|
expect(trim(jsContents)).toContain(trim(`
|
||||||
|
[{
|
||||||
|
provide: 'token-a',
|
||||||
|
useFactory: (function (service) {
|
||||||
|
return (/**
|
||||||
|
* @return {?}
|
||||||
|
*/
|
||||||
|
function () { return service.id; });
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
provide: 'token-b',
|
||||||
|
useFactory: (function (service) {
|
||||||
|
return (/**
|
||||||
|
* @return {?}
|
||||||
|
*/
|
||||||
|
function () {
|
||||||
|
return service.id;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}]
|
||||||
|
`));
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should wrap functions in "providers" list in NgModule', () => {
|
||||||
|
env.tsconfig({
|
||||||
|
'annotateForClosureCompiler': true,
|
||||||
|
});
|
||||||
|
env.write('service.ts', service);
|
||||||
|
env.write('test.ts', `
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
import {Service} from './service';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
providers: ${providers}
|
||||||
|
})
|
||||||
|
export class SomeModule {}
|
||||||
|
`);
|
||||||
|
|
||||||
|
env.driveMain();
|
||||||
|
verifyOutput(env.getContents('test.js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should wrap functions in "providers" list in Component', () => {
|
||||||
|
env.tsconfig({
|
||||||
|
'annotateForClosureCompiler': true,
|
||||||
|
});
|
||||||
|
env.write('service.ts', service);
|
||||||
|
env.write('test.ts', `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {Service} from './service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: '...',
|
||||||
|
providers: ${providers}
|
||||||
|
})
|
||||||
|
export class SomeComponent {}
|
||||||
|
`);
|
||||||
|
|
||||||
|
env.driveMain();
|
||||||
|
verifyOutput(env.getContents('test.js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should wrap functions in "viewProviders" list in Component', () => {
|
||||||
|
env.tsconfig({
|
||||||
|
'annotateForClosureCompiler': true,
|
||||||
|
});
|
||||||
|
env.write('service.ts', service);
|
||||||
|
env.write('test.ts', `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {Service} from './service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: '...',
|
||||||
|
viewProviders: ${providers}
|
||||||
|
})
|
||||||
|
export class SomeComponent {}
|
||||||
|
`);
|
||||||
|
|
||||||
|
env.driveMain();
|
||||||
|
verifyOutput(env.getContents('test.js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should wrap functions in "providers" list in Directive', () => {
|
||||||
|
env.tsconfig({
|
||||||
|
'annotateForClosureCompiler': true,
|
||||||
|
});
|
||||||
|
env.write('service.ts', service);
|
||||||
|
env.write('test.ts', `
|
||||||
|
import {Directive} from '@angular/core';
|
||||||
|
import {Service} from './service';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
providers: ${providers}
|
||||||
|
})
|
||||||
|
export class SomeDirective {}
|
||||||
|
`);
|
||||||
|
|
||||||
|
env.driveMain();
|
||||||
|
verifyOutput(env.getContents('test.js'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const jsContents = env.getContents('test.js');
|
|
||||||
expect(jsContents).toContain('/** @nocollapse */ TestCmp.ɵcmp');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue