Skip to content

Commit

Permalink
feat(rule): add option in max-inline-declarations to limit animations…
Browse files Browse the repository at this point in the history
… lines (#569)
  • Loading branch information
rafaelss95 authored and mgechev committed Jun 7, 2018
1 parent dfe8c76 commit 25f3e16
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 60 deletions.
7 changes: 6 additions & 1 deletion src/angular/metadata.ts
@@ -1,5 +1,5 @@
import * as ts from 'typescript';
import { RawSourceMap } from 'source-map';
import * as ts from 'typescript';

export interface CodeWithSourceMap {
code: string;
Expand All @@ -12,6 +12,10 @@ interface PropertyMetadata {
url?: string;
}

export interface AnimationMetadata extends PropertyMetadata {
animation: CodeWithSourceMap;
}

export interface StyleMetadata extends PropertyMetadata {
style: CodeWithSourceMap;
}
Expand All @@ -27,6 +31,7 @@ export class DirectiveMetadata {
}

export class ComponentMetadata extends DirectiveMetadata {
animations!: AnimationMetadata[];
styles!: StyleMetadata[];
template!: TemplateMetadata;
}
17 changes: 15 additions & 2 deletions src/angular/metadataReader.ts
Expand Up @@ -9,11 +9,11 @@ import {
} from '../util/astQuery';
import { ifTrue, listToMaybe, Maybe, unwrapFirst } from '../util/function';
import { logger } from '../util/logger';
import { getInlineStyle, getTemplate } from '../util/ngQuery';
import { getAnimations, getInlineStyle, getTemplate } from '../util/ngQuery';
import { maybeNodeArray } from '../util/utils';
import { Config } from './config';
import { FileResolver } from './fileResolver/fileResolver';
import { CodeWithSourceMap, ComponentMetadata, DirectiveMetadata, StyleMetadata, TemplateMetadata } from './metadata';
import { AnimationMetadata, CodeWithSourceMap, ComponentMetadata, DirectiveMetadata, StyleMetadata, TemplateMetadata } from './metadata';
import { AbstractResolver, MetadataUrls } from './urlResolvers/abstractResolver';
import { PathResolver } from './urlResolvers/pathResolver';
import { UrlResolver } from './urlResolvers/urlResolver';
Expand Down Expand Up @@ -71,10 +71,12 @@ export class MetadataReader {
const expr = this.getDecoratorArgument(dec);
const directiveMetadata = this.readDirectiveMetadata(d, dec);
const external_M = expr.fmap(() => this._urlResolver!.resolve(dec));
const animations_M = external_M.bind(external => this.readComponentAnimationsMetadata(dec, external!));
const style_M = external_M.bind(external => this.readComponentStylesMetadata(dec, external!));
const template_M = external_M.bind(external => this.readComponentTemplateMetadata(dec, external!));

return Object.assign(new ComponentMetadata(), directiveMetadata, {
animations: animations_M.unwrap(),
styles: style_M.unwrap(),
template: template_M.unwrap()
});
Expand All @@ -84,6 +86,17 @@ export class MetadataReader {
return decoratorArgument(decorator).bind(ifTrue(hasProperties));
}

protected readComponentAnimationsMetadata(dec: ts.Decorator, external: MetadataUrls): Maybe<AnimationMetadata[] | undefined> {
return getAnimations(dec).fmap(inlineAnimations =>
inlineAnimations!.elements
.filter(inlineAnimation => isSimpleTemplateString(inlineAnimation))
.map<AnimationMetadata>(inlineAnimation => ({
animation: normalizeTransformed({ code: (inlineAnimation as ts.StringLiteral | ts.NoSubstitutionTemplateLiteral).text }),
node: inlineAnimation as ts.Node
}))
);
}

protected readComponentTemplateMetadata(dec: ts.Decorator, external: MetadataUrls): Maybe<TemplateMetadata | undefined> {
// Resolve Inline template
return getTemplate(dec)
Expand Down
47 changes: 41 additions & 6 deletions src/maxInlineDeclarationsRule.ts
Expand Up @@ -4,19 +4,24 @@ import { SourceFile } from 'typescript/lib/typescript';
import { CodeWithSourceMap, ComponentMetadata } from './angular/metadata';
import { NgWalker } from './angular/ngWalker';

const DEFAULT_ANIMATIONS_LIMIT: number = 15;
const DEFAULT_STYLES_LIMIT: number = 3;
const DEFAULT_TEMPLATE_LIMIT: number = 3;
const OPTION_ANIMATIONS = 'animations';
const OPTION_STYLES = 'styles';
const OPTION_TEMPLATE = 'template';

export class Rule extends Rules.AbstractRule {
static readonly metadata: IRuleMetadata = {
description: 'Disallows having too many lines in inline template and styles. Forces separate template or styles file creation.',
descriptionDetails: 'See more at https://angular.io/guide/styleguide#style-05-04.',
optionExamples: [true, [true, { [OPTION_STYLES]: 8, [OPTION_TEMPLATE]: 5 }]],
optionExamples: [true, [true, { [OPTION_ANIMATIONS]: 20, [OPTION_STYLES]: 8, [OPTION_TEMPLATE]: 5 }]],
options: {
items: {
properties: {
[OPTION_ANIMATIONS]: {
type: 'number'
},
[OPTION_STYLES]: {
type: 'number'
},
Expand All @@ -31,7 +36,8 @@ export class Rule extends Rules.AbstractRule {
type: 'array'
},
optionsDescription: Utils.dedent`
It can take an optional object with the properties '${OPTION_STYLES}' and '${OPTION_TEMPLATE}':
It can take an optional object with the properties '${OPTION_ANIMATIONS}', '${OPTION_STYLES}' and '${OPTION_TEMPLATE}':
* \`${OPTION_ANIMATIONS}\` - number > 0 defining the maximum allowed inline lines for animations. Defaults to ${DEFAULT_ANIMATIONS_LIMIT}.
* \`${OPTION_STYLES}\` - number > 0 defining the maximum allowed inline lines for styles. Defaults to ${DEFAULT_STYLES_LIMIT}.
* \`${OPTION_TEMPLATE}\` - number > 0 defining the maximum allowed inline lines for template. Defaults to ${DEFAULT_TEMPLATE_LIMIT}.
`,
Expand Down Expand Up @@ -60,14 +66,17 @@ export class Rule extends Rules.AbstractRule {
}
}

type PropertyType = 'styles' | 'template';

type PropertyType = 'animations' | 'styles' | 'template';
export type PropertyPair = { [key in PropertyType]?: number };

const generateFailure = (type: PropertyType, limit: number, value: number): string => {
return sprintf(Rule.FAILURE_STRING, type, limit, value);
};

export const getAnimationsFailure = (value: number, limit = DEFAULT_ANIMATIONS_LIMIT): string => {
return generateFailure(OPTION_ANIMATIONS, limit, value);
};

export const getStylesFailure = (value: number, limit = DEFAULT_STYLES_LIMIT): string => {
return generateFailure(OPTION_STYLES, limit, value);
};
Expand All @@ -77,29 +86,54 @@ export const getTemplateFailure = (value: number, limit = DEFAULT_TEMPLATE_LIMIT
};

export class MaxInlineDeclarationsWalker extends NgWalker {
private readonly animationsLinesLimit = DEFAULT_ANIMATIONS_LIMIT;
private readonly stylesLinesLimit = DEFAULT_STYLES_LIMIT;
private readonly templateLinesLimit = DEFAULT_TEMPLATE_LIMIT;
private readonly newLineRegExp = /\r\n|\r|\n/;

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

const { styles = -1, template = -1 } = (options.ruleArguments[0] || []) as PropertyPair;
const { animations = -1, styles = -1, template = -1 } = (options.ruleArguments[0] || []) as PropertyPair;

this.animationsLinesLimit = animations > -1 ? animations : this.animationsLinesLimit;
this.stylesLinesLimit = styles > -1 ? styles : this.stylesLinesLimit;
this.templateLinesLimit = template > -1 ? template : this.templateLinesLimit;
}

protected visitNgComponent(metadata: ComponentMetadata): void {
this.validateInlineTemplate(metadata);
this.validateInlineAnimations(metadata);
this.validateInlineStyles(metadata);
this.validateInlineTemplate(metadata);
super.visitNgComponent(metadata);
}

private getLinesCount(source: CodeWithSourceMap['source']): number {
return source!.trim().split(this.newLineRegExp).length;
}

private getInlineAnimationsLinesCount(metadata: ComponentMetadata): number {
return (metadata.animations || []).reduce((previousValue, currentValue) => {
previousValue += this.getLinesCount(currentValue.animation.source);

return previousValue;
}, 0);
}

private validateInlineAnimations(metadata: ComponentMetadata): void {
const linesCount = this.getInlineAnimationsLinesCount(metadata);

if (linesCount <= this.animationsLinesLimit) {
return;
}

const failureMessage = getAnimationsFailure(linesCount, this.animationsLinesLimit);

for (const animation of metadata.animations) {
this.addFailureAtNode(animation.node!, failureMessage);
}
}

private getInlineStylesLinesCount(metadata: ComponentMetadata): number {
return (metadata.styles || []).reduce((previousValue, currentValue) => {
if (!currentValue.url) {
Expand All @@ -123,6 +157,7 @@ export class MaxInlineDeclarationsWalker extends NgWalker {
this.addFailureAtNode(style.node!, failureMessage);
}
}

private getTemplateLinesCount(metadata: ComponentMetadata): number {
return this.hasInlineTemplate(metadata) ? this.getLinesCount(metadata.template.template.source) : 0;
}
Expand Down
8 changes: 8 additions & 0 deletions src/util/ngQuery.ts
Expand Up @@ -2,6 +2,14 @@ import * as ts from 'typescript';
import { decoratorArgument, getInitializer, getStringInitializerFromProperty, isProperty, WithStringInitializer } from './astQuery';
import { Maybe } from './function';

export function getAnimations(dec: ts.Decorator): Maybe<ts.ArrayLiteralExpression | undefined> {
return decoratorArgument(dec).bind(expr => {
const property = expr!.properties.find(p => isProperty('animations', p))!;

return getInitializer(property).fmap(expr => (ts.isArrayLiteralExpression(expr!) ? (expr as ts.ArrayLiteralExpression) : undefined));
});
}

export function getInlineStyle(dec: ts.Decorator): Maybe<ts.ArrayLiteralExpression | undefined> {
return decoratorArgument(dec).bind(expr => {
const property = expr!.properties.find(p => isProperty('styles', p))!;
Expand Down

0 comments on commit 25f3e16

Please sign in to comment.