Skip to content

Commit

Permalink
fix: proper position in html/css source file
Browse files Browse the repository at this point in the history
Fix #343
  • Loading branch information
mgechev committed Jun 21, 2017
1 parent 859ff99 commit c503510
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 6 deletions.
8 changes: 7 additions & 1 deletion src/angular/ngWalker.ts
Expand Up @@ -193,7 +193,13 @@ export class NgWalker extends Lint.RuleWalker {
if (!path) {
return current;
}
return ts.createSourceFile(path, `\`${content}\``, ts.ScriptTarget.ES5);
const sf = ts.createSourceFile(path, `\`${content}\``, ts.ScriptTarget.ES5);
const original = sf.getFullText;
sf.getFullText = function () {
const text = original.apply(sf);
return text.substring(1, text.length - 1);
}.bind(sf);
return sf;
}
}

2 changes: 1 addition & 1 deletion src/angularWhitespaceRule.ts
Expand Up @@ -179,7 +179,7 @@ export class Rule extends Lint.Rules.AbstractRule {

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

public apply(sourceFile:ts.SourceFile): Lint.RuleFailure[] {
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(
new NgWalker(sourceFile,
this.getOptions(), {
Expand Down
23 changes: 22 additions & 1 deletion test/angularWhitespaceRule.spec.ts
@@ -1,6 +1,15 @@
import { assertSuccess, assertAnnotated, assertMultipleAnnotated } from './testHelper';
import { Replacement } from 'tslint';
import { expect } from 'chai';
import { FsFileResolver } from '../src/angular/fileResolver/fsFileResolver';
import { MetadataReader } from '../src/angular/metadataReader';
import * as ts from 'typescript';
import { ComponentMetadata } from '../src/angular/metadata';
import chai = require('chai');

const getAst = (code: string, file = 'file.ts') => {
return ts.createSourceFile(file, code, ts.ScriptTarget.ES5, true);
};

describe('angular-whitespace', () => {
describe('success', () => {
Expand Down Expand Up @@ -86,7 +95,19 @@ describe('angular-whitespace', () => {
assertSuccess('angular-whitespace', source, ['check-pipe']);
});


it('should work with external templates', () => {
const code = `
@Component({
selector: 'foo',
moduleId: module.id,
templateUrl: 'valid.html',
})
class Bar {}
`;
const reader = new MetadataReader(new FsFileResolver());
const ast = getAst(code, __dirname + '/../../test/fixtures/angularWhitespace/component.ts');
assertSuccess('angular-whitespace', ast, ['check-pipe']);
});
});
});

Expand Down
1 change: 1 addition & 0 deletions test/fixtures/angularWhitespace/valid.html
@@ -0,0 +1 @@
<div>{{ foo | async | uppercase }}</div>
26 changes: 23 additions & 3 deletions test/testHelper.ts
@@ -1,6 +1,12 @@
import * as tslint from 'tslint';
import * as Lint from 'tslint';
import chai = require('chai');
import * as ts from 'typescript';
import { IOptions } from 'tslint';
import { loadRules, convertRuleOptions } from './utils';

const fs = require('fs');
const path = require('path');

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

let linter = new tslint.Linter(linterOptions, undefined);
linter.lint('file.ts', source, configuration);
if (typeof source === 'string') {
linter.lint('file.ts', source, configuration);
} else {
const rules = loadRules(convertRuleOptions(configuration.rules), linterOptions.rulesDirectory, false);
const res = [].concat.apply([], rules.map(r => r.apply(source))) as tslint.RuleFailure[];
const errCount = res.filter(r => !r.getRuleSeverity || r.getRuleSeverity() === 'error').length;
return {
errorCount: errCount,
warningCount: res.length - errCount,
output: '',
format: null,
fixes: [].concat.apply(res.map(r => r.getFix())),
failures: res
};
}
return linter.getResult();
}

Expand Down Expand Up @@ -235,7 +255,7 @@ export function assertFailures(ruleName: string, source: string, fails: IExpecte
* @param source
* @param options
*/
export function assertSuccess(ruleName: string, source: string, options = null) {
export function assertSuccess(ruleName: string, source: string | ts.SourceFile, options = null) {
const result = lint(ruleName, source, options);
chai.assert.isTrue(result && result.failures.length === 0);
}
133 changes: 133 additions & 0 deletions test/utils.ts
@@ -0,0 +1,133 @@
import { IOptions, IRule } from 'tslint';
import * as fs from 'fs';
import * as path from 'path';

export function convertRuleOptions(ruleConfiguration: Map<string, Partial<IOptions>>): IOptions[] {
const output: IOptions[] = [];
ruleConfiguration.forEach(({ ruleArguments, ruleSeverity }, ruleName) => {
const options: IOptions = {
disabledIntervals: [], // deprecated, so just provide an empty array.
ruleArguments: ruleArguments !== null ? ruleArguments : [],
ruleName,
ruleSeverity: ruleSeverity !== null ? ruleSeverity : 'error',
};
output.push(options);
});
return output;
}

const cachedRules = new Map<string, any | 'not-found'>();

export function camelize(stringWithHyphens: string): string {
return stringWithHyphens.replace(/-(.)/g, (_, nextLetter) => (nextLetter as string).toUpperCase());
}

function transformName(name: string): string {
// camelize strips out leading and trailing underscores and dashes, so make sure they aren't passed to camelize
// the regex matches the groups (leading underscores and dashes)(other characters)(trailing underscores and dashes)
const nameMatch = name.match(/^([-_]*)(.*?)([-_]*)$/);
if (nameMatch === null) {
return `${name}Rule`;
}
return `${nameMatch[1]}${camelize(nameMatch[2])}${nameMatch[3]}Rule`;
}

/**
* @param directory - An absolute path to a directory of rules
* @param ruleName - A name of a rule in filename format. ex) "someLintRule"
*/
function loadRule(directory: string, ruleName: string): any | 'not-found' {
const fullPath = path.join(directory, ruleName);
if (fs.existsSync(`${fullPath}.js`)) {
const ruleModule = require(fullPath) as { Rule: any } | undefined;
if (ruleModule !== undefined) {
return ruleModule.Rule;
}
}
return 'not-found';
}

export function getRelativePath(directory?: string | null, relativeTo?: string) {
if (directory !== null) {
const basePath = relativeTo !== undefined ? relativeTo : process.cwd();
return path.resolve(basePath, directory);
}
return undefined;
}

export function arrayify<T>(arg?: T | T[]): T[] {
if (Array.isArray(arg)) {
return arg;
} else if (arg !== null) {
return [arg];
} else {
return [];
}
}

function loadCachedRule(directory: string, ruleName: string, isCustomPath?: boolean): any | undefined {
// use cached value if available
const fullPath = path.join(directory, ruleName);
const cachedRule = cachedRules.get(fullPath);
if (cachedRule !== undefined) {
return cachedRule === 'not-found' ? undefined : cachedRule;
}

// get absolute path
let absolutePath: string | undefined = directory;
if (isCustomPath) {
absolutePath = getRelativePath(directory);
if (absolutePath !== undefined && !fs.existsSync(absolutePath)) {
throw new Error(`Could not find custom rule directory: ${directory}`);
}
}

const Rule = absolutePath === undefined ? 'not-found' : loadRule(absolutePath, ruleName);

cachedRules.set(fullPath, Rule);
return Rule === 'not-found' ? undefined : Rule;
}

export function find<T, U>(inputs: T[], getResult: (t: T) => U | undefined): U | undefined {
for (const element of inputs) {
const result = getResult(element);
if (result !== undefined) {
return result;
}
}
return undefined;
}

function findRule(name: string, rulesDirectories?: string | string[]): any | undefined {
const camelizedName = transformName(name);
return find(arrayify(rulesDirectories), (dir) => loadCachedRule(dir, camelizedName, true));
}

export function loadRules(ruleOptionsList: IOptions[],
rulesDirectories?: string | string[],
isJs = false): IRule[] {
const rules: IRule[] = [];
const notFoundRules: string[] = [];
const notAllowedInJsRules: string[] = [];

for (const ruleOptions of ruleOptionsList) {
if (ruleOptions.ruleSeverity === 'off') {
// Perf: don't bother finding the rule if it's disabled.
continue;
}

const ruleName = ruleOptions.ruleName;
const Rule = findRule(ruleName, rulesDirectories);
if (Rule === undefined) {
notFoundRules.push(ruleName);
} else if (isJs && Rule.metadata !== undefined && Rule.metadata.typescriptOnly) {
notAllowedInJsRules.push(ruleName);
} else {
const rule = new Rule(ruleOptions);
if (rule.isEnabled()) {
rules.push(rule);
}
}
}
return rules;
}

0 comments on commit c503510

Please sign in to comment.