UNPKG

5.25 kBPlain TextView Raw
1import * as ts from 'typescript';
2import * as Lint from 'tslint';
3
4import { ErrorTolerantWalker } from './utils/ErrorTolerantWalker';
5import { ExtendedMetadata } from './utils/ExtendedMetadata';
6import { forEachTokenWithTrivia } from 'tsutils';
7
8const FAILURE_STRING: string = 'No commented out code.';
9
10/**
11 * Implementation of the no-commented-out-code rule.
12 */
13export class Rule extends Lint.Rules.AbstractRule {
14 public static metadata: ExtendedMetadata = {
15 ruleName: 'no-commented-out-code',
16 type: 'maintainability', // one of: 'functionality' | 'maintainability' | 'style' | 'typescript'
17 description: 'Code must not be commented out.',
18 options: null,
19 optionsDescription: '',
20 optionExamples: [], // Remove this property if the rule has no options
21 typescriptOnly: false,
22 issueClass: 'Non-SDL', // one of: 'SDL' | 'Non-SDL' | 'Ignored'
23 issueType: 'Warning', // one of: 'Error' | 'Warning'
24 severity: 'Low', // one of: 'Critical' | 'Important' | 'Moderate' | 'Low'
25 level: 'Opportunity for Excellence', // one of 'Mandatory' | 'Opportunity for Excellence'
26 group: 'Clarity', // one of 'Ignored' | 'Security' | 'Correctness' | 'Clarity' | 'Whitespace' | 'Configurable' | 'Deprecated'
27 commonWeaknessEnumeration: '', // if possible, please map your rule to a CWE (see cwe_descriptions.json and https://cwe.mitre.org)
28 };
29
30 public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
31 return this.applyWithWalker(new NoCommentedOutCodeRuleWalker(sourceFile, this.getOptions()));
32 }
33}
34
35class NoCommentedOutCodeRuleWalker extends ErrorTolerantWalker {
36 public visitSourceFile(node: ts.SourceFile) {
37 forEachTokenWithTrivia(node, (text, tokenSyntaxKind, range) => {
38 if (tokenSyntaxKind === ts.SyntaxKind.SingleLineCommentTrivia || tokenSyntaxKind === ts.SyntaxKind.MultiLineCommentTrivia) {
39 this.scanCommentForCode(range.pos, text.substring(range.pos, range.end));
40 }
41 });
42 }
43
44 private scanCommentForCode(startPosition: number, commentText: string) {
45 const trimmedCommentText = this.trimTextLines(commentText);
46
47 if (this.isTextCode(trimmedCommentText)) {
48 this.handleSuspiciousComment(startPosition, commentText);
49 }
50 }
51
52 /**
53 * Removes spaces and comment delimiters at beginning
54 * and end of each line
55 */
56 private trimTextLines(text: string): string {
57 const lines = text.split('\n');
58
59 const trimAtStart = /^\s*\/*\**\s*/;
60 const trimAtEnd = /\s*\**\/*\s*$/;
61
62 const trimmedLines = lines.map(line => {
63 return line.replace(trimAtStart, '').replace(trimAtEnd, '');
64 });
65
66 return trimmedLines.join('\n');
67 }
68
69 private isTextCode(text: string): boolean {
70 if (this.isTextSingleWord(text)) {
71 return false;
72 }
73 if (this.isTextTsLintFlag(text)) {
74 return false;
75 }
76 if (this.isTextToDoLikeNote(text)) {
77 return false;
78 }
79
80 return this.isTextCodeWithoutErrors(text);
81 }
82
83 private isTextSingleWord(text: string): boolean {
84 const pattern = new RegExp('^([\\w-]*)$');
85 return pattern.test(text.trim());
86 }
87
88 /**
89 * TSLint flags will be will be parsed as labeled statements and thus
90 * may result in valid code, so they need to be handled separately
91 */
92 private isTextTsLintFlag(text: string): boolean {
93 return text.startsWith('tslint:');
94 }
95
96 /**
97 * These notes followed by a colon will be parsed as labeled statements
98 * and thus may result in valid code, so they need to be handled separately
99 */
100 private isTextToDoLikeNote(text: string): boolean {
101 return /^(NOTE|TODO|FIXME|BUG|HACK|XXX):/.test(text);
102 }
103
104 /**
105 * If text contains statements but not one error, it must be code
106 */
107 private isTextCodeWithoutErrors(text: string) {
108 const sourceFile = this.createSourceFileFromText(text);
109
110 if (!this.hasSourceFileStatements(sourceFile)) {
111 return false;
112 }
113
114 const sourceFileDiagnostics = this.getSourceFileDiagnostics(sourceFile);
115
116 return sourceFileDiagnostics.length === 0;
117 }
118
119 private createSourceFileFromText(text: string): ts.SourceFile {
120 return ts.createSourceFile('', text, ts.ScriptTarget.ES5, true);
121 }
122
123 private hasSourceFileStatements(sourceFile: ts.SourceFile): boolean {
124 return sourceFile && sourceFile.statements.length > 0;
125 }
126
127 /**
128 * The most efficient way to get a source file's diagnostics is from parseDiagnostics,
129 * which isn't exposed in the API, since the cast to any.
130 * @see https://github.com/Microsoft/TypeScript/issues/21940
131 * Tried using ts.Program.getSyntacticDiagnostics + getDeclarationDiagnostics, which
132 * wasn't quiet as fast.
133 */
134 private getSourceFileDiagnostics(sourceFile: ts.SourceFile): ts.Diagnostic[] {
135 return (<any>sourceFile).parseDiagnostics;
136 }
137
138 private handleSuspiciousComment(startPosition: number, commentText: string) {
139 this.addFailureAt(startPosition, commentText.length, FAILURE_STRING);
140 }
141}