diff --git a/src/angular/metadataReader.ts b/src/angular/metadataReader.ts index 3c0a367b6..db0c5ee1c 100644 --- a/src/angular/metadataReader.ts +++ b/src/angular/metadataReader.ts @@ -1,162 +1,155 @@ import * as ts from 'typescript'; -import {current} from '../util/syntaxKind'; - import {FileResolver} from './fileResolver/fileResolver'; import {AbstractResolver, MetadataUrls} from './urlResolvers/abstractResolver'; import {UrlResolver} from './urlResolvers/urlResolver'; import {PathResolver} from './urlResolvers/pathResolver'; import {logger} from '../util/logger'; -import {isSimpleTemplateString, getDecoratorPropertyInitializer} from '../util/utils'; import {Config} from './config'; -import {DirectiveMetadata, ComponentMetadata, StylesMetadata, CodeWithSourceMap, TemplateMetadata} from './metadata'; - -const kinds = current(); +import {DirectiveMetadata, ComponentMetadata, CodeWithSourceMap, TemplateMetadata} from './metadata'; +import {Maybe, unwrapFirst, ifTrue,} from '../util/function'; +import { + callExpression, withIdentifier, hasProperties, + isSimpleTemplateString, getStringInitializerFromProperty, decoratorArgument +} from '../util/astQuery'; +import {getTemplate, getInlineStyle} from '../util/ngQuery'; const normalizeTransformed = (t: CodeWithSourceMap) => { - if (!t.map) { - t.source = t.code; - } - return t; + if (!t.map) { + t.source = t.code; + } + return t; }; + /** * For async implementation https://gist.github.com/mgechev/6f2245c0dfb38539cc606ea9211ecb37 */ export class MetadataReader { - constructor(private _fileResolver: FileResolver, private _urlResolver?: AbstractResolver) { - this._urlResolver = this._urlResolver || new UrlResolver(new PathResolver()); - } - - read(d: ts.ClassDeclaration): DirectiveMetadata { - let directiveDecorator: ts.Decorator = null; - let componentDecorator: ts.Decorator = null; - (d.decorators || ([] as ts.Decorator[])).forEach((dec: ts.Decorator) => { - let expr = dec.expression; - if (expr && expr.kind === kinds.CallExpression && (expr).expression) { - expr = (expr).expression; - } - const identifier = (expr); - if (expr && expr.kind === kinds.Identifier && identifier.text) { - if (identifier.text === 'Component') { - componentDecorator = dec; - } else if (identifier.text === 'Directive') { - directiveDecorator = dec; - } - } - }); - if (directiveDecorator) { - return this.readDirectiveMetadata(d, directiveDecorator); + constructor(private _fileResolver: FileResolver, private _urlResolver?: AbstractResolver) { + this._urlResolver = this._urlResolver || new UrlResolver(new PathResolver()); } - if (componentDecorator) { - return this.readComponentMetadata(d, componentDecorator); + + read(d: ts.ClassDeclaration): DirectiveMetadata { + let componentMetadata = unwrapFirst( + (d.decorators || ([] as ts.Decorator[])).map((dec: ts.Decorator) => { + return Maybe.lift(dec).bind(callExpression) + .bind(withIdentifier('Component')) + .fmap(() => this.readComponentMetadata(d, dec)); + })); + + let directiveMetadata = unwrapFirst( + (d.decorators || ([] as ts.Decorator[])).map((dec: ts.Decorator) => + Maybe.lift(dec) + .bind(callExpression) + .bind(withIdentifier('Directive')) + .fmap(() => this.readDirectiveMetadata(d, dec)) + )); + + return directiveMetadata || componentMetadata || undefined; } - return null; - } - - readDirectiveMetadata(d: ts.ClassDeclaration, dec: ts.Decorator) { - const expr = this.getDecoratorArgument(dec); - const metadata = new DirectiveMetadata(); - metadata.controller = d; - metadata.decorator = dec; - if (!expr) { - return metadata; + + readDirectiveMetadata(d: ts.ClassDeclaration, dec: ts.Decorator): DirectiveMetadata { + + const selector = this.getDecoratorArgument(dec) + .bind(expr => getStringInitializerFromProperty('selector', expr.properties)) + .fmap(initializer => initializer.text); + + return Object.assign(new DirectiveMetadata(), { + controller: d, + decorator: dec, + selector: selector.unwrap(), + }); } - expr.properties.forEach((p: any) => { - if (p.kind !== kinds.PropertyAssignment) { - return; - } - const prop = p; - if ((prop).name.text === 'selector' && isSimpleTemplateString(prop.initializer)) { - metadata.selector = (prop).initializer.text; - } - }); - return metadata; - } - - readComponentTemplateMetadata(dec: ts.Decorator, external: MetadataUrls): TemplateMetadata { - const inlineTemplate = getDecoratorPropertyInitializer(dec, 'template'); - if (inlineTemplate && isSimpleTemplateString(inlineTemplate)) { - const transformed = normalizeTransformed(Config.transformTemplate(inlineTemplate.text, null, dec)); - return { - template: transformed, - url: null, - node: inlineTemplate - }; - } else { - if (external.templateUrl) { - try { - const template = this._fileResolver.resolve(external.templateUrl); - const transformed = normalizeTransformed(Config.transformTemplate(template, external.templateUrl, dec)); - return { - template: transformed, - url: external.templateUrl, - node: null - }; - } catch (e) { - logger.info('Cannot read the external template ' + external.templateUrl); + + readComponentTemplateMetadata(dec: ts.Decorator, external: MetadataUrls): TemplateMetadata { + const template_M = getTemplate(dec) + .fmap(inlineTemplate => { + const transformed = normalizeTransformed(Config.transformTemplate(inlineTemplate.text, null, dec)); + return { + template: transformed, + url: null, + node: inlineTemplate, + }; + }); + + if (template_M.isSomething) { + return template_M.unwrap(); + } else { + // TODO: Refactoring this requires adding seem to fileResolver + if (external.templateUrl) { + try { + const template = this._fileResolver.resolve(external.templateUrl); + const transformed = normalizeTransformed(Config.transformTemplate(template, external.templateUrl, dec)); + return { + template: transformed, + url: external.templateUrl, + node: null + }; + } catch (e) { + logger.info('Cannot read the external template ' + external.templateUrl); + } + } } - } } - } - - readComponentStylesMetadata(dec: ts.Decorator, external: MetadataUrls) { - const inlineStyles = getDecoratorPropertyInitializer(dec, 'styles'); - let styles: any[]; - if (inlineStyles && inlineStyles.kind === kinds.ArrayLiteralExpression) { - inlineStyles.elements.forEach((inlineStyle: any) => { - if (isSimpleTemplateString(inlineStyle)) { - styles = styles || []; - styles.push({ - style: normalizeTransformed(Config.transformStyle(inlineStyle.text, null, dec)), - url: null, - node: inlineStyle, - }); + + readComponentStylesMetadata(dec: ts.Decorator, external: MetadataUrls) { + let styles: any[]; + const inlineStyles_M = getInlineStyle(dec) + .fmap(inlineStyles => { + return inlineStyles.elements.map((inlineStyle: ts.Expression) => { + if (isSimpleTemplateString(inlineStyle)) { + return { + style: normalizeTransformed(Config.transformStyle(inlineStyle.text, null, dec)), + url: null, + node: inlineStyle, + }; + } + }).filter(v => !!v); + }); + + if (inlineStyles_M.isSomething) { + return inlineStyles_M.unwrap(); + } else if (external.styleUrls) { + // TODO: Refactoring this requires adding seem to fileResolver + try { + styles = external.styleUrls.map((url: string) => { + const style = this._fileResolver.resolve(url); + const transformed = normalizeTransformed(Config.transformStyle(style, url, dec)); + return { + style: transformed, url, + node: null + }; + }); + } catch (e) { + logger.info('Unable to read external style. ' + e.toString()); + } } - }); - } else if (external.styleUrls) { - try { - styles = external.styleUrls.map((url: string) => { - const style = this._fileResolver.resolve(url); - const transformed = normalizeTransformed(Config.transformStyle(style, url, dec)); - return { - style: transformed, url, - node: null - }; - }); - } catch (e) { - logger.info('Unable to read external style. ' + e.toString()); - } + return styles; } - return styles; - } - - readComponentMetadata(d: ts.ClassDeclaration, dec: ts.Decorator) { - const expr = this.getDecoratorArgument(dec); - const metadata = this.readDirectiveMetadata(d, dec); - const result = new ComponentMetadata(); - result.selector = metadata.selector; - result.controller = metadata.controller; - if (!expr) { - return result; + + readComponentMetadata(d: ts.ClassDeclaration, dec: ts.Decorator) { + const expr = this.getDecoratorArgument(dec); + const directiveMetadata = this.readDirectiveMetadata(d, dec); + + const external_M = expr.fmap(() => this._urlResolver.resolve(dec)); + + const template_M = external_M.fmap( + (external) => this.readComponentTemplateMetadata(dec, external)); + const style_M = external_M.fmap( + (external) => this.readComponentStylesMetadata(dec, external)); + + return Object.assign(new ComponentMetadata(), directiveMetadata, { + template: template_M.unwrap(), + styles: style_M.unwrap(), + }); } - const external = this._urlResolver.resolve(dec); - result.template = this.readComponentTemplateMetadata(dec, external); - result.styles = this.readComponentStylesMetadata(dec, external); - return result; - } - - protected getDecoratorArgument(decorator: ts.Decorator): ts.ObjectLiteralExpression { - const expr = decorator.expression; - if (expr && expr.arguments && expr.arguments.length) { - const arg = expr.arguments[0]; - if (arg.kind === kinds.ObjectLiteralExpression && arg.properties) { - return arg; - } + + protected getDecoratorArgument(decorator: ts.Decorator): Maybe { + return decoratorArgument(decorator) + .bind(ifTrue(hasProperties)); } - return null; - } } diff --git a/src/componentClassSuffixRule.ts b/src/componentClassSuffixRule.ts index 0512f8667..5f893d9d1 100644 --- a/src/componentClassSuffixRule.ts +++ b/src/componentClassSuffixRule.ts @@ -1,36 +1,40 @@ import * as Lint from 'tslint'; import * as ts from 'typescript'; import {sprintf} from 'sprintf-js'; -import {Ng2Walker} from './angular/ng2Walker'; import {ComponentMetadata} from './angular/metadata'; +import {Failure} from './walkerFactory/walkerFactory'; +import {all, validateComponent} from './walkerFactory/walkerFn'; +import {Maybe, F2} from './util/function'; +import {IOptions} from 'tslint'; +import {Ng2Walker} from './angular/ng2Walker'; export class Rule extends Lint.Rules.AbstractRule { - static FAILURE: string = 'The name of the class %s should end with the suffix %s ($$02-03$$)'; - static validate(className: string, suffix: string):boolean { - return className.endsWith(suffix); - } + static FAILURE: string = 'The name of the class %s should end with the suffix %s ($$02-03$$)'; - public apply(sourceFile:ts.SourceFile):Lint.RuleFailure[] { - return this.applyWithWalker( - new ClassMetadataWalker(sourceFile, - this.getOptions())); - } -} + static validate(className: string, suffixList: string[]): boolean { + return suffixList.some(suffix => className.endsWith(suffix)); + } -export class ClassMetadataWalker extends Ng2Walker { - visitNg2Component(meta: ComponentMetadata) { - let name = meta.controller.name; - let className: string = name.text; - const suffixList = this.getOptions().length > 0 ? this.getOptions() : ['Component']; - let ruleInvalidate = !suffixList.some(suffix => Rule.validate(className, suffix)); - if (ruleInvalidate) { - this.addFailure( - this.createFailure( - name.getStart(), - name.getWidth(), - sprintf.apply(this, [Rule.FAILURE, className, suffixList]))); + static walkerBuilder: F2 = + all( + validateComponent((meta: ComponentMetadata, suffixList?: string[]) => + Maybe.lift(meta.controller) + .fmap(controller => controller.name) + .fmap(name => { + const className = name.text; + const _suffixList = suffixList.length > 0 ? suffixList : ['Component']; + if (!Rule.validate(className, _suffixList)) { + return [new Failure(name, sprintf(Rule.FAILURE, className, _suffixList))]; + } + }) + )); + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker( + Rule.walkerBuilder(sourceFile, this.getOptions()) + ); } - super.visitNg2Component(meta); - } } + + diff --git a/src/noAttributeParameterDecoratorRule.ts b/src/noAttributeParameterDecoratorRule.ts index 6776c9d4a..4731a3518 100644 --- a/src/noAttributeParameterDecoratorRule.ts +++ b/src/noAttributeParameterDecoratorRule.ts @@ -2,51 +2,57 @@ import * as Lint from 'tslint'; import * as ts from 'typescript'; import {sprintf} from 'sprintf-js'; import SyntaxKind = require('./util/syntaxKind'); +import {validate, all} from './walkerFactory/walkerFn'; +import {Maybe, listToMaybe} from './util/function'; +import {isDecorator, withIdentifier, callExpression} from './util/astQuery'; +import {Failure} from './walkerFactory/walkerFactory'; export class Rule extends Lint.Rules.AbstractRule { - static FAILURE_STRING:string = 'In the constructor of class "%s",' + - ' the parameter "%s" uses the @Attribute decorator, ' + - 'which is considered as a bad practice. Please,' + - ' consider construction of type "@Input() %s: string"'; + static FAILURE_STRING: string = 'In the constructor of class "%s",' + + ' the parameter "%s" uses the @Attribute decorator, ' + + 'which is considered as a bad practice. Please,' + + ' consider construction of type "@Input() %s: string"'; - public apply(sourceFile:ts.SourceFile):Lint.RuleFailure[] { - return this.applyWithWalker( - new ConstructorMetadataWalker(sourceFile, - this.getOptions())); - } -} -export class ConstructorMetadataWalker extends Lint.RuleWalker { - visitConstructorDeclaration(node:ts.ConstructorDeclaration) { - let syntaxKind = SyntaxKind.current(); - let parentName: string = ''; - let parent = (node.parent); - if (parent.kind === syntaxKind.ClassExpression) { - parentName= parent.parent.name.text; - } else if (parent.kind = syntaxKind.ClassDeclaration) { - parentName= parent.name.text; - } - (node.parameters || []).forEach(this.validateParameter.bind(this, parentName)); - super.visitConstructorDeclaration(node); - } + static walkerBuilder = all( + validate(SyntaxKind.current().Constructor)((node: ts.ConstructorDeclaration) => { + const syntaxKind = SyntaxKind.current(); + return Maybe.lift(node.parent) + .fmap(parent => { + if (parent.kind === syntaxKind.ClassExpression) { + return parent.parent.name.text; + } else if (parent.kind = syntaxKind.ClassDeclaration) { + return parent.name.text; + } + }) + .bind(parentName => { + const failures: Maybe[] = node.parameters.map(p => + Maybe.lift(p.decorators) + .bind(decorators => { + // Check if any @Attribute + const decoratorsFailed = listToMaybe( + decorators.map(d => Rule.decoratorIsAttribute(d))); + + // We only care about 1 since we highlight the whole 'parameter' + return decoratorsFailed.fmap(() => + new Failure(p, sprintf(Rule.FAILURE_STRING, + parentName, (p.name).text, (p.name).text))) + }) + ); + return listToMaybe(failures) + }); + }) + ); - validateParameter(className: string, parameter) { - let parameterName = (parameter.name).text; - if (parameter.decorators) { - parameter.decorators.forEach((decorator)=> { - let baseExpr = decorator.expression || {}; - let expr = baseExpr.expression || {}; - let name = expr.text; - if (name === 'Attribute') { - let failureConfig:string[] = [className, parameterName, parameterName]; - failureConfig.unshift(Rule.FAILURE_STRING); - this.addFailure( - this.createFailure( - parameter.getStart(), - parameter.getWidth(), - sprintf.apply(this, failureConfig))); + private static decoratorIsAttribute(dec: ts.Decorator): Maybe { + if (isDecorator(dec)) { + return callExpression(dec).bind(withIdentifier('Attribute')); } - }); + return Maybe.nothing; + } + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker( + Rule.walkerBuilder(sourceFile, this.getOptions())); } - } } diff --git a/src/util/astQuery.ts b/src/util/astQuery.ts new file mode 100644 index 000000000..44828df77 --- /dev/null +++ b/src/util/astQuery.ts @@ -0,0 +1,78 @@ +import * as ts from 'typescript'; +import {current} from './syntaxKind'; +import {Maybe, ifTrue} from './function'; +const kinds = current(); + +export function isCallExpression(expr: ts.LeftHandSideExpression): expr is ts.CallExpression { + return expr && expr.kind === kinds.CallExpression; +} + +export function callExpression(dec: ts.Decorator): Maybe { + return Maybe.lift(dec.expression) + .fmap(expr => isCallExpression(expr) ? expr as ts.CallExpression : undefined); +} + +export function isPropertyAssignment(expr: ts.ObjectLiteralElement): expr is ts.PropertyAssignment { + return expr && expr.kind === kinds.PropertyAssignment; +} + +export function isSimpleTemplateString(expr: ts.Expression): expr is (ts.StringLiteral | ts.NoSubstitutionTemplateLiteral) { + return expr && expr.kind === kinds.StringLiteral || expr.kind === kinds.NoSubstitutionTemplateLiteral; +} + +export function isArrayLiteralExpression(expr: ts.Expression): expr is ts.ArrayLiteralExpression { + return expr && expr.kind === kinds.ArrayLiteralExpression; +} + +export function hasProperties(expr: ts.ObjectLiteralExpression): boolean { + return expr && !!expr.properties; +} + +export function isObjectLiteralExpression(expr: ts.Expression): expr is ts.ObjectLiteralExpression { + return expr && expr.kind === kinds.ObjectLiteralExpression; +} + +export function objectLiteralExpression(expr: ts.CallExpression): Maybe { + return Maybe.lift(expr.arguments[0]) + .fmap(arg0 => (isObjectLiteralExpression(arg0) ? arg0 as ts.ObjectLiteralExpression : undefined)); +} + +export function isIdentifier(expr: ts.PropertyName | ts.LeftHandSideExpression): expr is ts.Identifier { + return expr && expr.kind === kinds.Identifier; +} + +export function withIdentifier(identifier: string): (expr: ts.CallExpression) => Maybe { + return ifTrue( + (expr: ts.CallExpression) => ( + isIdentifier(expr.expression) && expr.expression.text === identifier + )); +} + +export type WithStringInitializer = ts.StringLiteral | ts.NoSubstitutionTemplateLiteral; + +export function isProperty(propName: string, p: ts.ObjectLiteralElement): boolean { + return isPropertyAssignment(p) && isIdentifier(p.name) && p.name.text === propName; +} + +export function getInitializer(p: ts.ObjectLiteralElement): Maybe { + return Maybe.lift( + (isPropertyAssignment(p) && isIdentifier(p.name)) ? p.initializer : undefined); +} + +export function getStringInitializerFromProperty(propertyName: string, ps: ts.ObjectLiteralElement[]): Maybe { + const property = ps.find(p => isProperty(propertyName, p)); + return getInitializer(property) + // A little wrinkle to return Maybe + .fmap(expr => isSimpleTemplateString(expr) ? expr as WithStringInitializer : undefined); +} + +export function decoratorArgument(dec: ts.Decorator): Maybe { + return Maybe.lift(dec) + .bind(callExpression) + .bind(objectLiteralExpression); +} + +export function isDecorator(expr: ts.Node): expr is ts.Decorator { + return expr && expr.kind === kinds.Decorator; +} + diff --git a/src/util/function.ts b/src/util/function.ts new file mode 100644 index 000000000..87378afea --- /dev/null +++ b/src/util/function.ts @@ -0,0 +1,81 @@ +export interface F0 { + (): R; +} +export interface F1 { + (a0: A0): R; +} +export interface F2 { + (a0: A0, a1: A1): R; +} +export interface F3 { + (a0: A0, a1: A1, a2: A2): R; +} + +export class Maybe { + + static nothing = new Maybe(undefined); + + static lift(t: T) { + if (!t) { + return Maybe.nothing; + } + return new Maybe(t); + } + + bind(fn: F1>): Maybe { + if (!!this.t) { + return fn(this.t); + } + return Maybe.nothing; + } + + fmap(fn: F1): Maybe { + return this.bind(t => Maybe.lift(fn(t))); + } + + get isNothing() { + return !this.t; + } + + get isSomething() { + return !!this.t; + } + + catch(def: () => Maybe): Maybe { + if (this.isNothing) { + return def(); + } + return this; + } + + unwrap(): T | undefined { + return this.t; + }; + + private constructor(private t: T | undefined) { + } +} + +export function unwrapFirst(ts: Maybe[]): T|undefined { + const f = ts.find((t: Maybe) => t.isSomething); + if (!!f) { + return f.unwrap(); + } + return undefined; +} + +export function all(...preds: F1[]): F1 { + return (t: T) => !preds.find(p => !p(t)); +} +export function any(...preds: F1[]): F1 { + return (t: T) => !!preds.find(p => p(t)); +} + +export function ifTrue(pred: F1): F1> { + return (t: T) => (pred(t)) ? Maybe.lift(t) : Maybe.nothing; +} + +export function listToMaybe(ms: Maybe[]):Maybe { + const unWrapped = ms.filter(m => m.isSomething).map(m => m.unwrap()); + return unWrapped.length !== 0 ? Maybe.lift(unWrapped) : Maybe.nothing; +} diff --git a/src/util/ngQuery.ts b/src/util/ngQuery.ts new file mode 100644 index 000000000..629dd4f5f --- /dev/null +++ b/src/util/ngQuery.ts @@ -0,0 +1,29 @@ +import * as ts from 'typescript'; +import {Maybe} from './function'; +import { + decoratorArgument, getInitializer, isProperty, isArrayLiteralExpression, + WithStringInitializer, getStringInitializerFromProperty +} from './astQuery'; + +export function getInlineStyle(dec: ts.Decorator): Maybe { + return decoratorArgument(dec) + .bind((expr: ts.ObjectLiteralExpression) => { + const property = expr.properties.find(p => isProperty('styles', p)); + return getInitializer(property) + .fmap(expr => + isArrayLiteralExpression(expr) ? expr as ts.ArrayLiteralExpression : undefined); + }); +} + +export function getTemplateUrl(dec: ts.Decorator): Maybe { + return decoratorArgument(dec) + .bind((expr: ts.ObjectLiteralExpression) => + getStringInitializerFromProperty('templateUrl', expr.properties)); +} + +export function getTemplate(dec: ts.Decorator): Maybe { + return decoratorArgument(dec) + .bind((expr: ts.ObjectLiteralExpression) => + getStringInitializerFromProperty('template', expr.properties)); +} + diff --git a/src/walkerFactory/walkerFactory.ts b/src/walkerFactory/walkerFactory.ts new file mode 100644 index 000000000..1f4c492a5 --- /dev/null +++ b/src/walkerFactory/walkerFactory.ts @@ -0,0 +1,47 @@ +import * as ts from 'typescript'; +import {Ng2Walker} from '../angular/ng2Walker'; +import {IOptions} from 'tslint'; +import {ComponentMetadata} from '../angular/metadata'; +import {F1, Maybe} from '../util/function'; + +type Walkable = 'Ng2Component'; + +export function allNg2Component(): WalkerBuilder<'Ng2Component'> { + return new Ng2ComponentWalkerBuilder(); +} + +export class Failure { + constructor(public node: ts.Node, public message: string) {} +} + +interface WalkerBuilder { + where: (validate: F1>) => WalkerBuilder; + build: (sourceFile: ts.SourceFile, options: IOptions) => Ng2Walker; +} + +class Ng2ComponentWalkerBuilder implements WalkerBuilder<'Ng2Component'> { + private _where: F1>; + + where(validate: F1>):Ng2ComponentWalkerBuilder { + this._where = validate; + return this; + } + + build(sourceFile: ts.SourceFile, options: IOptions): Ng2Walker { + const self = this; + const e = class extends Ng2Walker { + visitNg2Component(meta: ComponentMetadata) { + self._where(meta).fmap(failure => { + this.addFailure( + this.createFailure( + failure.node.getStart(), + failure.node.getWidth(), + failure.message, + )); + }); + super.visitNg2Component(meta); + } + }; + return new e(sourceFile, options); + } +} diff --git a/src/walkerFactory/walkerFn.ts b/src/walkerFactory/walkerFn.ts new file mode 100644 index 000000000..8f4bb962c --- /dev/null +++ b/src/walkerFactory/walkerFn.ts @@ -0,0 +1,72 @@ +import * as ts from 'typescript'; +import {Ng2Walker} from '../angular/ng2Walker'; +import {IOptions} from 'tslint'; +import {ComponentMetadata} from '../angular/metadata'; +import {F1, F2, Maybe} from '../util/function'; +import {Failure} from './walkerFactory'; + +type ComponentWalkable = 'Ng2Component'; + +type Validator = NodeValidator | ComponentValidator; +type ValidateFn = F2>; +type WalkerOptions = any; + +interface NodeValidator { + kind: 'Node'; + validate: ValidateFn; +} + +interface ComponentValidator { + kind: ComponentWalkable; + validate: ValidateFn; +} + +export function validate(syntaxKind: ts.SyntaxKind): F1, NodeValidator> { + return validateFn => ({ + kind: 'Node', + validate: (node: ts.Node, options: WalkerOptions) => (node.kind === syntaxKind) ? validateFn(node, options) : Maybe.nothing, + }); +} + +export function validateComponent(validate: F2>): ComponentValidator { + return { + kind: 'Ng2Component', + validate, + }; +} + +export function all(...validators: Validator[]): F2 { + return (sourceFile, options) => { + const e = class extends Ng2Walker { + visitNg2Component(meta: ComponentMetadata) { + validators.forEach(v => { + if (v.kind === 'Ng2Component') { + v.validate(meta, this.getOptions()).fmap( + failures => failures.forEach(f => this.failed(f))); + } + }); + super.visitNg2Component(meta); + } + + visitNode(node: ts.Node) { + validators.forEach(v => { + if (v.kind === 'Node') { + v.validate(node, this.getOptions()).fmap( + failures => failures.forEach(f => this.failed(f))); + } + }); + super.visitNode(node); + } + + private failed(failure: Failure) { + this.addFailure( + this.createFailure( + failure.node.getStart(), + failure.node.getWidth(), + failure.message, + )); + } + }; + return new e(sourceFile, options); + }; +} diff --git a/test/angular/metadataReader.spec.ts b/test/angular/metadataReader.spec.ts index e8eef0a5a..6ebf768cb 100644 --- a/test/angular/metadataReader.spec.ts +++ b/test/angular/metadataReader.spec.ts @@ -171,12 +171,14 @@ describe('metadataReader', () => { it('should work invoke Config.resolveUrl after all resolves', () => { let invoked = false; const bak = Config.resolveUrl; - Config.resolveUrl = (url: string) => { - invoked = true; - chai.expect(url.startsWith(normalize(join(__dirname, '../..')))).eq(true); - return url; - }; - const code = ` + try { + + Config.resolveUrl = (url: string) => { + invoked = true; + chai.expect(url.startsWith(normalize(join(__dirname, '../..')))).eq(true); + return url; + }; + const code = ` @Component({ selector: 'foo', moduleId: module.id, @@ -185,25 +187,29 @@ describe('metadataReader', () => { }) class Bar {} `; - const reader = new MetadataReader(new FsFileResolver()); - const ast = getAst(code, __dirname + '/../../test/fixtures/metadataReader/moduleid/foo.ts'); - const classDeclaration = ast.statements.pop(); - chai.expect(invoked).eq(false); - const metadata = reader.read(classDeclaration); - chai.expect(metadata instanceof ComponentMetadata).eq(true); - chai.expect(metadata.selector).eq('foo'); - const m = metadata; - chai.expect(m.template.template.code.trim()).eq('
'); - chai.expect(m.template.url.endsWith('foo.html')).eq(true); - chai.expect(m.styles[0].style.code).eq('baz'); - chai.expect(m.styles[0].url).eq(null); - chai.expect(invoked).eq(true); - Config.resolveUrl = bak; + const reader = new MetadataReader(new FsFileResolver()); + const ast = getAst(code, __dirname + '/../../test/fixtures/metadataReader/moduleid/foo.ts'); + const classDeclaration = ast.statements.pop(); + chai.expect(invoked).eq(false); + const metadata = reader.read(classDeclaration); + chai.expect(metadata instanceof ComponentMetadata).eq(true); + chai.expect(metadata.selector).eq('foo'); + const m = metadata; + chai.expect(m.template.template.code.trim()).eq('
'); + chai.expect(m.template.url.endsWith('foo.html')).eq(true); + chai.expect(m.styles[0].style.code).eq('baz'); + chai.expect(m.styles[0].url).eq(null); + chai.expect(invoked).eq(true); + } finally { + Config.resolveUrl = bak; + } }); it('should work invoke Config.transformTemplate', () => { let invoked = false; const bak = Config.transformTemplate; + try { + Config.transformTemplate = (code: string) => { invoked = true; chai.expect(code.trim()).eq('
'); @@ -231,18 +237,22 @@ describe('metadataReader', () => { chai.expect(m.styles[0].style.code).eq('baz'); chai.expect(m.styles[0].url).eq(null); chai.expect(invoked).eq(true); - Config.transformTemplate = bak; + + } finally { + Config.transformTemplate = bak; + } }); it('should work invoke Config.transformStyle', () => { let invoked = false; const bak = Config.transformStyle; - Config.transformStyle = (code: string) => { - invoked = true; - chai.expect(code).eq('baz'); - return { code }; - }; - const code = ` + try { + Config.transformStyle = (code: string) => { + invoked = true; + chai.expect(code).eq('baz'); + return {code}; + }; + const code = ` @Component({ selector: 'foo', moduleId: module.id, @@ -251,20 +261,22 @@ describe('metadataReader', () => { }) class Bar {} `; - const reader = new MetadataReader(new FsFileResolver()); - const ast = getAst(code, __dirname + '/../../test/fixtures/metadataReader/moduleid/foo.ts'); - const classDeclaration = ast.statements.pop(); - chai.expect(invoked).eq(false); - const metadata = reader.read(classDeclaration); - chai.expect(metadata instanceof ComponentMetadata).eq(true); - chai.expect(metadata.selector).eq('foo'); - const m = metadata; - chai.expect(m.template.template.code.trim()).eq('
'); - chai.expect(m.template.url.endsWith('foo.html')).eq(true); - chai.expect(m.styles[0].style.code).eq('baz'); - chai.expect(m.styles[0].url).eq(null); - chai.expect(invoked).eq(true); - Config.transformStyle = bak; + const reader = new MetadataReader(new FsFileResolver()); + const ast = getAst(code, __dirname + '/../../test/fixtures/metadataReader/moduleid/foo.ts'); + const classDeclaration = ast.statements.pop(); + chai.expect(invoked).eq(false); + const metadata = reader.read(classDeclaration); + chai.expect(metadata instanceof ComponentMetadata).eq(true); + chai.expect(metadata.selector).eq('foo'); + const m = metadata; + chai.expect(m.template.template.code.trim()).eq('
'); + chai.expect(m.template.url.endsWith('foo.html')).eq(true); + chai.expect(m.styles[0].style.code).eq('baz'); + chai.expect(m.styles[0].url).eq(null); + chai.expect(invoked).eq(true); + } finally { + Config.transformStyle = bak; + } }); it('should work work with templates with "`"', () => {