Skip to content

Commit

Permalink
feat(rule): add new Rule RelativePathExternalResourcesRule (#725)
Browse files Browse the repository at this point in the history
* feat(rule): add new Rule RelativePathExternalResourcesRule

* feat(rule): fix regexp

* feat(rule): fix message
  • Loading branch information
wKoza authored and mgechev committed Nov 25, 2018
1 parent 465d5f8 commit f12f27b
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -38,6 +38,7 @@ export { Rule as UseOutputPropertyDecoratorRule } from './useOutputPropertyDecor
export { Rule as UsePipeDecoratorRule } from './usePipeDecoratorRule';
export { Rule as UsePipeTransformInterfaceRule } from './usePipeTransformInterfaceRule';
export { Rule as UseViewEncapsulationRule } from './useViewEncapsulationRule';
export { Rule as RelativePathExternalResourcesRule } from './relativeUrlPrefixRule';

export * from './angular';

Expand Down
66 changes: 66 additions & 0 deletions src/relativeUrlPrefixRule.ts
@@ -0,0 +1,66 @@
import { IOptions, IRuleMetadata, RuleFailure, Rules } from 'tslint/lib';
import { SourceFile } from 'typescript/lib/typescript';
import { NgWalker } from './angular/ngWalker';
import * as ts from 'typescript';

export class Rule extends Rules.AbstractRule {
static readonly metadata: IRuleMetadata = {
description: "The ./ prefix is standard syntax for relative URLs; don't depend on Angular's current ability to do without that prefix.",
descriptionDetails: 'See more at https://angular.io/styleguide#style-05-04.',
rationale: 'A component relative URL requires no change when you move the component files, as long as the files stay together.',
ruleName: 'relative-url-prefix',
options: null,
optionsDescription: 'Not configurable.',
type: 'maintainability',
typescriptOnly: true
};

static readonly FAILURE_STRING = 'The ./ prefix is standard syntax for relative URLs. (https://angular.io/styleguide#style-05-04)';

apply(sourceFile: SourceFile): RuleFailure[] {
return this.applyWithWalker(new RelativePathExternalResourcesRuleWalker(sourceFile, this.getOptions()));
}
}

export class RelativePathExternalResourcesRuleWalker extends NgWalker {
constructor(sourceFile: SourceFile, options: IOptions) {
super(sourceFile, options);
}

visitClassDecorator(decorator: ts.Decorator) {
if (ts.isCallExpression(decorator.expression) && decorator.expression.arguments) {
decorator.expression.arguments.forEach(arg => {
if (ts.isObjectLiteralExpression(arg) && arg.properties) {
arg.properties.forEach((prop: any) => {
if (prop && prop.name.text === 'templateUrl') {
const url = prop.initializer.text;
this.checkTemplateUrl(url, prop.initializer);
} else if (prop && prop.name.text === 'styleUrls') {
if (prop.initializer.elements.length > 0) {
prop.initializer.elements.forEach(e => {
const url = e.text;
this.checkStyleUrls(e);
});
}
}
});
}
});
}
super.visitClassDecorator(decorator);
}

private checkTemplateUrl(url: string, initializer: ts.StringLiteral) {
if (url && !/^\.\/[^\.\/|\.\.\/]/.test(url)) {
this.addFailureAtNode(initializer, Rule.FAILURE_STRING);
}
}

private checkStyleUrls(token: ts.StringLiteral) {
if (token && token.text) {
if (!/^\.\/[^\.\/|\.\.\/]/.test(token.text)) {
this.addFailureAtNode(token, Rule.FAILURE_STRING);
}
}
}
}
140 changes: 140 additions & 0 deletions test/relativeUrlPrefixRule.spec.ts
@@ -0,0 +1,140 @@
import { Rule } from '../src/relativeUrlPrefixRule';
import { assertAnnotated, assertSuccess } from './testHelper';

const {
metadata: { ruleName }
} = Rule;

describe(ruleName, () => {
describe('styleUrls', () => {
describe('success', () => {
it('should succeed when a relative URL is prefixed by ./', () => {
const source = `
@Component({
styleUrls: ['./foobar.css']
})
class Test {}
`;
assertSuccess(ruleName, source);
});

it('should succeed when all relative URLs is prefixed by ./', () => {
const source = `
@Component({
styleUrls: ['./foo.css', './bar.css', './whatyouwant.css']
})
class Test {}
`;
assertSuccess(ruleName, source);
});
});

describe('failure', () => {
it("should fail when a relative URL isn't prefixed by ./", () => {
const source = `
@Component({
styleUrls: ['foobar.css']
~~~~~~~~~~~~
})
class Test {}
`;
assertAnnotated({
ruleName,
message: Rule.FAILURE_STRING,
source
});
});

it("should fail when a relative URL isn't prefixed by ./", () => {
const source = `
@Component({
styleUrls: ['./../foobar.css']
~~~~~~~~~~~~~~~~~
})
class Test {}
`;
assertAnnotated({
ruleName,
message: Rule.FAILURE_STRING,
source
});
});

it("should fail when one relative URLs isn't prefixed by ./", () => {
const source = `
@Component({
styleUrls: ['./foo.css', 'bar.css', './whatyouwant.css']
~~~~~~~~~
})
class Test {}
`;
assertAnnotated({
ruleName,
message: Rule.FAILURE_STRING,
source
});
});
});
});

describe('templateUrl', () => {
describe('success', () => {
it('should succeed when a relative URL is prefixed by ./', () => {
const source = `
@Component({
templateUrl: './foobar.html'
})
class Test {}
`;
assertSuccess(ruleName, source);
});
});

describe('failure', () => {
it("should succeed when a relative URL isn't prefixed by ./", () => {
const source = `
@Component({
templateUrl: 'foobar.html'
~~~~~~~~~~~~~
})
class Test {}
`;
assertAnnotated({
ruleName,
message: Rule.FAILURE_STRING,
source
});
});

it('should fail when a relative URL is prefixed by ../', () => {
const source = `
@Component({
templateUrl: '../foobar.html'
~~~~~~~~~~~~~~~~
})
class Test {}
`;
assertAnnotated({
ruleName,
message: Rule.FAILURE_STRING,
source
});
});

it('should fail when a relative URL is prefixed by ../', () => {
const source = `
@Component({
templateUrl: '.././foobar.html'
~~~~~~~~~~~~~~~~~~
})
class Test {}
`;
assertAnnotated({
ruleName,
message: Rule.FAILURE_STRING,
source
});
});
});
});
});

0 comments on commit f12f27b

Please sign in to comment.