Skip to content
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

build: add tslint rule for validating decorators #6800

Merged
merged 1 commit into from Sep 6, 2017
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/cdk/table/row.ts
Expand Up @@ -141,6 +141,7 @@ export class CdkCellOutlet {

/** Header template container that contains the cell outlet. Adds the right class and role. */
@Component({
moduleId: module.id,
selector: 'cdk-header-row',
template: CDK_ROW_TEMPLATE,
host: {
Expand All @@ -154,6 +155,7 @@ export class CdkHeaderRow { }

/** Data row template container that contains the cell outlet. Adds the right class and role. */
@Component({
moduleId: module.id,
selector: 'cdk-row',
template: CDK_ROW_TEMPLATE,
host: {
Expand Down
1 change: 1 addition & 0 deletions src/cdk/table/table.ts
Expand Up @@ -69,6 +69,7 @@ export const CDK_TABLE_TEMPLATE = `
* a header row and data rows. Updates the rows when new data is provided by the data source.
*/
@Component({
moduleId: module.id,
selector: 'cdk-table',
template: CDK_TABLE_TEMPLATE,
host: {
Expand Down
3 changes: 2 additions & 1 deletion src/cdk/table/tsconfig-build.json
@@ -1,7 +1,8 @@
{
"extends": "../tsconfig-build",
"files": [
"public_api.ts"
"public_api.ts",
"../typings.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
Expand Down
2 changes: 2 additions & 0 deletions src/lib/table/row.ts
Expand Up @@ -61,6 +61,7 @@ export class MatRowDef extends _MdCdkRowDef { }

/** Header template container that contains the cell outlet. Adds the right class and role. */
@Component({
moduleId: module.id,
selector: 'md-header-row, mat-header-row',
template: CDK_ROW_TEMPLATE,
host: {
Expand All @@ -74,6 +75,7 @@ export class MdHeaderRow extends _MdHeaderRow { }

/** Data row template container that contains the cell outlet. Adds the right class and role. */
@Component({
moduleId: module.id,
selector: 'md-row, mat-row',
template: CDK_ROW_TEMPLATE,
host: {
Expand Down
119 changes: 119 additions & 0 deletions tools/tslint-rules/validateDecoratorsRule.ts
@@ -0,0 +1,119 @@
import * as path from 'path';
import * as ts from 'typescript';
import * as Lint from 'tslint';
import * as minimatch from 'minimatch';

/**
* Rule that enforces certain decorator properties to be defined and to match a pattern.
* Supports whitelisting via the third argument. E.g.
*
* ```
* "validate-decorators": [true, {
* "Component": {
* "encapsulation": "\\.None$"
* }
* }, "src/lib"]
* ```
*/
export class Rule extends Lint.Rules.AbstractRule {
apply(sourceFile: ts.SourceFile) {
return this.applyWithWalker(new Walker(sourceFile, this.getOptions()));
}
}

/** Rules that can be used to validate the decorators in a file. */
type DecoratorRules = {[key: string]: {[key: string]: RegExp}};

class Walker extends Lint.RuleWalker {
// Whether the file should be checked at all.
private _enabled: boolean;

// Rules that will be used to validate the decorators.
private _rules: DecoratorRules;

constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) {
super(sourceFile, options);

// Globs that are used to determine which files to lint.
const fileGlobs = options.ruleArguments.slice(1) || [];

// Relative path for the current TypeScript source file.
const relativeFilePath = path.relative(process.cwd(), sourceFile.fileName);

this._rules = this._generateRules(options.ruleArguments[0]);
this._enabled = Object.keys(this._rules).length > 0 &&
fileGlobs.some(p => minimatch(relativeFilePath, p));
}

visitClassDeclaration(node: ts.ClassDeclaration) {
if (this._enabled && node.decorators) {
node.decorators
.map(decorator => decorator.expression as any)
.filter(expression => expression.arguments.length && expression.arguments[0].properties)
.forEach(expression => this._validatedDecorator(expression));
}

super.visitClassDeclaration(node);
}

/**
* Validates that a decorator matches all of the defined rules.
* @param decorator Decorator to be checked.
*/
private _validatedDecorator(decorator: any) {
// Get the rules that are relevant for the current decorator.
const rules = this._rules[decorator.expression.getText()];

// Don't do anything if there are no rules.
if (!rules) {
return;
}

// Extract the property names and values.
const props = decorator.arguments[0].properties.map((node: ts.PropertyAssignment) => ({
name: node.name.getText(),
value: node.initializer.getText(),
node
}));

// Find all of the rule properties that are missing from the decorator.
const missing = Object.keys(rules).filter(key => !props.find((prop: any) => prop.name === key));

if (missing.length) {
// Exit early if any of the properties are missing.
this.addFailureAtNode(decorator.parent, 'Missing required properties: ' + missing.join(', '));
} else {
// If all the necessary properties are defined, ensure that they match the pattern.
props
.filter((prop: any) => rules[prop.name])
.filter((prop: any) => !rules[prop.name].test(prop.value))
.forEach((prop: any) => {
this.addFailureAtNode(prop.node,
`Invalid value for property. Expected value to match "${rules[prop.name]}".`);
});
}
}

/**
* Cleans out the blank rules that are passed through the tslint.json
* and converts the string patterns into regular expressions.
* @param config Config object passed in via the tslint.json.
* @returns Sanitized rules.
*/
private _generateRules(config: {[key: string]: {[key: string]: string}}): DecoratorRules {
const output: DecoratorRules = {};

if (config) {
Object.keys(config)
.filter(decoratorName => Object.keys(config[decoratorName]).length > 0)
.forEach(decoratorName => {
output[decoratorName] = Object.keys(config[decoratorName]).reduce((accumulator, prop) => {
accumulator[prop] = new RegExp(config[decoratorName][prop]);
return accumulator;
}, {} as {[key: string]: RegExp});
});
}

return output;
}
}
10 changes: 6 additions & 4 deletions tslint.json
Expand Up @@ -87,10 +87,12 @@
// Custom Rules
"ts-loader": true,
"no-exposed-todo": true,
"no-view-encapsulation": [
true,
"src/+(lib|cdk)/**/!(*.spec).ts"
],
"validate-decorators": [true, {
"Component": {
"encapsulation": "\\.None$",
"moduleId": "^module\\.id$"
}
}, "src/+(lib|cdk)/**/!(*.spec).ts"],
"require-license-banner": [
true,
"src/+(lib|cdk)/**/!(*.spec).ts"
Expand Down