Skip to content

refactor(schematics): cleanup input and output name rules #12800

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 20 additions & 12 deletions src/lib/schematics/update/html/angular.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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
Expand Down
8 changes: 6 additions & 2 deletions src/lib/schematics/update/material/data/input-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand Down
2 changes: 0 additions & 2 deletions src/lib/schematics/update/material/data/output-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading