refactor(ivy): obviate the Bazel component of the ivy_switch (#26550)

Originally, the ivy_switch mechanism used Bazel genrules to conditionally
compile one TS file or another depending on whether ngc or ngtsc was the
selected compiler. This was done because we wanted to avoid importing
certain modules (and thus pulling them into the build) if Ivy was on or
off. This mechanism had a major drawback: ivy_switch became a bottleneck
in the import graph, as it both imports from many places in the codebase
and is imported by many modules in the codebase. This frequently resulted
in cyclic imports which caused issues both with TS and Closure compilation.

It turns out ngcc needs both code paths in the bundle to perform the switch
during its operation anyway, so import switching was later abandoned. This
means that there's no real reason why the ivy_switch mechanism needed to
operate at the Bazel level, and for the ivy_switch file to be a bottleneck.

This commit removes the Bazel-level ivy_switch mechanism, and introduces
an additional TypeScript transform in ngtsc (and the pass-through tsc
compiler used for testing JIT) to perform the same operation that ngcc
does, and flip the switch during ngtsc compilation. This allows the
ivy_switch file to be removed, and the individual switches to be located
directly next to their consumers in the codebase, greatly mitigating the
circular import issues and making the mechanism much easier to use.

As part of this commit, the tag for marking switched variables was changed
from __PRE_NGCC__ to __PRE_R3__, since it's no longer just ngcc which
flips these tags. Most variables were renamed from R3_* to SWITCH_* as well,
since they're referenced mostly in render2 code.

Test strategy: existing test coverage is more than sufficient - if this
didn't work correctly it would break the hello world and todo apps.

PR Close #26550
This commit is contained in:
Alex Rickabaugh
2018-10-17 15:44:44 -07:00
committed by Alex Rickabaugh
parent 331989cea3
commit d4cee514f6
44 changed files with 507 additions and 437 deletions

View File

@ -18,6 +18,7 @@ import {BaseDefDecoratorHandler} from './annotations/src/base_def';
import {FactoryGenerator, FactoryInfo, GeneratedFactoryHostWrapper, generatedFactoryTransform} from './factories';
import {TypeScriptReflectionHost} from './metadata';
import {FileResourceLoader, HostResourceLoader} from './resource_loader';
import {ivySwitchTransform} from './switch';
import {IvyCompilation, ivyTransformFactory} from './transform';
import {TypeCheckContext, TypeCheckProgramHost} from './typecheck';
@ -177,6 +178,9 @@ export class NgtscProgram implements api.Program {
if (this.factoryToSourceInfo !== null) {
transforms.push(generatedFactoryTransform(this.factoryToSourceInfo, this.coreImportsFrom));
}
if (this.isCore) {
transforms.push(ivySwitchTransform);
}
// Run the emit, including a custom transformer that will downlevel the Ivy decorators in code.
const emitResult = emitCallback({
program: this.tsProgram,

View File

@ -0,0 +1,18 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ts_library")
ts_library(
name = "switch",
srcs = glob([
"index.ts",
"src/**/*.ts",
]),
module_name = "@angular/compiler-cli/src/ngtsc/switch",
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/host",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/util",
],
)

View File

@ -0,0 +1,9 @@
/**
* @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 {ivySwitchTransform} from './src/switch';

View File

@ -0,0 +1,152 @@
/**
* @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';
const IVY_SWITCH_PRE_SUFFIX = '__PRE_R3__';
const IVY_SWITCH_POST_SUFFIX = '__POST_R3__';
export function ivySwitchTransform(_: ts.TransformationContext): ts.Transformer<ts.SourceFile> {
return flipIvySwitchInFile;
}
function flipIvySwitchInFile(sf: ts.SourceFile): ts.SourceFile {
// To replace the statements array, it must be copied. This only needs to happen if a statement
// must actually be replaced within the array, so the newStatements array is lazily initialized.
let newStatements: ts.Statement[]|undefined = undefined;
// Iterate over the statements in the file.
for (let i = 0; i < sf.statements.length; i++) {
const statement = sf.statements[i];
// Skip over everything that isn't a variable statement.
if (!ts.isVariableStatement(statement) || !hasIvySwitches(statement)) {
continue;
}
// This statement needs to be replaced. Check if the newStatements array needs to be lazily
// initialized to a copy of the original statements.
if (newStatements === undefined) {
newStatements = [...sf.statements];
}
// Flip any switches in the VariableStatement. If there were any, a new statement will be
// returned; otherwise the old statement will be.
newStatements[i] = flipIvySwitchesInVariableStatement(statement, sf.statements);
}
// Only update the statements in the SourceFile if any have changed.
if (newStatements !== undefined) {
sf.statements = ts.createNodeArray(newStatements);
}
return sf;
}
/**
* Look for the ts.Identifier of a ts.Declaration with this name.
*
* The real identifier is needed (rather than fabricating one) as TypeScript decides how to
* reference this identifier based on information stored against its node in the AST, which a
* synthetic node would not have. In particular, since the post-switch variable is often exported,
* TypeScript needs to know this so it can write `exports.VAR` instead of just `VAR` when emitting
* code.
*
* Only variable, function, and class declarations are currently searched.
*/
function findPostSwitchIdentifier(
statements: ReadonlyArray<ts.Statement>, name: string): ts.Identifier|null {
for (const stmt of statements) {
if (ts.isVariableStatement(stmt)) {
const decl = stmt.declarationList.declarations.find(
decl => ts.isIdentifier(decl.name) && decl.name.text === name);
if (decl !== undefined) {
return decl.name as ts.Identifier;
}
} else if (ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) {
if (stmt.name !== undefined && ts.isIdentifier(stmt.name) && stmt.name.text === name) {
return stmt.name;
}
}
}
return null;
}
/**
* Flip any Ivy switches which are discovered in the given ts.VariableStatement.
*/
function flipIvySwitchesInVariableStatement(
stmt: ts.VariableStatement, statements: ReadonlyArray<ts.Statement>): ts.VariableStatement {
// Build a new list of variable declarations. Specific declarations that are initialized to a
// pre-switch identifier will be replaced with a declaration initialized to the post-switch
// identifier.
const newDeclarations = [...stmt.declarationList.declarations];
for (let i = 0; i < newDeclarations.length; i++) {
const decl = newDeclarations[i];
// Skip declarations that aren't initialized to an identifier.
if (decl.initializer === undefined || !ts.isIdentifier(decl.initializer)) {
continue;
}
// Skip declarations that aren't Ivy switches.
if (!decl.initializer.text.endsWith(IVY_SWITCH_PRE_SUFFIX)) {
continue;
}
// Determine the name of the post-switch variable.
const postSwitchName =
decl.initializer.text.replace(IVY_SWITCH_PRE_SUFFIX, IVY_SWITCH_POST_SUFFIX);
// Find the post-switch variable identifier. If one can't be found, it's an error. This is
// reported as a thrown error and not a diagnostic as transformers cannot output diagnostics.
let newIdentifier = findPostSwitchIdentifier(statements, postSwitchName);
if (newIdentifier === null) {
throw new Error(
`Unable to find identifier ${postSwitchName} in ${stmt.getSourceFile().fileName} for the Ivy switch.`);
}
// Copy the identifier with updateIdentifier(). This copies the internal information which
// allows TS to write a correct reference to the identifier.
newIdentifier = ts.updateIdentifier(newIdentifier);
newDeclarations[i] = ts.updateVariableDeclaration(
/* node */ decl,
/* name */ decl.name,
/* type */ decl.type,
/* initializer */ newIdentifier);
// Keeping parent pointers up to date is important for emit.
newIdentifier.parent = newDeclarations[i];
}
const newDeclList = ts.updateVariableDeclarationList(
/* declarationList */ stmt.declarationList,
/* declarations */ newDeclarations);
const newStmt = ts.updateVariableStatement(
/* statement */ stmt,
/* modifiers */ stmt.modifiers,
/* declarationList */ newDeclList);
// Keeping parent pointers up to date is important for emit.
for (const decl of newDeclarations) {
decl.parent = newDeclList;
}
newDeclList.parent = newStmt;
newStmt.parent = stmt.parent;
return newStmt;
}
/**
* Check whether the given VariableStatement has any Ivy switch variables.
*/
function hasIvySwitches(stmt: ts.VariableStatement) {
return stmt.declarationList.declarations.some(
decl => decl.initializer !== undefined && ts.isIdentifier(decl.initializer) &&
decl.initializer.text.endsWith(IVY_SWITCH_PRE_SUFFIX));
}