UNPKG

criticizer

Version:

Linting for Angular applications, following angular.io/styleguide.

239 lines (221 loc) 8.14 kB
import * as Lint from 'tslint'; import * as ts from 'typescript'; import {Ng2Walker} from './angular/ng2Walker'; import {getComponentDecorator, isSimpleTemplateString, getDecoratorPropertyInitializer} from './util/utils'; import {BasicCssAstVisitor} from './angular/styles/basicCssAstVisitor'; import {BasicTemplateAstVisitor} from './angular/templates/basicTemplateAstVisitor'; import { TemplateAst, ElementAst, PropertyBindingType } from '@angular/compiler'; import {parseTemplate} from './angular/templates/templateParser'; import {CssAst, CssSelectorRuleAst, CssSelectorAst} from './angular/styles/cssAst'; import {ComponentMetadata, StyleMetadata} from './angular/metadata'; import {ng2WalkerFactoryUtils} from './angular/ng2WalkerFactoryUtils'; import {logger} from './util/logger'; const CssSelectorTokenizer = require('css-selector-tokenizer'); const getSymbolName = (t: any) => { let expr = t.expression; if (t.expression && t.expression.name) { expr = t.expression.name; } return expr.text; }; const isEncapsulationEnabled = (encapsulation: any) => { if (!encapsulation) { return true; } else { // By default consider the encapsulation disabled if (getSymbolName(encapsulation) !== 'ViewEncapsulation') { return false; } else { const encapsulationType = encapsulation.name.text; if (/^(Emulated|Native)$/.test(encapsulationType)) { return true; } } } return false; }; // Initialize the selector accessors const lang = require('cssauron')({ tag(node: ElementAst) { return (node.name || '').toLowerCase(); }, // We do not support it for now contents(node: ElementAst) { return ''; }, id(node: ElementAst) { return this.attr(node, 'id'); }, 'class'(node: ElementAst) { const classBindings = (node.inputs || []) .filter(b => b.type === PropertyBindingType.Class) .map(b => b.name).join(' '); const classAttr = node.attrs.filter(a => a.name.toLowerCase() === 'class').pop(); let staticClasses = ''; if (classAttr) { staticClasses = classAttr.value + ' '; } return staticClasses + classBindings; }, parent(node: any) { return node.parentNode; }, children(node: ElementAst) { return node.children; }, attr(node: ElementAst, attr: string) { const targetAttr = node.attrs.filter(a => a.name === attr).pop(); if (targetAttr) { return targetAttr.value; } return undefined; } }); // Visitor which normalizes the elements and finds out if we have a match class ElementVisitor extends BasicTemplateAstVisitor { visitElement(ast: ElementAst, fn: any) { fn(ast); ast.children.forEach(c => { if (c instanceof ElementAst) { (<any>c).parentNode = ast; } this.visit(c, fn); }); } } // Finds out if selector of given type has been used const hasSelector = (s: any, type: string) => { if (!s) { return false; } if (s.type === 'selector' || s.type === 'selectors') { return (s.nodes || []).some(n => hasSelector(n, type)); } else { return s.type === type; } }; const dynamicFilters = { id(ast: ElementAst, selector: any) { return (ast.inputs || []).some(i => i.name === 'id'); }, attribute(ast: ElementAst, selector: any) { return (ast.inputs || []).some(i => i.type === PropertyBindingType.Attribute); }, 'class'(ast: ElementAst, selector: any) { return (ast.inputs || []).some(i => i.name === 'className' || i.name === 'ngClass'); } }; // Filters elements following the strategies: // - If has selector by id and any of the elements has a dynamically set id we just skip it. // - If has selector by class and any of the elements has a dynamically set class we just skip it. // - If has selector by attribute and any of the elements has a dynamically set attribute we just skip it. class ElementFilterVisitor extends BasicTemplateAstVisitor { shouldVisit(ast: ElementAst, strategies: any, selectorTypes: any): boolean { return Object.keys(strategies).every(s => { const strategy = strategies[s]; return !selectorTypes[s] || !strategy(ast); }) && (ast.children || []) .every(c => ast instanceof ElementAst && this.shouldVisit(<ElementAst>c, strategies, selectorTypes)); } } export class Rule extends Lint.Rules.AbstractRule { static FAILURE: string = 'The %s "%s" that you\'re trying to access does not exist in the class declaration.'; public apply(sourceFile:ts.SourceFile): Lint.RuleFailure[] { return this.applyWithWalker( new UnusedCssNg2Visitor(sourceFile, this.getOptions(), { cssVisitorCtrl: UnusedCssVisitor })); } } class UnusedCssVisitor extends BasicCssAstVisitor { templateAst: TemplateAst; visitCssSelectorRule(ast: CssSelectorRuleAst) { try { const match = ast.selectors.some(s => this.visitCssSelector(s)); if (!match) { this.addFailure(this.createFailure(ast.start.offset, ast.end.offset - ast.start.offset, 'Unused styles')); } } catch (e) { logger.error(e); } return true; } visitCssSelector(ast: CssSelectorAst) { const parts = []; for (let i = 0; i < ast.selectorParts.length; i += 1) { const c = ast.selectorParts[i]; c.strValue = c.strValue.split('::').shift(); // Stop on /deep/ and >>> if (c.strValue.endsWith('/') || c.strValue.endsWith('>')) { parts.push(c.strValue); break; } else if (!c.strValue.startsWith(':')) { // skip :host parts.push(c.strValue); } } if (!parts.length || !this.templateAst) { return true; } const strippedSelector = parts.map(s => s.replace(/\/|>$/, '').trim()).join(' '); const elementFilterVisitor = new ElementFilterVisitor(this.getSourceFile(), this._originalOptions, this.context, 0); const tokenized = CssSelectorTokenizer.parse(strippedSelector); const selectorTypesCache = Object.keys(dynamicFilters).reduce((a: any, key: string) => { a[key] = hasSelector(tokenized, key); return a; }, {}); if (!elementFilterVisitor.shouldVisit(<ElementAst>this.templateAst, dynamicFilters, selectorTypesCache)) { return true; } let matchFound = false; const selector = (element: ElementAst) => { if (lang(strippedSelector)(element)) { matchFound = true; return true; } return false; }; const visitor = new ElementVisitor(this.getSourceFile(), this._originalOptions, this.context, 0); visitor.visit(this.templateAst, selector); return matchFound; } } // Finds the template and wrapes the parsed content into a root element export class UnusedCssNg2Visitor extends Ng2Walker { private templateAst: TemplateAst; visitClassDeclaration(declaration: ts.ClassDeclaration) { const d = getComponentDecorator(declaration); if (d) { const meta: ComponentMetadata = <ComponentMetadata>this._metadataReader.read(declaration); this.visitNg2Component(meta); if (meta.template && meta.template.template) { try { this.templateAst = new ElementAst('*', [], [], [], [], [], [], false, parseTemplate(meta.template.template.code), 0, null, null); } catch (e) { logger.error('Cannot parse the template', e); } } } super.visitClassDeclaration(declaration); } protected visitNg2StyleHelper(style: CssAst, context: ComponentMetadata, styleMetadata: StyleMetadata, baseStart: number) { if (!style) { return; } else { const file = this.getContextSourceFile(styleMetadata.url, styleMetadata.style.source); const visitor = new UnusedCssVisitor(file, this._originalOptions, context, styleMetadata, baseStart); visitor.templateAst = this.templateAst; const d = getComponentDecorator(context.controller); const encapsulation = getDecoratorPropertyInitializer(d, 'encapsulation'); if (isEncapsulationEnabled(encapsulation)) { style.visit(visitor); visitor.getFailures().forEach(f => this.addFailure(f)); } } } }