UNPKG

ipsos-components

Version:

Material Design components for Angular

120 lines (101 loc) 4.25 kB
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; } }