diff --git a/src/index.ts b/src/index.ts index 88b2cd4a2..d9dd0f6cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/relativeUrlPrefixRule.ts b/src/relativeUrlPrefixRule.ts new file mode 100644 index 000000000..a43fe1083 --- /dev/null +++ b/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); + } + } + } +} diff --git a/test/relativeUrlPrefixRule.spec.ts b/test/relativeUrlPrefixRule.spec.ts new file mode 100644 index 000000000..cb934bc04 --- /dev/null +++ b/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 + }); + }); + }); + }); +});