mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
refactor(schematics): cleanup input and output name rules (#12800)
* Cleans up the update rules for `@Input()` and `@Output()` names. * Adds thorough test cases for input and output names.
This commit is contained in:
committed by
Jeremy Elbourn
parent
3723191be8
commit
1840008837
@@ -10,10 +10,26 @@ import {findAttributeOnElementWithAttrs, findAttributeOnElementWithTag} from './
|
||||
|
||||
/** Finds the specified Angular @Input in the given elements with tag name. */
|
||||
export function findInputsOnElementWithTag(html: string, inputName: string, tagNames: string[]) {
|
||||
// Add one column to the mapped offset because the first bracket for the @Input
|
||||
// is part of the attribute and therefore also part of the offset. We only want to return
|
||||
// the offset for the inner name of the input.
|
||||
return findAttributeOnElementWithTag(html, `[${inputName}]`, tagNames).map(offset => offset + 1);
|
||||
return [
|
||||
// Inputs can be also used without brackets (e.g. `<mat-toolbar color="primary">`)
|
||||
...findAttributeOnElementWithTag(html, inputName, tagNames),
|
||||
// Add one column to the mapped offset because the first bracket for the @Input
|
||||
// is part of the attribute and therefore also part of the offset. We only want to return
|
||||
// the offset for the inner name of the bracketed input.
|
||||
...findAttributeOnElementWithTag(html, `[${inputName}]`, tagNames).map(offset => offset + 1),
|
||||
];
|
||||
}
|
||||
|
||||
/** Finds the specified Angular @Input in elements that have one of the specified attributes. */
|
||||
export function findInputsOnElementWithAttr(html: string, inputName: string, attrs: string[]) {
|
||||
return [
|
||||
// Inputs can be also used without brackets (e.g. `<button mat-button color="primary">`)
|
||||
...findAttributeOnElementWithAttrs(html, inputName, attrs),
|
||||
// Add one column to the mapped offset because the first bracket for the @Input
|
||||
// is part of the attribute and therefore also part of the offset. We only want to return
|
||||
// the offset for the inner name of the bracketed input.
|
||||
...findAttributeOnElementWithAttrs(html, `[${inputName}]`, attrs).map(offset => offset + 1),
|
||||
];
|
||||
}
|
||||
|
||||
/** Finds the specified Angular @Output in the given elements with tag name. */
|
||||
@@ -24,14 +40,6 @@ export function findOutputsOnElementWithTag(html: string, outputName: string, ta
|
||||
return findAttributeOnElementWithTag(html, `(${outputName})`, tagNames).map(offset => offset + 1);
|
||||
}
|
||||
|
||||
/** Finds the specified Angular @Input in elements that have one of the specified attributes. */
|
||||
export function findInputsOnElementWithAttr(html: string, inputName: string, attrs: string[]) {
|
||||
// Add one column to the mapped offset because the first bracket for the @Input
|
||||
// is part of the attribute and therefore also part of the offset. We only want to return
|
||||
// the offset for the inner name of the input.
|
||||
return findAttributeOnElementWithAttrs(html, `[${inputName}]`, attrs).map(offset => offset + 1);
|
||||
}
|
||||
|
||||
/** Finds the specified Angular @Output in elements that have one of the specified attributes. */
|
||||
export function findOutputsOnElementWithAttr(html: string, outputName: string, attrs: string[]) {
|
||||
// Add one column to the mapped offset because the first bracket for the @Output
|
||||
|
||||
@@ -19,8 +19,12 @@ export interface MaterialInputNameData {
|
||||
elements?: string[],
|
||||
/** Limit to elements with any of these attributes. */
|
||||
attributes?: string[],
|
||||
/** Whether to ignore CSS attribute selectors when doing this replacement. */
|
||||
css?: boolean,
|
||||
/**
|
||||
* Whether inputs in stylesheets should be updated or not. Note that inputs inside of
|
||||
* stylesheets usually don't make sense, but if developers use an input as a plain one-time
|
||||
* attribute, it can be targeted through CSS selectors.
|
||||
*/
|
||||
stylesheet?: boolean,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ export interface MaterialOutputNameData {
|
||||
elements?: string[],
|
||||
/** Limit to elements with any of these attributes. */
|
||||
attributes?: string[],
|
||||
/** Whether to ignore CSS attribute selectors when doing this replacement. */
|
||||
css?: boolean,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 {green, red} from 'chalk';
|
||||
import {sync as globSync} from 'glob';
|
||||
import {IOptions, Replacement, RuleFailure, Rules} from 'tslint';
|
||||
import * as ts from 'typescript';
|
||||
import {inputNames} from '../../material/data/input-names';
|
||||
import {ExternalResource} from '../../tslint/component-file';
|
||||
import {ComponentWalker} from '../../tslint/component-walker';
|
||||
import {
|
||||
addFailureAtReplacement,
|
||||
createExternalReplacementFailure,
|
||||
} from '../../tslint/rule-failures';
|
||||
import {findAllSubstringIndices} from '../../typescript/literal';
|
||||
|
||||
/**
|
||||
* Rule that walks through every inline or external stylesheet and replaces outdated CSS selectors
|
||||
* that query for an @Input() with the new input name.
|
||||
*
|
||||
* Note that inputs inside of stylesheets usually don't make sense, but if developers use an
|
||||
* input as a plain one-time attribute, it can be targeted through CSS selectors.
|
||||
*
|
||||
* e.g. `<my-component color="primary">` becomes `my-component[color]`
|
||||
*/
|
||||
export class Rule extends Rules.AbstractRule {
|
||||
apply(sourceFile: ts.SourceFile): RuleFailure[] {
|
||||
return this.applyWithWalker(new Walker(sourceFile, this.getOptions()));
|
||||
}
|
||||
}
|
||||
|
||||
export class Walker extends ComponentWalker {
|
||||
|
||||
constructor(sourceFile: ts.SourceFile, options: IOptions) {
|
||||
// In some applications, developers will have global stylesheets that are not specified in any
|
||||
// Angular component. Therefore we glob up all css and scss files outside of node_modules and
|
||||
// dist and check them as well.
|
||||
const extraFiles = globSync('!(node_modules|dist)/**/*.+(css|scss)');
|
||||
super(sourceFile, options, extraFiles);
|
||||
extraFiles.forEach(styleUrl => this._reportExternalStyle(styleUrl));
|
||||
}
|
||||
|
||||
visitInlineStylesheet(literal: ts.StringLiteral) {
|
||||
this._createReplacementsForContent(literal, literal.getText())
|
||||
.forEach(data => addFailureAtReplacement(this, data.failureMessage, data.replacement));
|
||||
}
|
||||
|
||||
visitExternalStylesheet(node: ExternalResource) {
|
||||
this._createReplacementsForContent(node, node.getFullText())
|
||||
.map(data => createExternalReplacementFailure(node, data.failureMessage,
|
||||
this.getRuleName(), data.replacement))
|
||||
.forEach(failure => this.addFailure(failure));
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for outdated attribute selectors in the specified content and creates replacements
|
||||
* with the according messages that can be added to a rule failure.
|
||||
*/
|
||||
private _createReplacementsForContent(node: ts.Node, stylesheetContent: string) {
|
||||
const replacements: {failureMessage: string, replacement: Replacement}[] = [];
|
||||
|
||||
inputNames.forEach(name => {
|
||||
if (name.whitelist && !name.whitelist.stylesheet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSelector = `[${name.replace}]`;
|
||||
const updatedSelector = `[${name.replaceWith}]`;
|
||||
|
||||
const failureMessage = `Found deprecated @Input() CSS selector "${red(currentSelector)}" ` +
|
||||
`which has been renamed to "${green(updatedSelector)}"`;
|
||||
|
||||
findAllSubstringIndices(stylesheetContent, currentSelector)
|
||||
.map(offset => node.getStart() + offset)
|
||||
.map(start => new Replacement(start, currentSelector.length, updatedSelector))
|
||||
.forEach(replacement => replacements.push({replacement, failureMessage}));
|
||||
});
|
||||
|
||||
return replacements;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 {green, red} from 'chalk';
|
||||
import {Replacement, RuleFailure, Rules} from 'tslint';
|
||||
import * as ts from 'typescript';
|
||||
import {findInputsOnElementWithAttr, findInputsOnElementWithTag} from '../../html/angular';
|
||||
import {inputNames} from '../../material/data/input-names';
|
||||
import {ExternalResource} from '../../tslint/component-file';
|
||||
import {ComponentWalker} from '../../tslint/component-walker';
|
||||
import {
|
||||
addFailureAtReplacement,
|
||||
createExternalReplacementFailure,
|
||||
} from '../../tslint/rule-failures';
|
||||
|
||||
/**
|
||||
* Rule that walks through every inline or external HTML template and switches changed input
|
||||
* bindings to the proper new name.
|
||||
*/
|
||||
export class Rule extends Rules.AbstractRule {
|
||||
apply(sourceFile: ts.SourceFile): RuleFailure[] {
|
||||
return this.applyWithWalker(new Walker(sourceFile, this.getOptions()));
|
||||
}
|
||||
}
|
||||
|
||||
export class Walker extends ComponentWalker {
|
||||
|
||||
visitInlineTemplate(template: ts.StringLiteral) {
|
||||
this._createReplacementsForContent(template, template.getText())
|
||||
.forEach(data => addFailureAtReplacement(this, data.failureMessage, data.replacement));
|
||||
}
|
||||
|
||||
visitExternalTemplate(template: ExternalResource) {
|
||||
this._createReplacementsForContent(template, template.getFullText())
|
||||
.map(data => createExternalReplacementFailure(template, data.failureMessage,
|
||||
this.getRuleName(), data.replacement))
|
||||
.forEach(failure => this.addFailure(failure));
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for outdated input bindings in the specified content and creates
|
||||
* replacements with the according messages that can be added to a rule failure.
|
||||
*/
|
||||
private _createReplacementsForContent(node: ts.Node, templateContent: string) {
|
||||
const replacements: {failureMessage: string, replacement: Replacement}[] = [];
|
||||
|
||||
inputNames.forEach(name => {
|
||||
const whitelist = name.whitelist;
|
||||
const relativeOffsets = [];
|
||||
const failureMessage = `Found deprecated @Input() "${red(name.replace)}"` +
|
||||
` which has been renamed to "${green(name.replaceWith)}"`;
|
||||
|
||||
if (!whitelist || whitelist.attributes) {
|
||||
relativeOffsets.push(
|
||||
...findInputsOnElementWithAttr(templateContent, name.replace, whitelist.attributes));
|
||||
}
|
||||
|
||||
if (!whitelist || whitelist.elements) {
|
||||
relativeOffsets.push(
|
||||
...findInputsOnElementWithTag(templateContent, name.replace, whitelist.elements));
|
||||
}
|
||||
|
||||
relativeOffsets
|
||||
.map(offset => node.getStart() + offset)
|
||||
.map(start => new Replacement(start, name.replace.length, name.replaceWith))
|
||||
.forEach(replacement => replacements.push({replacement, failureMessage}));
|
||||
});
|
||||
|
||||
return replacements;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 {green, red} from 'chalk';
|
||||
import {Replacement, RuleFailure, Rules} from 'tslint';
|
||||
import * as ts from 'typescript';
|
||||
import {findOutputsOnElementWithAttr, findOutputsOnElementWithTag} from '../../html/angular';
|
||||
import {outputNames} from '../../material/data/output-names';
|
||||
import {ExternalResource} from '../../tslint/component-file';
|
||||
import {ComponentWalker} from '../../tslint/component-walker';
|
||||
import {
|
||||
addFailureAtReplacement,
|
||||
createExternalReplacementFailure,
|
||||
} from '../../tslint/rule-failures';
|
||||
|
||||
/**
|
||||
* Rule that walks through every inline or external HTML template and switches changed output
|
||||
* bindings to the proper new output name.
|
||||
*/
|
||||
export class Rule extends Rules.AbstractRule {
|
||||
apply(sourceFile: ts.SourceFile): RuleFailure[] {
|
||||
return this.applyWithWalker(new Walker(sourceFile, this.getOptions()));
|
||||
}
|
||||
}
|
||||
|
||||
export class Walker extends ComponentWalker {
|
||||
|
||||
visitInlineTemplate(template: ts.StringLiteral) {
|
||||
this._createReplacementsForContent(template, template.getText())
|
||||
.forEach(data => addFailureAtReplacement(this, data.failureMessage, data.replacement));
|
||||
}
|
||||
|
||||
visitExternalTemplate(template: ExternalResource) {
|
||||
this._createReplacementsForContent(template, template.getFullText())
|
||||
.map(data => createExternalReplacementFailure(template, data.failureMessage,
|
||||
this.getRuleName(), data.replacement))
|
||||
.forEach(failure => this.addFailure(failure));
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for outdated output bindings in the specified content and creates
|
||||
* replacements with the according messages that can be added to a rule failure.
|
||||
*/
|
||||
private _createReplacementsForContent(node: ts.Node, templateContent: string) {
|
||||
const replacements: {failureMessage: string, replacement: Replacement}[] = [];
|
||||
|
||||
outputNames.forEach(name => {
|
||||
const whitelist = name.whitelist;
|
||||
const relativeOffsets = [];
|
||||
const failureMessage = `Found deprecated @Output() "${red(name.replace)}"` +
|
||||
` which has been renamed to "${green(name.replaceWith)}"`;
|
||||
|
||||
if (!whitelist || whitelist.attributes) {
|
||||
relativeOffsets.push(
|
||||
...findOutputsOnElementWithAttr(templateContent, name.replace, whitelist.attributes));
|
||||
}
|
||||
|
||||
if (!whitelist || whitelist.elements) {
|
||||
relativeOffsets.push(
|
||||
...findOutputsOnElementWithTag(templateContent, name.replace, whitelist.elements));
|
||||
}
|
||||
|
||||
relativeOffsets
|
||||
.map(offset => node.getStart() + offset)
|
||||
.map(start => new Replacement(start, name.replace.length, name.replaceWith))
|
||||
.forEach(replacement => replacements.push({replacement, failureMessage}));
|
||||
});
|
||||
|
||||
return replacements;
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 {green, red} from 'chalk';
|
||||
import {sync as globSync} from 'glob';
|
||||
import {IOptions, Replacement, RuleFailure, Rules} from 'tslint';
|
||||
import {inputNames} from '../material/data/input-names';
|
||||
import {ExternalResource} from '../tslint/component-file';
|
||||
import {ComponentWalker} from '../tslint/component-walker';
|
||||
import {findAllSubstringIndices} from '../typescript/literal';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
/**
|
||||
* Rule that walks through every component decorator and updates their inline or external
|
||||
* stylesheets.
|
||||
*/
|
||||
export class Rule extends Rules.AbstractRule {
|
||||
apply(sourceFile: ts.SourceFile): RuleFailure[] {
|
||||
return this.applyWithWalker(
|
||||
new SwitchStylesheetInputNamesWalker(sourceFile, this.getOptions()));
|
||||
}
|
||||
}
|
||||
|
||||
export class SwitchStylesheetInputNamesWalker extends ComponentWalker {
|
||||
|
||||
constructor(sourceFile: ts.SourceFile, options: IOptions) {
|
||||
// In some applications, developers will have global stylesheets that are not specified in any
|
||||
// Angular component. Therefore we glob up all css and scss files outside of node_modules and
|
||||
// dist and check them as well.
|
||||
const extraFiles = globSync('!(node_modules|dist)/**/*.+(css|scss)');
|
||||
super(sourceFile, options, extraFiles);
|
||||
extraFiles.forEach(styleUrl => this._reportExternalStyle(styleUrl));
|
||||
}
|
||||
|
||||
visitInlineStylesheet(stylesheet: ts.StringLiteral) {
|
||||
this.replaceNamesInStylesheet(stylesheet, stylesheet.getText()).forEach(replacement => {
|
||||
const fix = replacement.replacement;
|
||||
const ruleFailure = new RuleFailure(stylesheet.getSourceFile(), fix.start, fix.end,
|
||||
replacement.message, this.getRuleName(), fix);
|
||||
this.addFailure(ruleFailure);
|
||||
});
|
||||
}
|
||||
|
||||
visitExternalStylesheet(stylesheet: ExternalResource) {
|
||||
this.replaceNamesInStylesheet(stylesheet, stylesheet.getFullText()).forEach(replacement => {
|
||||
const fix = replacement.replacement;
|
||||
const ruleFailure = new RuleFailure(stylesheet, fix.start + 1, fix.end + 1,
|
||||
replacement.message, this.getRuleName(), fix);
|
||||
this.addFailure(ruleFailure);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the outdated name in the stylesheet with the new one and returns an updated
|
||||
* stylesheet.
|
||||
*/
|
||||
private replaceNamesInStylesheet(node: ts.Node, stylesheetContent: string):
|
||||
{message: string, replacement: Replacement}[] {
|
||||
const replacements: {message: string, replacement: Replacement}[] = [];
|
||||
|
||||
inputNames.forEach(name => {
|
||||
if (!name.whitelist || name.whitelist.css) {
|
||||
const bracketedName = {replace: `[${name.replace}]`, replaceWith: `[${name.replaceWith}]`};
|
||||
const foundOffsets = findAllSubstringIndices(stylesheetContent, bracketedName.replace);
|
||||
|
||||
this.createReplacementsForOffsets(node, name, foundOffsets).forEach(replacement => {
|
||||
replacements.push({
|
||||
message: `Found deprecated @Input() "${red(name.replace)}" which has been renamed` +
|
||||
` to "${green(name.replaceWith)}"`,
|
||||
replacement
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return replacements;
|
||||
}
|
||||
|
||||
private createReplacementsForOffsets(node: ts.Node,
|
||||
update: {replace: string, replaceWith: string},
|
||||
offsets: number[]): Replacement[] {
|
||||
return offsets.map(offset => this.createReplacement(
|
||||
node.getStart() + offset, update.replace.length, update.replaceWith));
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 {green, red} from 'chalk';
|
||||
import {sync as globSync} from 'glob';
|
||||
import {IOptions, Replacement, RuleFailure, Rules} from 'tslint';
|
||||
import {outputNames} from '../material/data/output-names';
|
||||
import {ExternalResource} from '../tslint/component-file';
|
||||
import {ComponentWalker} from '../tslint/component-walker';
|
||||
import {findAllSubstringIndices} from '../typescript/literal';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
/**
|
||||
* Rule that walks through every component decorator and updates their inline or external
|
||||
* stylesheets.
|
||||
*/
|
||||
export class Rule extends Rules.AbstractRule {
|
||||
apply(sourceFile: ts.SourceFile): RuleFailure[] {
|
||||
return this.applyWithWalker(
|
||||
new SwitchStylesheetOutputNamesWalker(sourceFile, this.getOptions()));
|
||||
}
|
||||
}
|
||||
|
||||
export class SwitchStylesheetOutputNamesWalker extends ComponentWalker {
|
||||
|
||||
constructor(sourceFile: ts.SourceFile, options: IOptions) {
|
||||
// In some applications, developers will have global stylesheets that are not specified in any
|
||||
// Angular component. Therefore we glob up all css and scss files outside of node_modules and
|
||||
// dist and check them as well.
|
||||
const extraFiles = globSync('!(node_modules|dist)/**/*.+(css|scss)');
|
||||
super(sourceFile, options, extraFiles);
|
||||
extraFiles.forEach(styleUrl => this._reportExternalStyle(styleUrl));
|
||||
}
|
||||
|
||||
visitInlineStylesheet(stylesheet: ts.StringLiteral) {
|
||||
this.replaceNamesInStylesheet(stylesheet, stylesheet.getText()).forEach(replacement => {
|
||||
const fix = replacement.replacement;
|
||||
const ruleFailure = new RuleFailure(stylesheet.getSourceFile(), fix.start, fix.end,
|
||||
replacement.message, this.getRuleName(), fix);
|
||||
this.addFailure(ruleFailure);
|
||||
});
|
||||
}
|
||||
|
||||
visitExternalStylesheet(stylesheet: ExternalResource) {
|
||||
this.replaceNamesInStylesheet(stylesheet, stylesheet.getFullText()).forEach(replacement => {
|
||||
const fix = replacement.replacement;
|
||||
const ruleFailure = new RuleFailure(stylesheet, fix.start + 1, fix.end + 1,
|
||||
replacement.message, this.getRuleName(), fix);
|
||||
this.addFailure(ruleFailure);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the outdated name in the stylesheet with the new one and returns an updated
|
||||
* stylesheet.
|
||||
*/
|
||||
private replaceNamesInStylesheet(node: ts.Node, stylesheetContent: string):
|
||||
{message: string, replacement: Replacement}[] {
|
||||
const replacements: {message: string, replacement: Replacement}[] = [];
|
||||
|
||||
outputNames.forEach(name => {
|
||||
if (!name.whitelist || name.whitelist.css) {
|
||||
const bracketedName = {replace: `[${name.replace}]`, replaceWith: `[${name.replaceWith}]`};
|
||||
const foundOffsets = findAllSubstringIndices(stylesheetContent, bracketedName.replace);
|
||||
|
||||
this.createReplacementsForOffsets(node, name, foundOffsets).forEach(replacement => {
|
||||
replacements.push({
|
||||
message: `Found deprecated @Output() "${red(name.replace)}" which has been` +
|
||||
` renamed to "${green(name.replaceWith)}"`,
|
||||
replacement
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return replacements;
|
||||
}
|
||||
|
||||
private createReplacementsForOffsets(node: ts.Node,
|
||||
update: {replace: string, replaceWith: string},
|
||||
offsets: number[]): Replacement[] {
|
||||
return offsets.map(offset => this.createReplacement(
|
||||
node.getStart() + offset, update.replace.length, update.replaceWith));
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 {green, red} from 'chalk';
|
||||
import {Replacement, RuleFailure, Rules} from 'tslint';
|
||||
import * as ts from 'typescript';
|
||||
import {findInputsOnElementWithAttr, findInputsOnElementWithTag} from '../html/angular';
|
||||
import {inputNames} from '../material/data/input-names';
|
||||
import {ExternalResource} from '../tslint/component-file';
|
||||
import {ComponentWalker} from '../tslint/component-walker';
|
||||
import {findAllSubstringIndices} from '../typescript/literal';
|
||||
|
||||
/**
|
||||
* Rule that walks through every component decorator and updates their inline or external
|
||||
* templates.
|
||||
*/
|
||||
export class Rule extends Rules.AbstractRule {
|
||||
apply(sourceFile: ts.SourceFile): RuleFailure[] {
|
||||
return this.applyWithWalker(new SwitchTemplateInputNamesWalker(sourceFile, this.getOptions()));
|
||||
}
|
||||
}
|
||||
|
||||
export class SwitchTemplateInputNamesWalker extends ComponentWalker {
|
||||
visitInlineTemplate(template: ts.StringLiteral) {
|
||||
this.replaceNamesInTemplate(template, template.getText()).forEach(replacement => {
|
||||
const fix = replacement.replacement;
|
||||
const ruleFailure = new RuleFailure(template.getSourceFile(), fix.start, fix.end,
|
||||
replacement.message, this.getRuleName(), fix);
|
||||
this.addFailure(ruleFailure);
|
||||
});
|
||||
}
|
||||
|
||||
visitExternalTemplate(template: ExternalResource) {
|
||||
this.replaceNamesInTemplate(template, template.getFullText()).forEach(replacement => {
|
||||
const fix = replacement.replacement;
|
||||
const ruleFailure = new RuleFailure(template, fix.start + 1, fix.end + 1,
|
||||
replacement.message, this.getRuleName(), fix);
|
||||
this.addFailure(ruleFailure);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the outdated name in the template with the new one and returns an updated template.
|
||||
*/
|
||||
private replaceNamesInTemplate(node: ts.Node, templateContent: string):
|
||||
{message: string, replacement: Replacement}[] {
|
||||
const replacements: {message: string, replacement: Replacement}[] = [];
|
||||
|
||||
inputNames.forEach(name => {
|
||||
let offsets: number[] = [];
|
||||
if (name.whitelist && name.whitelist.attributes && name.whitelist.attributes.length) {
|
||||
offsets = offsets.concat(findInputsOnElementWithAttr(
|
||||
templateContent, name.replace, name.whitelist.attributes));
|
||||
}
|
||||
if (name.whitelist && name.whitelist.elements && name.whitelist.elements.length) {
|
||||
offsets = offsets.concat(findInputsOnElementWithTag(
|
||||
templateContent, name.replace, name.whitelist.elements));
|
||||
}
|
||||
if (!name.whitelist) {
|
||||
offsets = offsets.concat(findAllSubstringIndices(templateContent, name.replace));
|
||||
}
|
||||
this.createReplacementsForOffsets(node, name, offsets).forEach(replacement => {
|
||||
replacements.push({
|
||||
message: `Found deprecated @Input() "${red(name.replace)}" which has been renamed to` +
|
||||
` "${green(name.replaceWith)}"`,
|
||||
replacement
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return replacements;
|
||||
}
|
||||
|
||||
private createReplacementsForOffsets(node: ts.Node,
|
||||
update: {replace: string, replaceWith: string},
|
||||
offsets: number[]): Replacement[] {
|
||||
return offsets.map(offset => this.createReplacement(
|
||||
node.getStart() + offset, update.replace.length, update.replaceWith));
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 {green, red} from 'chalk';
|
||||
import {Replacement, RuleFailure, Rules} from 'tslint';
|
||||
import * as ts from 'typescript';
|
||||
import {findOutputsOnElementWithAttr, findOutputsOnElementWithTag} from '../html/angular';
|
||||
import {outputNames} from '../material/data/output-names';
|
||||
import {ExternalResource} from '../tslint/component-file';
|
||||
import {ComponentWalker} from '../tslint/component-walker';
|
||||
import {findAllSubstringIndices} from '../typescript/literal';
|
||||
|
||||
/**
|
||||
* Rule that walks through every component decorator and updates their inline or external
|
||||
* templates.
|
||||
*/
|
||||
export class Rule extends Rules.AbstractRule {
|
||||
apply(sourceFile: ts.SourceFile): RuleFailure[] {
|
||||
return this.applyWithWalker(new SwitchTemplateOutputNamesWalker(sourceFile, this.getOptions()));
|
||||
}
|
||||
}
|
||||
|
||||
export class SwitchTemplateOutputNamesWalker extends ComponentWalker {
|
||||
visitInlineTemplate(template: ts.StringLiteral) {
|
||||
this.replaceNamesInTemplate(template, template.getText()).forEach(replacement => {
|
||||
const fix = replacement.replacement;
|
||||
const ruleFailure = new RuleFailure(template.getSourceFile(), fix.start, fix.end,
|
||||
replacement.message, this.getRuleName(), fix);
|
||||
this.addFailure(ruleFailure);
|
||||
});
|
||||
}
|
||||
|
||||
visitExternalTemplate(template: ExternalResource) {
|
||||
this.replaceNamesInTemplate(template, template.getFullText()).forEach(replacement => {
|
||||
const fix = replacement.replacement;
|
||||
const ruleFailure = new RuleFailure(template, fix.start + 1, fix.end + 1,
|
||||
replacement.message, this.getRuleName(), fix);
|
||||
this.addFailure(ruleFailure);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the outdated name in the template with the new one and returns an updated template.
|
||||
*/
|
||||
private replaceNamesInTemplate(node: ts.Node, templateContent: string):
|
||||
{message: string, replacement: Replacement}[] {
|
||||
const replacements: {message: string, replacement: Replacement}[] = [];
|
||||
|
||||
outputNames.forEach(name => {
|
||||
let offsets: number[] = [];
|
||||
if (name.whitelist && name.whitelist.attributes && name.whitelist.attributes.length) {
|
||||
offsets = offsets.concat(findOutputsOnElementWithAttr(
|
||||
templateContent, name.replace, name.whitelist.attributes));
|
||||
}
|
||||
if (name.whitelist && name.whitelist.elements && name.whitelist.elements.length) {
|
||||
offsets = offsets.concat(findOutputsOnElementWithTag(
|
||||
templateContent, name.replace, name.whitelist.elements));
|
||||
}
|
||||
if (!name.whitelist) {
|
||||
offsets = offsets.concat(findAllSubstringIndices(templateContent, name.replace));
|
||||
}
|
||||
this.createReplacementsForOffsets(node, name, offsets).forEach(replacement => {
|
||||
replacements.push({
|
||||
message: `Found deprecated @Output() "${red(name.replace)}" which has been renamed to` +
|
||||
` "${green(name.replaceWith)}"`,
|
||||
replacement
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return replacements;
|
||||
}
|
||||
|
||||
private createReplacementsForOffsets(node: ts.Node,
|
||||
update: {replace: string, replaceWith: string},
|
||||
offsets: number[]): Replacement[] {
|
||||
return offsets.map(offset => this.createReplacement(
|
||||
node.getStart() + offset, update.replace.length, update.replaceWith));
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,11 @@ describe('test cases', () => {
|
||||
*/
|
||||
const testCases = [
|
||||
'v5/attribute-selectors',
|
||||
'v5/class-names',
|
||||
'v5/css-names',
|
||||
'v5/element-selectors',
|
||||
'v5/ts-class-names',
|
||||
'v5/input-names',
|
||||
'v5/output-names',
|
||||
];
|
||||
|
||||
// Iterates through every test case directory and generates a jasmine test block that will
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<ng-template cdkConnectedOverlay
|
||||
[cdkConnectedOverlayOrigin]="myOrigin"
|
||||
[cdkConnectedOverlayPositions]="myPositions"
|
||||
[cdkConnectedOverlayOffsetX]="myOffsetX"
|
||||
[cdkConnectedOverlayOffsetY]="myOffsetY"
|
||||
[cdkConnectedOverlayWidth]="myWidth"
|
||||
[cdkConnectedOverlayHeight]="myHeight"
|
||||
[cdkConnectedOverlayMinWidth]="myMinWidth"
|
||||
[cdkConnectedOverlayMinHeight]="myMinHeight"
|
||||
[cdkConnectedOverlayBackdropClass]="myBackdropClass"
|
||||
[cdkConnectedOverlayScrollStrategy]="myScrollStrategy"
|
||||
[cdkConnectedOverlayOpen]="isOpen"
|
||||
[cdkConnectedOverlayHasBackdrop]="hasBackdrop">
|
||||
</ng-template>
|
||||
`
|
||||
})
|
||||
class A {}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<mat-radio-group labelPosition="end"></mat-radio-group>
|
||||
<mat-radio-group [labelPosition]="myAlign">
|
||||
<mat-radio-button [labelPosition]="myAlign"></mat-radio-button>
|
||||
<mat-radio-button labelPosition="start"></mat-radio-button>
|
||||
</mat-radio-group>
|
||||
`
|
||||
})
|
||||
class B {}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<mat-drawer position="end"></mat-drawer>
|
||||
<mat-drawer [position]="myAlign"></mat-drawer>
|
||||
<mat-sidenav [position]="myAlign"></mat-sidenav>
|
||||
`
|
||||
})
|
||||
class C {}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<mat-form-field color="primary"></mat-form-field>
|
||||
<mat-form-field [color]="myColor"></mat-form-field>
|
||||
<mat-form-field floatLabel="always"></mat-form-field>
|
||||
<mat-form-field [floatLabel]="floatState"></mat-form-field>
|
||||
`
|
||||
})
|
||||
class D {}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<mat-tab-group [dynamicHeight]="myHeight"></mat-tab-group>
|
||||
<mat-checkbox labelPosition="end"></mat-checkbox>
|
||||
<div matTooltip [matTooltipPosition]="end"></div>
|
||||
<mat-slider [tickInterval]="interval"></mat-slider>
|
||||
<mat-slider [thumbLabel]="myLabel"></mat-slider>
|
||||
`
|
||||
})
|
||||
class E {}
|
||||
@@ -0,0 +1,62 @@
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<ng-template cdkConnectedOverlay
|
||||
[origin]="myOrigin"
|
||||
[positions]="myPositions"
|
||||
[offsetX]="myOffsetX"
|
||||
[offsetY]="myOffsetY"
|
||||
[width]="myWidth"
|
||||
[height]="myHeight"
|
||||
[minWidth]="myMinWidth"
|
||||
[minHeight]="myMinHeight"
|
||||
[backdropClass]="myBackdropClass"
|
||||
[scrollStrategy]="myScrollStrategy"
|
||||
[open]="isOpen"
|
||||
[hasBackdrop]="hasBackdrop">
|
||||
</ng-template>
|
||||
`
|
||||
})
|
||||
class A {}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<mat-radio-group align="end"></mat-radio-group>
|
||||
<mat-radio-group [align]="myAlign">
|
||||
<mat-radio-button [align]="myAlign"></mat-radio-button>
|
||||
<mat-radio-button align="start"></mat-radio-button>
|
||||
</mat-radio-group>
|
||||
`
|
||||
})
|
||||
class B {}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<mat-drawer align="end"></mat-drawer>
|
||||
<mat-drawer [align]="myAlign"></mat-drawer>
|
||||
<mat-sidenav [align]="myAlign"></mat-sidenav>
|
||||
`
|
||||
})
|
||||
class C {}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<mat-form-field dividerColor="primary"></mat-form-field>
|
||||
<mat-form-field [dividerColor]="myColor"></mat-form-field>
|
||||
<mat-form-field floatPlaceholder="always"></mat-form-field>
|
||||
<mat-form-field [floatPlaceholder]="floatState"></mat-form-field>
|
||||
`
|
||||
})
|
||||
class D {}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<mat-tab-group [mat-dynamic-height]="myHeight"></mat-tab-group>
|
||||
<mat-checkbox align="end"></mat-checkbox>
|
||||
<div matTooltip [tooltip-position]="end"></div>
|
||||
<mat-slider [tick-interval]="interval"></mat-slider>
|
||||
<mat-slider [thumb-label]="myLabel"></mat-slider>
|
||||
`
|
||||
})
|
||||
class E {}
|
||||
@@ -0,0 +1,29 @@
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<mat-select (selectionChange)="onChange($event)"></mat-select>
|
||||
<mat-select (closed)="onClose($event)"></mat-select>
|
||||
<mat-select (opened)="onOpen($event)"></mat-select>
|
||||
`
|
||||
})
|
||||
class A {}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<mat-drawer (positionChanged)="onAlignChanged()"></mat-drawer>
|
||||
<mat-drawer (closed)="onClose()" (opened)="onOpen()"></mat-drawer>
|
||||
<mat-tab-group (selectedTabChange)="onSelectionChange()"></mat-tab-group>
|
||||
`
|
||||
})
|
||||
class B {}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<mat-chip (removed)="removeFromList()"></mat-chip>
|
||||
<mat-basic-chip (removed)="removeFromList()"></mat-basic-chip>
|
||||
<mat-chip (destroyed)="onDestroy()"></mat-chip>
|
||||
<mat-basic-chip (destroyed)="onDestroy()"></mat-basic-chip>
|
||||
`
|
||||
})
|
||||
class C {}
|
||||
@@ -0,0 +1,29 @@
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<mat-select (change)="onChange($event)"></mat-select>
|
||||
<mat-select (onClose)="onClose($event)"></mat-select>
|
||||
<mat-select (onOpen)="onOpen($event)"></mat-select>
|
||||
`
|
||||
})
|
||||
class A {}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<mat-drawer (align-changed)="onAlignChanged()"></mat-drawer>
|
||||
<mat-drawer (close)="onClose()" (open)="onOpen()"></mat-drawer>
|
||||
<mat-tab-group (selectChange)="onSelectionChange()"></mat-tab-group>
|
||||
`
|
||||
})
|
||||
class B {}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<mat-chip (remove)="removeFromList()"></mat-chip>
|
||||
<mat-basic-chip (remove)="removeFromList()"></mat-basic-chip>
|
||||
<mat-chip (destroy)="onDestroy()"></mat-chip>
|
||||
<mat-basic-chip (destroy)="onDestroy()"></mat-basic-chip>
|
||||
`
|
||||
})
|
||||
class C {}
|
||||
@@ -31,15 +31,13 @@ export default function(): Rule {
|
||||
path.join(__dirname, 'rules/attribute-selectors'),
|
||||
path.join(__dirname, 'rules/class-names'),
|
||||
path.join(__dirname, 'rules/css-names'),
|
||||
path.join(__dirname, 'rules/input-names'),
|
||||
path.join(__dirname, 'rules/output-names'),
|
||||
],
|
||||
rules: {
|
||||
// Automatic fixes.
|
||||
'switch-property-names': true,
|
||||
'switch-stylesheet-input-names': true,
|
||||
'switch-stylesheet-output-names': true,
|
||||
'switch-template-export-as-names': true,
|
||||
'switch-template-input-names': true,
|
||||
'switch-template-output-names': true,
|
||||
|
||||
// Attribute selector update rules.
|
||||
'attribute-selectors-string-literal': true,
|
||||
@@ -60,6 +58,13 @@ export default function(): Rule {
|
||||
'element-selectors-stylesheet': true,
|
||||
'element-selectors-template': true,
|
||||
|
||||
// Input name update rules
|
||||
'input-names-stylesheet': true,
|
||||
'input-names-template': true,
|
||||
|
||||
// Output name update rules
|
||||
'output-names-template': true,
|
||||
|
||||
// Additional issues we can detect but not automatically fix.
|
||||
'check-class-declaration-misc': true,
|
||||
'check-import-misc': true,
|
||||
|
||||
Reference in New Issue
Block a user