Skip to content
This repository has been archived by the owner on Mar 25, 2021. It is now read-only.

Rewrite 'no-mergeable-namespace' to use AST walking instead of document highlights #2105

Merged
merged 3 commits into from
Jan 30, 2017
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
73 changes: 44 additions & 29 deletions src/rules/noMergeableNamespaceRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,48 +32,63 @@ export class Rule extends Lint.Rules.AbstractRule {
};
/* tslint:enable:object-literal-sort-keys */

public static failureStringFactory(identifier: string, locationToMerge: ts.LineAndCharacter): string {
return `Mergeable namespace ${identifier} found. Merge its contents with the namespace on line ${locationToMerge.line}.`;
public static failureStringFactory(name: string, seenBeforeLine: number) {
return `Mergeable namespace '${name}' found. Merge its contents with the namespace on line ${seenBeforeLine}.`;
}

public apply(sourceFile: ts.SourceFile, languageService: ts.LanguageService): Lint.RuleFailure[] {
const noMergeableNamespaceWalker = new NoMergeableNamespaceWalker(sourceFile, this.getOptions(), languageService);
return this.applyWithWalker(noMergeableNamespaceWalker);
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new Walker(sourceFile, this.getOptions()));
}
}

class NoMergeableNamespaceWalker extends Lint.RuleWalker {
constructor(sourceFile: ts.SourceFile, options: Lint.IOptions, private languageService: ts.LanguageService) {
super(sourceFile, options);
class Walker extends Lint.RuleWalker {
public visitSourceFile(node: ts.SourceFile) {
this.checkStatements(node.statements);
// All tree-walking handled by 'checkStatements'
}

public visitModuleDeclaration(node: ts.ModuleDeclaration) {
if (Lint.isNodeFlagSet(node, ts.NodeFlags.Namespace)
&& node.name.kind === ts.SyntaxKind.Identifier) {
this.validateReferencesForNamespace((node.name as ts.Identifier).text, node.name.getStart());
}
super.visitModuleDeclaration(node);
}
private checkStatements(statements: ts.Statement[]): void {
const seen = new Map<string, ts.NamespaceDeclaration>();

private validateReferencesForNamespace(name: string, position: number) {
const { fileName } = this.getSourceFile();
const highlights = this.languageService.getDocumentHighlights(fileName, position, [fileName]);
for (const statement of statements) {
if (statement.kind !== ts.SyntaxKind.ModuleDeclaration) {
continue;
}

const { name } = statement as ts.ModuleDeclaration;
if (name.kind === ts.SyntaxKind.Identifier) {
const { text } = name;
const prev = seen.get(text);
if (prev) {
this.addFailureAtNode(name, Rule.failureStringFactory(text, this.getLineOfNode(prev)));
}
seen.set(text, statement as ts.NamespaceDeclaration);
}

if (highlights == null || highlights[0].highlightSpans.length > 1) {
const failureString = Rule.failureStringFactory(name, this.findLocationToMerge(position, highlights[0].highlightSpans));
this.addFailureAt(position, name.length, failureString);
// Recursively check in all module declarations
this.checkModuleDeclaration(statement as ts.ModuleDeclaration);
}
}

private findLocationToMerge(currentPosition: number, highlightSpans: ts.HighlightSpan[]): ts.LineAndCharacter {
const { line } = this.getLineAndCharacterOfPosition(currentPosition);
private checkModuleDeclaration(decl: ts.ModuleDeclaration): void {
const { body } = decl;
if (!body) {
return;
}

for (const span of highlightSpans) {
const lineAndCharacter = this.getLineAndCharacterOfPosition(span.textSpan.start);
if (lineAndCharacter.line !== line) {
return lineAndCharacter;
}
switch (body.kind) {
case ts.SyntaxKind.ModuleBlock:
this.checkStatements(body.statements);
break;
case ts.SyntaxKind.ModuleDeclaration:
this.checkModuleDeclaration(body as ts.ModuleDeclaration);
break;
default:
break;
}
throw new Error("expected more than one highlightSpan");
}

private getLineOfNode(node: ts.Node): number {
return this.getLineAndCharacterOfPosition(node.pos).line;
}
}
29 changes: 24 additions & 5 deletions test/rules/no-mergeable-namespace/test.ts.lint
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,34 @@ module bar {}

// invalid case
namespace a {}
~ [Mergeable namespace a found. Merge its contents with the namespace on line 10.]
namespace a {}
~ [Mergeable namespace a found. Merge its contents with the namespace on line 9.]
~ [Mergeable namespace 'a' found. Merge its contents with the namespace on line 6.]

namespace b {
namespace b {}
~ [Mergeable namespace b found. Merge its contents with the namespace on line 14.]
namespace b {}
~ [Mergeable namespace b found. Merge its contents with the namespace on line 13.]
~ [Mergeable namespace 'b' found. Merge its contents with the namespace on line 12.]
namespace b {}
~ [Mergeable namespace b found. Merge its contents with the namespace on line 13.]
~ [Mergeable namespace 'b' found. Merge its contents with the namespace on line 13.]
}

namespace x.y {}
namespace x { namespace y{} }
~ [Mergeable namespace 'x' found. Merge its contents with the namespace on line 16.]

namespace m.n {
namespace l {}
namespace l {}
~ [Mergeable namespace 'l' found. Merge its contents with the namespace on line 21.]
}

module "foo" {
// Different than outer 'a'
namespace a {}

namespace q {}
namespace q {}
~ [Mergeable namespace 'q' found. Merge its contents with the namespace on line 28.]
}

[0]: This namespace has already been declared. Merge the declarations.