feat(core): template-var-assignment update schematic (#29608)
Introduces a new update schematic called "template-var-assignment" that is responsible for analyzing template files in order to warn developers if template variables are assigned to values. The schematic also comes with a driver for `tslint` so that the check can be used wtihin Google. PR Close #29608
This commit is contained in:

committed by
Jason Aden

parent
15eb1e0ce1
commit
7c8f4e3202
@ -0,0 +1,126 @@
|
||||
/**
|
||||
* @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 {existsSync, readFileSync} from 'fs';
|
||||
import {dirname, resolve} from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {computeLineStartsMap, getLineAndCharacterFromPosition} from '../../../utils/line_mappings';
|
||||
import {getAngularDecorators} from '../../../utils/ng_decorators';
|
||||
import {unwrapExpression} from '../../../utils/typescript/functions';
|
||||
import {getPropertyNameText} from '../../../utils/typescript/property_name';
|
||||
|
||||
export interface ResolvedTemplate {
|
||||
/** File content of the given template. */
|
||||
content: string;
|
||||
/** Start offset of the template content (e.g. in the inline source file) */
|
||||
start: number;
|
||||
/** Whether the given template is inline or not. */
|
||||
inline: boolean;
|
||||
/**
|
||||
* Gets the character and line of a given position index in the template.
|
||||
* If the template is declared inline within a TypeScript source file, the line and
|
||||
* character are based on the full source file content.
|
||||
*/
|
||||
getCharacterAndLineOfPosition: (pos: number) => { character: number, line: number };
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitor that can be used to determine Angular templates referenced within given
|
||||
* TypeScript source files (inline templates or external referenced templates)
|
||||
*/
|
||||
export class NgComponentTemplateVisitor {
|
||||
resolvedTemplates = new Map<string, ResolvedTemplate>();
|
||||
|
||||
constructor(public typeChecker: ts.TypeChecker) {}
|
||||
|
||||
visitNode(node: ts.Node) {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ClassDeclaration:
|
||||
this.visitClassDeclaration(node as ts.ClassDeclaration);
|
||||
break;
|
||||
}
|
||||
|
||||
ts.forEachChild(node, node => this.visitNode(node));
|
||||
}
|
||||
|
||||
private visitClassDeclaration(node: ts.ClassDeclaration) {
|
||||
if (!node.decorators || !node.decorators.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ngDecorators = getAngularDecorators(this.typeChecker, node.decorators);
|
||||
const componentDecorator = ngDecorators.find(dec => dec.name === 'Component');
|
||||
|
||||
// In case no "@Component" decorator could be found on the current class, skip.
|
||||
if (!componentDecorator) {
|
||||
return;
|
||||
}
|
||||
|
||||
const decoratorCall = componentDecorator.node.expression;
|
||||
|
||||
// In case the component decorator call is not valid, skip this class declaration.
|
||||
if (decoratorCall.arguments.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const componentMetadata = unwrapExpression(decoratorCall.arguments[0]);
|
||||
|
||||
// Ensure that the component metadata is an object literal expression.
|
||||
if (!ts.isObjectLiteralExpression(componentMetadata)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceFile = node.getSourceFile();
|
||||
const sourceFileName = sourceFile.fileName;
|
||||
|
||||
// Walk through all component metadata properties and determine the referenced
|
||||
// HTML templates (either external or inline)
|
||||
componentMetadata.properties.forEach(property => {
|
||||
if (!ts.isPropertyAssignment(property)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const propertyName = getPropertyNameText(property.name);
|
||||
|
||||
// In case there is an inline template specified, ensure that the value is statically
|
||||
// analyzable by checking if the initializer is a string literal-like node.
|
||||
if (propertyName === 'template' && ts.isStringLiteralLike(property.initializer)) {
|
||||
// Need to add an offset of one to the start because the template quotes are
|
||||
// not part of the template content.
|
||||
const templateStartIdx = property.initializer.getStart() + 1;
|
||||
this.resolvedTemplates.set(resolve(sourceFileName), {
|
||||
content: property.initializer.text,
|
||||
inline: true,
|
||||
start: templateStartIdx,
|
||||
getCharacterAndLineOfPosition:
|
||||
pos => ts.getLineAndCharacterOfPosition(sourceFile, pos + templateStartIdx)
|
||||
});
|
||||
}
|
||||
if (propertyName === 'templateUrl' && ts.isStringLiteralLike(property.initializer)) {
|
||||
const templatePath = resolve(dirname(sourceFileName), property.initializer.text);
|
||||
|
||||
// In case the template does not exist in the file system, skip this
|
||||
// external template.
|
||||
if (!existsSync(templatePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileContent = readFileSync(templatePath, 'utf8');
|
||||
const lineStartsMap = computeLineStartsMap(fileContent);
|
||||
|
||||
this.resolvedTemplates.set(templatePath, {
|
||||
content: fileContent,
|
||||
inline: false,
|
||||
start: 0,
|
||||
getCharacterAndLineOfPosition: pos => getLineAndCharacterFromPosition(lineStartsMap, pos),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @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 {ParseSourceSpan, PropertyWrite, RecursiveAstVisitor} from '@angular/compiler';
|
||||
import {BoundEvent, Element, NullVisitor, Template, Variable, visitAll} from '@angular/compiler/src/render3/r3_ast';
|
||||
|
||||
export interface PropertyAssignment {
|
||||
start: number;
|
||||
end: number;
|
||||
node: PropertyWrite;
|
||||
}
|
||||
|
||||
/**
|
||||
* AST visitor that traverses the Render3 HTML AST in order to find all declared
|
||||
* template variables and property assignments within bound events.
|
||||
*/
|
||||
export class PropertyWriteHtmlVisitor extends NullVisitor {
|
||||
templateVariables: Variable[] = [];
|
||||
propertyAssignments: PropertyAssignment[] = [];
|
||||
|
||||
private expressionAstVisitor = new ExpressionAstVisitor(this.propertyAssignments);
|
||||
|
||||
visitElement(element: Element): void {
|
||||
visitAll(this, element.outputs);
|
||||
visitAll(this, element.children);
|
||||
}
|
||||
|
||||
visitTemplate(template: Template): void {
|
||||
// Visit all children of the template. The template proxies the outputs of the
|
||||
// immediate child elements, so we just ignore outputs on the "Template" in order
|
||||
// to not visit similar bound events twice.
|
||||
visitAll(this, template.children);
|
||||
|
||||
// Keep track of all declared local template variables.
|
||||
this.templateVariables.push(...template.variables);
|
||||
}
|
||||
|
||||
visitBoundEvent(node: BoundEvent) {
|
||||
node.handler.visit(this.expressionAstVisitor, node.handlerSpan);
|
||||
}
|
||||
}
|
||||
|
||||
/** AST visitor that resolves all property assignments with a given expression AST. */
|
||||
class ExpressionAstVisitor extends RecursiveAstVisitor {
|
||||
constructor(private propertyAssignments: PropertyAssignment[]) { super(); }
|
||||
|
||||
visitPropertyWrite(node: PropertyWrite, span: ParseSourceSpan) {
|
||||
this.propertyAssignments.push({
|
||||
node: node,
|
||||
start: span.start.offset,
|
||||
end: span.end.offset,
|
||||
});
|
||||
|
||||
super.visitPropertyWrite(node, span);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user