4 * Copyright 2014 Palantir Technologies, Inc.
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
18 Object.defineProperty(exports, "__esModule", { value: true });
19 var tslib_1 = require("tslib");
20 var semver = require("semver");
21 var utils = require("tsutils");
22 var ts = require("typescript");
23 var Lint = require("../index");
24 var OPTION_CHECK_PARAMETERS = "check-parameters";
25 var OPTION_IGNORE_PATTERN = "ignore-pattern";
26 var Rule = /** @class */ (function (_super) {
27 tslib_1.__extends(Rule, _super);
29 return _super !== null && _super.apply(this, arguments) || this;
31 /* tslint:enable:object-literal-sort-keys */
32 Rule.prototype.applyWithProgram = function (sourceFile, program) {
33 return this.applyWithFunction(sourceFile, walk, parseOptions(this.ruleArguments), program);
35 /* tslint:disable:object-literal-sort-keys */
37 ruleName: "no-unused-variable",
38 description: Lint.Utils.dedent(templateObject_1 || (templateObject_1 = tslib_1.__makeTemplateObject(["\n Disallows unused imports, variables, functions and\n private class members. Similar to tsc's --noUnusedParameters and --noUnusedLocals\n options, but does not interrupt code compilation."], ["\n Disallows unused imports, variables, functions and\n private class members. Similar to tsc's --noUnusedParameters and --noUnusedLocals\n options, but does not interrupt code compilation."]))),
39 descriptionDetails: Lint.Utils.dedent(templateObject_2 || (templateObject_2 = tslib_1.__makeTemplateObject(["\n In addition to avoiding compilation errors, this rule may still be useful if you\n wish to have `tslint` automatically remove unused imports, variables, functions,\n and private class members, when using TSLint's `--fix` option."], ["\n In addition to avoiding compilation errors, this rule may still be useful if you\n wish to have \\`tslint\\` automatically remove unused imports, variables, functions,\n and private class members, when using TSLint's \\`--fix\\` option."]))),
41 optionsDescription: Lint.Utils.dedent(templateObject_3 || (templateObject_3 = tslib_1.__makeTemplateObject(["\n Two optional arguments may be optionally provided:\n\n * `\"check-parameters\"` disallows unused function and constructor parameters.\n * NOTE: this option is experimental and does not work with classes\n that use abstract method declarations, among other things.\n * `{\"ignore-pattern\": \"pattern\"}` where pattern is a case-sensitive regexp.\n Variable names and imports that match the pattern will be ignored."], ["\n Two optional arguments may be optionally provided:\n\n * \\`\"check-parameters\"\\` disallows unused function and constructor parameters.\n * NOTE: this option is experimental and does not work with classes\n that use abstract method declarations, among other things.\n * \\`{\"ignore-pattern\": \"pattern\"}\\` where pattern is a case-sensitive regexp.\n Variable names and imports that match the pattern will be ignored."]))),
48 enum: ["check-parameters"],
53 "ignore-pattern": { type: "string" },
55 additionalProperties: false,
62 optionExamples: [true, [true, { "ignore-pattern": "^_" }]],
63 rationale: Lint.Utils.dedent(templateObject_4 || (templateObject_4 = tslib_1.__makeTemplateObject(["\n Variables that are declared and not used anywhere in code are likely an error due to incomplete refactoring.\n Such variables take up space in the code, are mild performance pains, and can lead to confusion by readers.\n "], ["\n Variables that are declared and not used anywhere in code are likely an error due to incomplete refactoring.\n Such variables take up space in the code, are mild performance pains, and can lead to confusion by readers.\n "]))),
64 type: "functionality",
66 requiresTypeInfo: true,
67 deprecationMessage: semver.gte(ts.version, "2.9.0-dev.0")
68 ? "Since TypeScript 2.9. Please use the built-in compiler checks instead."
72 }(Lint.Rules.TypedRule));
74 function parseOptions(options) {
75 var checkParameters = options.indexOf(OPTION_CHECK_PARAMETERS) !== -1;
77 for (var _i = 0, options_1 = options; _i < options_1.length; _i++) {
78 var o = options_1[_i];
79 if (typeof o === "object") {
80 // tslint:disable-next-line no-unsafe-any no-null-undefined-union
81 var ignore = o[OPTION_IGNORE_PATTERN];
82 if (ignore != undefined) {
83 ignorePattern = new RegExp(ignore);
88 return { checkParameters: checkParameters, ignorePattern: ignorePattern };
90 function walk(ctx, program) {
91 var sourceFile = ctx.sourceFile, _a = ctx.options, checkParameters = _a.checkParameters, ignorePattern = _a.ignorePattern;
92 var unusedCheckedProgram = getUnusedCheckedProgram(program, checkParameters);
93 var diagnostics = ts.getPreEmitDiagnostics(unusedCheckedProgram, sourceFile);
94 var checker = unusedCheckedProgram.getTypeChecker(); // Doesn't matter which program is used for this.
95 var declaration = program.getCompilerOptions().declaration;
96 // If all specifiers in an import are unused, we elide the entire import.
97 var importSpecifierFailures = new Map();
98 for (var _i = 0, diagnostics_1 = diagnostics; _i < diagnostics_1.length; _i++) {
99 var diag = diagnostics_1[_i];
100 if (diag.start === undefined) {
103 var kind = getUnusedDiagnostic(diag);
104 if (kind === undefined) {
107 var failure = ts.flattenDiagnosticMessageText(diag.messageText, "\n");
108 // BUG: this means imports / destructures with all (2+) unused variables don't respect ignore pattern
109 if (ignorePattern !== undefined &&
110 kind !== 2 /* DECLARATION */ &&
111 kind !== 3 /* ALL_DESTRUCTURES */) {
112 var varName = /'(.*)'/.exec(failure)[1];
113 if (ignorePattern.test(varName)) {
117 if (kind === 0 /* VARIABLE_OR_PARAMETER */ || kind === 2 /* DECLARATION */) {
118 var importNames = findImports(diag.start, sourceFile, kind);
119 if (importNames.length > 0) {
120 for (var _b = 0, importNames_1 = importNames; _b < importNames_1.length; _b++) {
121 var importName = importNames_1[_b];
122 if (declaration && isImportUsed(importName, sourceFile, checker)) {
125 if (importSpecifierFailures.has(importName)) {
126 throw new Error("Should not get 2 errors for the same import.");
128 importSpecifierFailures.set(importName, failure);
133 ctx.addFailureAt(diag.start, diag.length, failure);
135 if (importSpecifierFailures.size !== 0) {
136 addImportSpecifierFailures(ctx, importSpecifierFailures, sourceFile);
140 * Handle import-specifier failures separately.
141 * - If all of the import specifiers in an import are unused, add a combined failure for them all.
142 * - Unused imports are fixable.
144 function addImportSpecifierFailures(ctx, failures, sourceFile) {
145 forEachImport(sourceFile, function (importNode) {
146 if (importNode.kind === ts.SyntaxKind.ImportEqualsDeclaration) {
147 tryRemoveAll(importNode.name);
150 if (importNode.importClause === undefined) {
154 var _a = importNode.importClause, defaultName = _a.name, namedBindings = _a.namedBindings;
155 if (namedBindings !== undefined && namedBindings.kind === ts.SyntaxKind.NamespaceImport) {
156 tryRemoveAll(namedBindings.name);
159 var allNamedBindingsAreFailures = namedBindings === undefined || namedBindings.elements.every(function (e) { return failures.has(e.name); });
160 if (namedBindings !== undefined && allNamedBindingsAreFailures) {
161 for (var _i = 0, _b = namedBindings.elements; _i < _b.length; _i++) {
163 failures.delete(e.name);
166 if ((defaultName === undefined || failures.has(defaultName)) &&
167 allNamedBindingsAreFailures) {
168 if (defaultName !== undefined) {
169 failures.delete(defaultName);
171 removeAll(importNode, "All imports on this line are unused.");
174 if (defaultName !== undefined) {
175 var failure = tryDelete(defaultName);
176 if (failure !== undefined) {
177 var start = defaultName.getStart();
178 var end = namedBindings !== undefined
179 ? namedBindings.getStart()
180 : importNode.moduleSpecifier.getStart();
181 var fix = Lint.Replacement.deleteFromTo(start, end);
182 ctx.addFailureAtNode(defaultName, failure, fix);
185 if (namedBindings !== undefined) {
186 if (allNamedBindingsAreFailures) {
187 var start = defaultName !== undefined ? defaultName.getEnd() : namedBindings.getStart();
188 var fix = Lint.Replacement.deleteFromTo(start, namedBindings.getEnd());
189 var failure = "All named bindings are unused.";
190 ctx.addFailureAtNode(namedBindings, failure, fix);
193 var elements = namedBindings.elements;
194 for (var i = 0; i < elements.length; i++) {
195 var element = elements[i];
196 var failure = tryDelete(element.name);
197 if (failure === undefined) {
200 var prevElement = elements[i - 1];
201 var nextElement = elements[i + 1];
202 var start = prevElement !== undefined ? prevElement.getEnd() : element.getStart();
203 var end = nextElement !== undefined && prevElement == undefined
204 ? nextElement.getStart()
206 var fix = Lint.Replacement.deleteFromTo(start, end);
207 ctx.addFailureAtNode(element.name, failure, fix);
211 function tryRemoveAll(name) {
212 var failure = tryDelete(name);
213 if (failure !== undefined) {
214 removeAll(name, failure);
217 function removeAll(errorNode, failure) {
218 var start = importNode.getStart();
219 var end = importNode.getEnd();
220 utils.forEachToken(importNode, function (token) {
221 ts.forEachTrailingCommentRange(ctx.sourceFile.text, token.end, function (_, commentEnd, __) {
225 if (isEntireLine(start, end)) {
226 end = getNextLineStart(end);
228 var fix = Lint.Replacement.deleteFromTo(start, end);
229 ctx.addFailureAtNode(errorNode, failure, fix);
231 function isEntireLine(start, end) {
232 return (ctx.sourceFile.getLineAndCharacterOfPosition(start).character === 0 &&
233 ctx.sourceFile.getLineEndOfPosition(end) === end);
235 function getNextLineStart(position) {
236 var nextLine = ctx.sourceFile.getLineAndCharacterOfPosition(position).line + 1;
237 var lineStarts = ctx.sourceFile.getLineStarts();
238 if (nextLine < lineStarts.length) {
239 return lineStarts[nextLine];
246 if (failures.size !== 0) {
247 throw new Error("Should have revisited all import specifier failures.");
249 function tryDelete(name) {
250 var failure = failures.get(name);
251 if (failure !== undefined) {
252 failures.delete(name);
259 * Ignore this import if it's used as an implicit type somewhere.
260 * Workround for https://github.com/Microsoft/TypeScript/issues/9944
262 function isImportUsed(importSpecifier, sourceFile, checker) {
263 var importedSymbol = checker.getSymbolAtLocation(importSpecifier);
264 if (importedSymbol === undefined) {
267 var symbol = checker.getAliasedSymbol(importedSymbol);
268 if (!utils.isSymbolFlagSet(symbol, ts.SymbolFlags.Type)) {
271 return (ts.forEachChild(sourceFile, function cb(child) {
272 if (isImportLike(child)) {
275 var type = getImplicitType(child, checker);
276 // TODO: checker.typeEquals https://github.com/Microsoft/TypeScript/issues/13502
277 if (type !== undefined &&
278 checker.typeToString(type) === checker.symbolToString(symbol)) {
281 return ts.forEachChild(child, cb);
284 function getImplicitType(node, checker) {
285 if (((utils.isPropertyDeclaration(node) || utils.isVariableDeclaration(node)) &&
286 node.type === undefined &&
287 node.name.kind === ts.SyntaxKind.Identifier) ||
288 (utils.isBindingElement(node) && node.name.kind === ts.SyntaxKind.Identifier)) {
289 return checker.getTypeAtLocation(node);
291 else if (utils.isSignatureDeclaration(node) && node.type === undefined) {
292 var sig = checker.getSignatureFromDeclaration(node);
293 return sig === undefined ? undefined : sig.getReturnType();
299 function isImportLike(node) {
300 return (node.kind === ts.SyntaxKind.ImportDeclaration ||
301 node.kind === ts.SyntaxKind.ImportEqualsDeclaration);
303 function forEachImport(sourceFile, f) {
304 return ts.forEachChild(sourceFile, function (child) {
305 if (isImportLike(child)) {
307 if (res !== undefined) {
314 function findImports(pos, sourceFile, kind) {
315 var imports = forEachImport(sourceFile, function (i) {
316 if (!isInRange(i, pos)) {
319 if (i.kind === ts.SyntaxKind.ImportEqualsDeclaration) {
323 if (i.importClause === undefined) {
327 var _a = i.importClause, defaultName = _a.name, namedBindings = _a.namedBindings;
328 if (namedBindings !== undefined &&
329 namedBindings.kind === ts.SyntaxKind.NamespaceImport) {
330 return [namedBindings.name];
332 // Starting from TS2.8, when all imports in an import node are not used,
333 // TS emits only 1 diagnostic object for the whole line as opposed
334 // to the previous behavior of outputting a diagnostic with kind == 6192
335 // (UnusedKind.VARIABLE_OR_PARAMETER) for every unused import.
336 // From TS2.8, in the case of none of the imports in a line being used,
337 // the single diagnostic TS outputs are different between the 1 import
338 // and 2+ imports cases:
339 // - 1 import in node:
340 // - diagnostic has kind == 6133 (UnusedKind.VARIABLE_OR_PARAMETER)
341 // - the text range is the whole node (`import { ... } from "..."`)
342 // whereas pre-TS2.8, the text range was for the import node. so
343 // `name.getStart()` won't equal `pos` like in pre-TS2.8
344 // - 2+ imports in node:
345 // - diagnostic has kind == 6192 (UnusedKind.DECLARATION)
346 // - we know that all of these are unused
347 if (kind === 2 /* DECLARATION */) {
349 if (defaultName !== undefined) {
350 imp.push(defaultName);
352 if (namedBindings !== undefined) {
353 imp.push.apply(imp, namedBindings.elements.map(function (el) { return el.name; }));
355 return imp.length > 0 ? imp : undefined;
357 else if (defaultName !== undefined &&
358 (isInRange(defaultName, pos) || namedBindings === undefined) // defaultName is the only option
360 return [defaultName];
362 else if (namedBindings !== undefined) {
363 if (namedBindings.elements.length === 1) {
364 return [namedBindings.elements[0].name];
366 for (var _i = 0, _b = namedBindings.elements; _i < _b.length; _i++) {
367 var element = _b[_i];
368 if (isInRange(element, pos)) {
369 return [element.name];
376 return imports !== undefined ? imports : [];
378 function isInRange(range, pos) {
379 return range.pos <= pos && range.end >= pos;
381 function getUnusedDiagnostic(diag) {
382 // https://github.com/Microsoft/TypeScript/blob/master/src/compiler/diagnosticMessages.json
384 case 6133: // Pre TS 2.9 "'{0}' is declared but never used.
385 // TS 2.9+ "'{0}' is declared but its value is never read."
386 case 6196: // TS 2.9+ "'{0}' is declared but never used."
387 return 0 /* VARIABLE_OR_PARAMETER */;
389 return 1 /* PROPERTY */; // "Property '{0}' is declared but never used."
391 return 2 /* DECLARATION */; // "All imports in import declaration are unused."
393 return 3 /* ALL_DESTRUCTURES */; // "All destructured elements are unused."
398 var programToUnusedCheckedProgram = new WeakMap();
399 function getUnusedCheckedProgram(program, checkParameters) {
400 // Assuming checkParameters will always have the same value, so only lookup by program.
401 var checkedProgram = programToUnusedCheckedProgram.get(program);
402 if (checkedProgram !== undefined) {
403 return checkedProgram;
405 checkedProgram = makeUnusedCheckedProgram(program, checkParameters);
406 programToUnusedCheckedProgram.set(program, checkedProgram);
407 return checkedProgram;
409 function makeUnusedCheckedProgram(program, checkParameters) {
410 var originalOptions = program.getCompilerOptions();
411 var options = tslib_1.__assign({}, originalOptions, { noEmit: true, noUnusedLocals: true, noUnusedParameters: originalOptions.noUnusedParameters || checkParameters });
412 var sourceFilesByName = new Map(program
414 .map(function (s) { return [getCanonicalFileName(s.fileName), s]; }));
415 // tslint:disable object-literal-sort-keys
416 return ts.createProgram(Array.from(sourceFilesByName.keys()), options, {
417 fileExists: function (f) { return sourceFilesByName.has(getCanonicalFileName(f)); },
418 readFile: function (f) { return sourceFilesByName.get(getCanonicalFileName(f)).text; },
419 getSourceFile: function (f) { return sourceFilesByName.get(getCanonicalFileName(f)); },
420 getDefaultLibFileName: function () { return ts.getDefaultLibFileName(options); },
421 writeFile: function () { return undefined; },
422 getCurrentDirectory: function () { return ""; },
423 getDirectories: function () { return []; },
424 getCanonicalFileName: getCanonicalFileName,
425 useCaseSensitiveFileNames: function () { return ts.sys.useCaseSensitiveFileNames; },
426 getNewLine: function () { return "\n"; },
428 // tslint:enable object-literal-sort-keys
429 // We need to be careful with file system case sensitivity
430 function getCanonicalFileName(fileName) {
431 return ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase();
434 var templateObject_1, templateObject_2, templateObject_3, templateObject_4;