ipsos-components
Version:
Material Design components for Angular
120 lines (101 loc) • 4.25 kB
text/typescript
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;
}
}