Skip to content

Commit 1840008

Browse files
devversionjelbourn
authored andcommitted
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.
1 parent 3723191 commit 1840008

18 files changed

+458
-371
lines changed

src/lib/schematics/update/html/angular.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,26 @@ import {findAttributeOnElementWithAttrs, findAttributeOnElementWithTag} from './
1010

1111
/** Finds the specified Angular @Input in the given elements with tag name. */
1212
export function findInputsOnElementWithTag(html: string, inputName: string, tagNames: string[]) {
13-
// Add one column to the mapped offset because the first bracket for the @Input
14-
// is part of the attribute and therefore also part of the offset. We only want to return
15-
// the offset for the inner name of the input.
16-
return findAttributeOnElementWithTag(html, `[${inputName}]`, tagNames).map(offset => offset + 1);
13+
return [
14+
// Inputs can be also used without brackets (e.g. `<mat-toolbar color="primary">`)
15+
...findAttributeOnElementWithTag(html, inputName, tagNames),
16+
// Add one column to the mapped offset because the first bracket for the @Input
17+
// is part of the attribute and therefore also part of the offset. We only want to return
18+
// the offset for the inner name of the bracketed input.
19+
...findAttributeOnElementWithTag(html, `[${inputName}]`, tagNames).map(offset => offset + 1),
20+
];
21+
}
22+
23+
/** Finds the specified Angular @Input in elements that have one of the specified attributes. */
24+
export function findInputsOnElementWithAttr(html: string, inputName: string, attrs: string[]) {
25+
return [
26+
// Inputs can be also used without brackets (e.g. `<button mat-button color="primary">`)
27+
...findAttributeOnElementWithAttrs(html, inputName, attrs),
28+
// Add one column to the mapped offset because the first bracket for the @Input
29+
// is part of the attribute and therefore also part of the offset. We only want to return
30+
// the offset for the inner name of the bracketed input.
31+
...findAttributeOnElementWithAttrs(html, `[${inputName}]`, attrs).map(offset => offset + 1),
32+
];
1733
}
1834

1935
/** Finds the specified Angular @Output in the given elements with tag name. */
@@ -24,14 +40,6 @@ export function findOutputsOnElementWithTag(html: string, outputName: string, ta
2440
return findAttributeOnElementWithTag(html, `(${outputName})`, tagNames).map(offset => offset + 1);
2541
}
2642

27-
/** Finds the specified Angular @Input in elements that have one of the specified attributes. */
28-
export function findInputsOnElementWithAttr(html: string, inputName: string, attrs: string[]) {
29-
// Add one column to the mapped offset because the first bracket for the @Input
30-
// is part of the attribute and therefore also part of the offset. We only want to return
31-
// the offset for the inner name of the input.
32-
return findAttributeOnElementWithAttrs(html, `[${inputName}]`, attrs).map(offset => offset + 1);
33-
}
34-
3543
/** Finds the specified Angular @Output in elements that have one of the specified attributes. */
3644
export function findOutputsOnElementWithAttr(html: string, outputName: string, attrs: string[]) {
3745
// Add one column to the mapped offset because the first bracket for the @Output

src/lib/schematics/update/material/data/input-names.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@ export interface MaterialInputNameData {
1919
elements?: string[],
2020
/** Limit to elements with any of these attributes. */
2121
attributes?: string[],
22-
/** Whether to ignore CSS attribute selectors when doing this replacement. */
23-
css?: boolean,
22+
/**
23+
* Whether inputs in stylesheets should be updated or not. Note that inputs inside of
24+
* stylesheets usually don't make sense, but if developers use an input as a plain one-time
25+
* attribute, it can be targeted through CSS selectors.
26+
*/
27+
stylesheet?: boolean,
2428
};
2529
}
2630

src/lib/schematics/update/material/data/output-names.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ export interface MaterialOutputNameData {
1919
elements?: string[],
2020
/** Limit to elements with any of these attributes. */
2121
attributes?: string[],
22-
/** Whether to ignore CSS attribute selectors when doing this replacement. */
23-
css?: boolean,
2422
};
2523
}
2624

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {green, red} from 'chalk';
10+
import {sync as globSync} from 'glob';
11+
import {IOptions, Replacement, RuleFailure, Rules} from 'tslint';
12+
import * as ts from 'typescript';
13+
import {inputNames} from '../../material/data/input-names';
14+
import {ExternalResource} from '../../tslint/component-file';
15+
import {ComponentWalker} from '../../tslint/component-walker';
16+
import {
17+
addFailureAtReplacement,
18+
createExternalReplacementFailure,
19+
} from '../../tslint/rule-failures';
20+
import {findAllSubstringIndices} from '../../typescript/literal';
21+
22+
/**
23+
* Rule that walks through every inline or external stylesheet and replaces outdated CSS selectors
24+
* that query for an @Input() with the new input name.
25+
*
26+
* Note that inputs inside of stylesheets usually don't make sense, but if developers use an
27+
* input as a plain one-time attribute, it can be targeted through CSS selectors.
28+
*
29+
* e.g. `<my-component color="primary">` becomes `my-component[color]`
30+
*/
31+
export class Rule extends Rules.AbstractRule {
32+
apply(sourceFile: ts.SourceFile): RuleFailure[] {
33+
return this.applyWithWalker(new Walker(sourceFile, this.getOptions()));
34+
}
35+
}
36+
37+
export class Walker extends ComponentWalker {
38+
39+
constructor(sourceFile: ts.SourceFile, options: IOptions) {
40+
// In some applications, developers will have global stylesheets that are not specified in any
41+
// Angular component. Therefore we glob up all css and scss files outside of node_modules and
42+
// dist and check them as well.
43+
const extraFiles = globSync('!(node_modules|dist)/**/*.+(css|scss)');
44+
super(sourceFile, options, extraFiles);
45+
extraFiles.forEach(styleUrl => this._reportExternalStyle(styleUrl));
46+
}
47+
48+
visitInlineStylesheet(literal: ts.StringLiteral) {
49+
this._createReplacementsForContent(literal, literal.getText())
50+
.forEach(data => addFailureAtReplacement(this, data.failureMessage, data.replacement));
51+
}
52+
53+
visitExternalStylesheet(node: ExternalResource) {
54+
this._createReplacementsForContent(node, node.getFullText())
55+
.map(data => createExternalReplacementFailure(node, data.failureMessage,
56+
this.getRuleName(), data.replacement))
57+
.forEach(failure => this.addFailure(failure));
58+
}
59+
60+
/**
61+
* Searches for outdated attribute selectors in the specified content and creates replacements
62+
* with the according messages that can be added to a rule failure.
63+
*/
64+
private _createReplacementsForContent(node: ts.Node, stylesheetContent: string) {
65+
const replacements: {failureMessage: string, replacement: Replacement}[] = [];
66+
67+
inputNames.forEach(name => {
68+
if (name.whitelist && !name.whitelist.stylesheet) {
69+
return;
70+
}
71+
72+
const currentSelector = `[${name.replace}]`;
73+
const updatedSelector = `[${name.replaceWith}]`;
74+
75+
const failureMessage = `Found deprecated @Input() CSS selector "${red(currentSelector)}" ` +
76+
`which has been renamed to "${green(updatedSelector)}"`;
77+
78+
findAllSubstringIndices(stylesheetContent, currentSelector)
79+
.map(offset => node.getStart() + offset)
80+
.map(start => new Replacement(start, currentSelector.length, updatedSelector))
81+
.forEach(replacement => replacements.push({replacement, failureMessage}));
82+
});
83+
84+
return replacements;
85+
}
86+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {green, red} from 'chalk';
10+
import {Replacement, RuleFailure, Rules} from 'tslint';
11+
import * as ts from 'typescript';
12+
import {findInputsOnElementWithAttr, findInputsOnElementWithTag} from '../../html/angular';
13+
import {inputNames} from '../../material/data/input-names';
14+
import {ExternalResource} from '../../tslint/component-file';
15+
import {ComponentWalker} from '../../tslint/component-walker';
16+
import {
17+
addFailureAtReplacement,
18+
createExternalReplacementFailure,
19+
} from '../../tslint/rule-failures';
20+
21+
/**
22+
* Rule that walks through every inline or external HTML template and switches changed input
23+
* bindings to the proper new name.
24+
*/
25+
export class Rule extends Rules.AbstractRule {
26+
apply(sourceFile: ts.SourceFile): RuleFailure[] {
27+
return this.applyWithWalker(new Walker(sourceFile, this.getOptions()));
28+
}
29+
}
30+
31+
export class Walker extends ComponentWalker {
32+
33+
visitInlineTemplate(template: ts.StringLiteral) {
34+
this._createReplacementsForContent(template, template.getText())
35+
.forEach(data => addFailureAtReplacement(this, data.failureMessage, data.replacement));
36+
}
37+
38+
visitExternalTemplate(template: ExternalResource) {
39+
this._createReplacementsForContent(template, template.getFullText())
40+
.map(data => createExternalReplacementFailure(template, data.failureMessage,
41+
this.getRuleName(), data.replacement))
42+
.forEach(failure => this.addFailure(failure));
43+
}
44+
45+
/**
46+
* Searches for outdated input bindings in the specified content and creates
47+
* replacements with the according messages that can be added to a rule failure.
48+
*/
49+
private _createReplacementsForContent(node: ts.Node, templateContent: string) {
50+
const replacements: {failureMessage: string, replacement: Replacement}[] = [];
51+
52+
inputNames.forEach(name => {
53+
const whitelist = name.whitelist;
54+
const relativeOffsets = [];
55+
const failureMessage = `Found deprecated @Input() "${red(name.replace)}"` +
56+
` which has been renamed to "${green(name.replaceWith)}"`;
57+
58+
if (!whitelist || whitelist.attributes) {
59+
relativeOffsets.push(
60+
...findInputsOnElementWithAttr(templateContent, name.replace, whitelist.attributes));
61+
}
62+
63+
if (!whitelist || whitelist.elements) {
64+
relativeOffsets.push(
65+
...findInputsOnElementWithTag(templateContent, name.replace, whitelist.elements));
66+
}
67+
68+
relativeOffsets
69+
.map(offset => node.getStart() + offset)
70+
.map(start => new Replacement(start, name.replace.length, name.replaceWith))
71+
.forEach(replacement => replacements.push({replacement, failureMessage}));
72+
});
73+
74+
return replacements;
75+
}
76+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {green, red} from 'chalk';
10+
import {Replacement, RuleFailure, Rules} from 'tslint';
11+
import * as ts from 'typescript';
12+
import {findOutputsOnElementWithAttr, findOutputsOnElementWithTag} from '../../html/angular';
13+
import {outputNames} from '../../material/data/output-names';
14+
import {ExternalResource} from '../../tslint/component-file';
15+
import {ComponentWalker} from '../../tslint/component-walker';
16+
import {
17+
addFailureAtReplacement,
18+
createExternalReplacementFailure,
19+
} from '../../tslint/rule-failures';
20+
21+
/**
22+
* Rule that walks through every inline or external HTML template and switches changed output
23+
* bindings to the proper new output name.
24+
*/
25+
export class Rule extends Rules.AbstractRule {
26+
apply(sourceFile: ts.SourceFile): RuleFailure[] {
27+
return this.applyWithWalker(new Walker(sourceFile, this.getOptions()));
28+
}
29+
}
30+
31+
export class Walker extends ComponentWalker {
32+
33+
visitInlineTemplate(template: ts.StringLiteral) {
34+
this._createReplacementsForContent(template, template.getText())
35+
.forEach(data => addFailureAtReplacement(this, data.failureMessage, data.replacement));
36+
}
37+
38+
visitExternalTemplate(template: ExternalResource) {
39+
this._createReplacementsForContent(template, template.getFullText())
40+
.map(data => createExternalReplacementFailure(template, data.failureMessage,
41+
this.getRuleName(), data.replacement))
42+
.forEach(failure => this.addFailure(failure));
43+
}
44+
45+
/**
46+
* Searches for outdated output bindings in the specified content and creates
47+
* replacements with the according messages that can be added to a rule failure.
48+
*/
49+
private _createReplacementsForContent(node: ts.Node, templateContent: string) {
50+
const replacements: {failureMessage: string, replacement: Replacement}[] = [];
51+
52+
outputNames.forEach(name => {
53+
const whitelist = name.whitelist;
54+
const relativeOffsets = [];
55+
const failureMessage = `Found deprecated @Output() "${red(name.replace)}"` +
56+
` which has been renamed to "${green(name.replaceWith)}"`;
57+
58+
if (!whitelist || whitelist.attributes) {
59+
relativeOffsets.push(
60+
...findOutputsOnElementWithAttr(templateContent, name.replace, whitelist.attributes));
61+
}
62+
63+
if (!whitelist || whitelist.elements) {
64+
relativeOffsets.push(
65+
...findOutputsOnElementWithTag(templateContent, name.replace, whitelist.elements));
66+
}
67+
68+
relativeOffsets
69+
.map(offset => node.getStart() + offset)
70+
.map(start => new Replacement(start, name.replace.length, name.replaceWith))
71+
.forEach(replacement => replacements.push({replacement, failureMessage}));
72+
});
73+
74+
return replacements;
75+
}
76+
}

0 commit comments

Comments
 (0)