Skip to content

Commit c503510

Browse files
committed
fix: proper position in html/css source file
Fix #343
1 parent 859ff99 commit c503510

File tree

6 files changed

+187
-6
lines changed

6 files changed

+187
-6
lines changed

src/angular/ngWalker.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,13 @@ export class NgWalker extends Lint.RuleWalker {
193193
if (!path) {
194194
return current;
195195
}
196-
return ts.createSourceFile(path, `\`${content}\``, ts.ScriptTarget.ES5);
196+
const sf = ts.createSourceFile(path, `\`${content}\``, ts.ScriptTarget.ES5);
197+
const original = sf.getFullText;
198+
sf.getFullText = function () {
199+
const text = original.apply(sf);
200+
return text.substring(1, text.length - 1);
201+
}.bind(sf);
202+
return sf;
197203
}
198204
}
199205

src/angularWhitespaceRule.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export class Rule extends Lint.Rules.AbstractRule {
179179

180180
static FAILURE: string = 'The %s "%s" that you\'re trying to access does not exist in the class declaration.';
181181

182-
public apply(sourceFile:ts.SourceFile): Lint.RuleFailure[] {
182+
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
183183
return this.applyWithWalker(
184184
new NgWalker(sourceFile,
185185
this.getOptions(), {

test/angularWhitespaceRule.spec.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import { assertSuccess, assertAnnotated, assertMultipleAnnotated } from './testHelper';
22
import { Replacement } from 'tslint';
33
import { expect } from 'chai';
4+
import { FsFileResolver } from '../src/angular/fileResolver/fsFileResolver';
5+
import { MetadataReader } from '../src/angular/metadataReader';
6+
import * as ts from 'typescript';
7+
import { ComponentMetadata } from '../src/angular/metadata';
8+
import chai = require('chai');
9+
10+
const getAst = (code: string, file = 'file.ts') => {
11+
return ts.createSourceFile(file, code, ts.ScriptTarget.ES5, true);
12+
};
413

514
describe('angular-whitespace', () => {
615
describe('success', () => {
@@ -86,7 +95,19 @@ describe('angular-whitespace', () => {
8695
assertSuccess('angular-whitespace', source, ['check-pipe']);
8796
});
8897

89-
98+
it('should work with external templates', () => {
99+
const code = `
100+
@Component({
101+
selector: 'foo',
102+
moduleId: module.id,
103+
templateUrl: 'valid.html',
104+
})
105+
class Bar {}
106+
`;
107+
const reader = new MetadataReader(new FsFileResolver());
108+
const ast = getAst(code, __dirname + '/../../test/fixtures/angularWhitespace/component.ts');
109+
assertSuccess('angular-whitespace', ast, ['check-pipe']);
110+
});
90111
});
91112
});
92113

Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div>{{ foo | async | uppercase }}</div>

test/testHelper.ts

+23-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import * as tslint from 'tslint';
22
import * as Lint from 'tslint';
33
import chai = require('chai');
4+
import * as ts from 'typescript';
5+
import { IOptions } from 'tslint';
6+
import { loadRules, convertRuleOptions } from './utils';
7+
8+
const fs = require('fs');
9+
const path = require('path');
410

511
interface ISourcePosition {
612
line: number;
@@ -26,7 +32,7 @@ export interface IExpectedFailure {
2632
* @param options additional options for the lint rule
2733
* @returns {LintResult} the result of linting
2834
*/
29-
function lint(ruleName: string, source: string, options: any): tslint.LintResult {
35+
function lint(ruleName: string, source: string | ts.SourceFile, options: any): tslint.LintResult {
3036
let configuration = {
3137
extends: [],
3238
rules: new Map<string, Partial<tslint.IOptions>>(),
@@ -46,7 +52,21 @@ function lint(ruleName: string, source: string, options: any): tslint.LintResult
4652
};
4753

4854
let linter = new tslint.Linter(linterOptions, undefined);
49-
linter.lint('file.ts', source, configuration);
55+
if (typeof source === 'string') {
56+
linter.lint('file.ts', source, configuration);
57+
} else {
58+
const rules = loadRules(convertRuleOptions(configuration.rules), linterOptions.rulesDirectory, false);
59+
const res = [].concat.apply([], rules.map(r => r.apply(source))) as tslint.RuleFailure[];
60+
const errCount = res.filter(r => !r.getRuleSeverity || r.getRuleSeverity() === 'error').length;
61+
return {
62+
errorCount: errCount,
63+
warningCount: res.length - errCount,
64+
output: '',
65+
format: null,
66+
fixes: [].concat.apply(res.map(r => r.getFix())),
67+
failures: res
68+
};
69+
}
5070
return linter.getResult();
5171
}
5272

@@ -235,7 +255,7 @@ export function assertFailures(ruleName: string, source: string, fails: IExpecte
235255
* @param source
236256
* @param options
237257
*/
238-
export function assertSuccess(ruleName: string, source: string, options = null) {
258+
export function assertSuccess(ruleName: string, source: string | ts.SourceFile, options = null) {
239259
const result = lint(ruleName, source, options);
240260
chai.assert.isTrue(result && result.failures.length === 0);
241261
}

test/utils.ts

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { IOptions, IRule } from 'tslint';
2+
import * as fs from 'fs';
3+
import * as path from 'path';
4+
5+
export function convertRuleOptions(ruleConfiguration: Map<string, Partial<IOptions>>): IOptions[] {
6+
const output: IOptions[] = [];
7+
ruleConfiguration.forEach(({ ruleArguments, ruleSeverity }, ruleName) => {
8+
const options: IOptions = {
9+
disabledIntervals: [], // deprecated, so just provide an empty array.
10+
ruleArguments: ruleArguments !== null ? ruleArguments : [],
11+
ruleName,
12+
ruleSeverity: ruleSeverity !== null ? ruleSeverity : 'error',
13+
};
14+
output.push(options);
15+
});
16+
return output;
17+
}
18+
19+
const cachedRules = new Map<string, any | 'not-found'>();
20+
21+
export function camelize(stringWithHyphens: string): string {
22+
return stringWithHyphens.replace(/-(.)/g, (_, nextLetter) => (nextLetter as string).toUpperCase());
23+
}
24+
25+
function transformName(name: string): string {
26+
// camelize strips out leading and trailing underscores and dashes, so make sure they aren't passed to camelize
27+
// the regex matches the groups (leading underscores and dashes)(other characters)(trailing underscores and dashes)
28+
const nameMatch = name.match(/^([-_]*)(.*?)([-_]*)$/);
29+
if (nameMatch === null) {
30+
return `${name}Rule`;
31+
}
32+
return `${nameMatch[1]}${camelize(nameMatch[2])}${nameMatch[3]}Rule`;
33+
}
34+
35+
/**
36+
* @param directory - An absolute path to a directory of rules
37+
* @param ruleName - A name of a rule in filename format. ex) "someLintRule"
38+
*/
39+
function loadRule(directory: string, ruleName: string): any | 'not-found' {
40+
const fullPath = path.join(directory, ruleName);
41+
if (fs.existsSync(`${fullPath}.js`)) {
42+
const ruleModule = require(fullPath) as { Rule: any } | undefined;
43+
if (ruleModule !== undefined) {
44+
return ruleModule.Rule;
45+
}
46+
}
47+
return 'not-found';
48+
}
49+
50+
export function getRelativePath(directory?: string | null, relativeTo?: string) {
51+
if (directory !== null) {
52+
const basePath = relativeTo !== undefined ? relativeTo : process.cwd();
53+
return path.resolve(basePath, directory);
54+
}
55+
return undefined;
56+
}
57+
58+
export function arrayify<T>(arg?: T | T[]): T[] {
59+
if (Array.isArray(arg)) {
60+
return arg;
61+
} else if (arg !== null) {
62+
return [arg];
63+
} else {
64+
return [];
65+
}
66+
}
67+
68+
function loadCachedRule(directory: string, ruleName: string, isCustomPath?: boolean): any | undefined {
69+
// use cached value if available
70+
const fullPath = path.join(directory, ruleName);
71+
const cachedRule = cachedRules.get(fullPath);
72+
if (cachedRule !== undefined) {
73+
return cachedRule === 'not-found' ? undefined : cachedRule;
74+
}
75+
76+
// get absolute path
77+
let absolutePath: string | undefined = directory;
78+
if (isCustomPath) {
79+
absolutePath = getRelativePath(directory);
80+
if (absolutePath !== undefined && !fs.existsSync(absolutePath)) {
81+
throw new Error(`Could not find custom rule directory: ${directory}`);
82+
}
83+
}
84+
85+
const Rule = absolutePath === undefined ? 'not-found' : loadRule(absolutePath, ruleName);
86+
87+
cachedRules.set(fullPath, Rule);
88+
return Rule === 'not-found' ? undefined : Rule;
89+
}
90+
91+
export function find<T, U>(inputs: T[], getResult: (t: T) => U | undefined): U | undefined {
92+
for (const element of inputs) {
93+
const result = getResult(element);
94+
if (result !== undefined) {
95+
return result;
96+
}
97+
}
98+
return undefined;
99+
}
100+
101+
function findRule(name: string, rulesDirectories?: string | string[]): any | undefined {
102+
const camelizedName = transformName(name);
103+
return find(arrayify(rulesDirectories), (dir) => loadCachedRule(dir, camelizedName, true));
104+
}
105+
106+
export function loadRules(ruleOptionsList: IOptions[],
107+
rulesDirectories?: string | string[],
108+
isJs = false): IRule[] {
109+
const rules: IRule[] = [];
110+
const notFoundRules: string[] = [];
111+
const notAllowedInJsRules: string[] = [];
112+
113+
for (const ruleOptions of ruleOptionsList) {
114+
if (ruleOptions.ruleSeverity === 'off') {
115+
// Perf: don't bother finding the rule if it's disabled.
116+
continue;
117+
}
118+
119+
const ruleName = ruleOptions.ruleName;
120+
const Rule = findRule(ruleName, rulesDirectories);
121+
if (Rule === undefined) {
122+
notFoundRules.push(ruleName);
123+
} else if (isJs && Rule.metadata !== undefined && Rule.metadata.typescriptOnly) {
124+
notAllowedInJsRules.push(ruleName);
125+
} else {
126+
const rule = new Rule(ruleOptions);
127+
if (rule.isEnabled()) {
128+
rules.push(rule);
129+
}
130+
}
131+
}
132+
return rules;
133+
}

0 commit comments

Comments
 (0)