Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/angular/metadata.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading