Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Experimental] First attempt at replacing AST traversal with Maybe<T> #180

Merged
merged 5 commits into from
Dec 25, 2016
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
259 changes: 126 additions & 133 deletions src/angular/metadataReader.ts
Original file line number Diff line number Diff line change
@@ -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 && (<ts.CallExpression>expr).expression) {
expr = (<ts.CallExpression>expr).expression;
}
const identifier = (<ts.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) => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks beautiful!

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 = <ts.PropertyAssignment>p;
if ((<any>prop).name.text === 'selector' && isSimpleTemplateString(prop.initializer)) {
metadata.selector = (<any>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)) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can first apply filter and after that map it, so we can drop the ugly if condition?

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 = <any>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 = <any>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(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice that decorator is not copied to result. Is that intentional?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like an issue.

(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 = <ts.CallExpression>decorator.expression;
if (expr && expr.arguments && expr.arguments.length) {
const arg = <ts.ObjectLiteralExpression>expr.arguments[0];
if (arg.kind === kinds.ObjectLiteralExpression && arg.properties) {
return arg;
}

protected getDecoratorArgument(decorator: ts.Decorator): Maybe<ts.ObjectLiteralExpression> {
return decoratorArgument(decorator)
.bind(ifTrue(hasProperties));
}
return null;
}
}
54 changes: 29 additions & 25 deletions src/componentClassSuffixRule.ts
Original file line number Diff line number Diff line change
@@ -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<ts.SourceFile, IOptions, Ng2Walker> =
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);
}
}


Loading