"use strict"; /** * @license * Copyright 2014 Palantir Technologies, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); var tslib_1 = require("tslib"); var utils = require("tsutils"); var ts = require("typescript"); var Lint = require("../index"); var OPTION_CHECK_PARAMETERS = "check-parameters"; var OPTION_IGNORE_PATTERN = "ignore-pattern"; var Rule = (function (_super) { tslib_1.__extends(Rule, _super); function Rule() { return _super !== null && _super.apply(this, arguments) || this; } /* tslint:enable:object-literal-sort-keys */ Rule.prototype.applyWithProgram = function (sourceFile, program) { var _this = this; var x = program.getCompilerOptions(); if (x.noUnusedLocals && x.noUnusedParameters) { console.warn("WARNING: 'no-unused-variable' lint rule does not need to be set if " + "the 'no-unused-locals' and 'no-unused-parameters' compiler options are enabled."); } return this.applyWithFunction(sourceFile, function (ctx) { return walk(ctx, program, parseOptions(_this.ruleArguments)); }); }; /* tslint:disable:object-literal-sort-keys */ Rule.metadata = { ruleName: "no-unused-variable", description: (_a = ["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."], _a.raw = ["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."], Lint.Utils.dedent(_a)), hasFix: true, optionsDescription: (_b = ["\n Three 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 that match the pattern will be ignored."], _b.raw = ["\n Three 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 that match the pattern will be ignored."], Lint.Utils.dedent(_b)), options: { type: "array", items: { oneOf: [ { type: "string", enum: ["check-parameters"], }, { type: "object", properties: { "ignore-pattern": { type: "string" }, }, additionalProperties: false, }, ], }, minLength: 0, maxLength: 3, }, optionExamples: [true, [true, { "ignore-pattern": "^_" }]], type: "functionality", typescriptOnly: true, requiresTypeInfo: true, }; return Rule; }(Lint.Rules.TypedRule)); exports.Rule = Rule; function parseOptions(options) { var checkParameters = options.indexOf(OPTION_CHECK_PARAMETERS) !== -1; var ignorePattern; for (var _i = 0, options_1 = options; _i < options_1.length; _i++) { var o = options_1[_i]; if (typeof o === "object") { // tslint:disable-next-line no-unsafe-any var ignore = o[OPTION_IGNORE_PATTERN]; if (ignore != null) { ignorePattern = new RegExp(ignore); break; } } } return { checkParameters: checkParameters, ignorePattern: ignorePattern }; } function walk(ctx, program, _a) { var checkParameters = _a.checkParameters, ignorePattern = _a.ignorePattern; var sourceFile = ctx.sourceFile; var unusedCheckedProgram = getUnusedCheckedProgram(program, checkParameters); var diagnostics = ts.getPreEmitDiagnostics(unusedCheckedProgram, sourceFile); var checker = unusedCheckedProgram.getTypeChecker(); // Doesn't matter which program is used for this. var declaration = program.getCompilerOptions().declaration; // If all specifiers in an import are unused, we elide the entire import. var importSpecifierFailures = new Map(); for (var _i = 0, diagnostics_1 = diagnostics; _i < diagnostics_1.length; _i++) { var diag = diagnostics_1[_i]; if (diag.start === undefined) { continue; } var kind = getUnusedDiagnostic(diag); if (kind === undefined) { continue; } var failure = ts.flattenDiagnosticMessageText(diag.messageText, "\n"); if (kind === 0 /* VARIABLE_OR_PARAMETER */) { var importName = findImport(diag.start, sourceFile); if (importName !== undefined) { if (declaration && isImportUsed(importName, sourceFile, checker)) { continue; } if (importSpecifierFailures.has(importName)) { throw new Error("Should not get 2 errors for the same import."); } importSpecifierFailures.set(importName, failure); continue; } } if (ignorePattern !== undefined) { var varName = /'(.*)'/.exec(failure)[1]; if (ignorePattern.test(varName)) { continue; } } ctx.addFailureAt(diag.start, diag.length, failure); } if (importSpecifierFailures.size !== 0) { addImportSpecifierFailures(ctx, importSpecifierFailures, sourceFile); } } /** * Handle import-specifier failures separately. * - If all of the import specifiers in an import are unused, add a combined failure for them all. * - Unused imports are fixable. */ function addImportSpecifierFailures(ctx, failures, sourceFile) { forEachImport(sourceFile, function (importNode) { if (importNode.kind === ts.SyntaxKind.ImportEqualsDeclaration) { tryRemoveAll(importNode.name); return; } if (importNode.importClause === undefined) { // Error node return; } var _a = importNode.importClause, defaultName = _a.name, namedBindings = _a.namedBindings; if (namedBindings !== undefined && namedBindings.kind === ts.SyntaxKind.NamespaceImport) { tryRemoveAll(namedBindings.name); return; } var allNamedBindingsAreFailures = namedBindings === undefined || namedBindings.elements.every(function (e) { return failures.has(e.name); }); if (namedBindings !== undefined && allNamedBindingsAreFailures) { for (var _i = 0, _b = namedBindings.elements; _i < _b.length; _i++) { var e = _b[_i]; failures.delete(e.name); } } if ((defaultName === undefined || failures.has(defaultName)) && allNamedBindingsAreFailures) { if (defaultName !== undefined) { failures.delete(defaultName); } removeAll(importNode, "All imports are unused."); return; } if (defaultName !== undefined) { var failure = tryDelete(defaultName); if (failure !== undefined) { var start = defaultName.getStart(); var end = namedBindings !== undefined ? namedBindings.getStart() : importNode.moduleSpecifier.getStart(); var fix = Lint.Replacement.deleteFromTo(start, end); ctx.addFailureAtNode(defaultName, failure, fix); } } if (namedBindings !== undefined) { if (allNamedBindingsAreFailures) { var start = defaultName !== undefined ? defaultName.getEnd() : namedBindings.getStart(); var fix = Lint.Replacement.deleteFromTo(start, namedBindings.getEnd()); var failure = "All named bindings are unused."; ctx.addFailureAtNode(namedBindings, failure, fix); } else { var elements = namedBindings.elements; for (var i = 0; i < elements.length; i++) { var element = elements[i]; var failure = tryDelete(element.name); if (failure === undefined) { continue; } var prevElement = elements[i - 1]; var nextElement = elements[i + 1]; var start = prevElement !== undefined ? prevElement.getEnd() : element.getStart(); var end = nextElement !== undefined && prevElement == undefined ? nextElement.getStart() : element.getEnd(); var fix = Lint.Replacement.deleteFromTo(start, end); ctx.addFailureAtNode(element.name, failure, fix); } } } function tryRemoveAll(name) { var failure = tryDelete(name); if (failure !== undefined) { removeAll(name, failure); } } function removeAll(errorNode, failure) { var start = importNode.getStart(); var end = importNode.getEnd(); if (isEntireLine(start, end)) { end = getNextLineStart(end); } var fix = Lint.Replacement.deleteFromTo(start, end); ctx.addFailureAtNode(errorNode, failure, fix); } function isEntireLine(start, end) { return ctx.sourceFile.getLineAndCharacterOfPosition(start).character === 0 && ctx.sourceFile.getLineEndOfPosition(end) === end; } function getNextLineStart(position) { var nextLine = ctx.sourceFile.getLineAndCharacterOfPosition(position).line + 1; var lineStarts = ctx.sourceFile.getLineStarts(); if (nextLine < lineStarts.length) { return lineStarts[nextLine]; } else { return position; } } }); if (failures.size !== 0) { throw new Error("Should have revisited all import specifier failures."); } function tryDelete(name) { var failure = failures.get(name); if (failure !== undefined) { failures.delete(name); return failure; } return undefined; } } /** * Ignore this import if it's used as an implicit type somewhere. * Workround for https://github.com/Microsoft/TypeScript/issues/9944 */ function isImportUsed(importSpecifier, sourceFile, checker) { var importedSymbol = checker.getSymbolAtLocation(importSpecifier); if (importedSymbol === undefined) { return false; } var symbol = checker.getAliasedSymbol(importedSymbol); if (!Lint.isSymbolFlagSet(symbol, ts.SymbolFlags.Type)) { return false; } return ts.forEachChild(sourceFile, function cb(child) { if (isImportLike(child)) { return false; } var type = getImplicitType(child, checker); // TODO: checker.typeEquals https://github.com/Microsoft/TypeScript/issues/13502 if (type !== undefined && checker.typeToString(type) === checker.symbolToString(symbol)) { return true; } return ts.forEachChild(child, cb); }) === true; } function getImplicitType(node, checker) { if ((utils.isPropertyDeclaration(node) || utils.isVariableDeclaration(node)) && node.type === undefined && node.name.kind === ts.SyntaxKind.Identifier || utils.isBindingElement(node) && node.name.kind === ts.SyntaxKind.Identifier) { return checker.getTypeAtLocation(node); } else if (utils.isSignatureDeclaration(node) && node.type === undefined) { var sig = checker.getSignatureFromDeclaration(node); return sig === undefined ? undefined : sig.getReturnType(); } else { return undefined; } } function isImportLike(node) { return node.kind === ts.SyntaxKind.ImportDeclaration || node.kind === ts.SyntaxKind.ImportEqualsDeclaration; } function forEachImport(sourceFile, f) { return ts.forEachChild(sourceFile, function (child) { if (isImportLike(child)) { var res = f(child); if (res !== undefined) { return res; } } return undefined; }); } function findImport(pos, sourceFile) { return forEachImport(sourceFile, function (i) { if (i.kind === ts.SyntaxKind.ImportEqualsDeclaration) { if (i.name.getStart() === pos) { return i.name; } } else { if (i.importClause === undefined) { // Error node return undefined; } var _a = i.importClause, defaultName = _a.name, namedBindings = _a.namedBindings; if (namedBindings !== undefined && namedBindings.kind === ts.SyntaxKind.NamespaceImport) { var name = namedBindings.name; if (name.getStart() === pos) { return name; } return undefined; } if (defaultName !== undefined && defaultName.getStart() === pos) { return defaultName; } else if (namedBindings !== undefined) { for (var _i = 0, _b = namedBindings.elements; _i < _b.length; _i++) { var name = _b[_i].name; if (name.getStart() === pos) { return name; } } } } return undefined; }); } function getUnusedDiagnostic(diag) { switch (diag.code) { case 6133: return 0 /* VARIABLE_OR_PARAMETER */; // "'{0}' is declared but never used. case 6138: return 1 /* PROPERTY */; // "Property '{0}' is declared but never used." default: return undefined; } } var programToUnusedCheckedProgram = new WeakMap(); function getUnusedCheckedProgram(program, checkParameters) { // Assuming checkParameters will always have the same value, so only lookup by program. var checkedProgram = programToUnusedCheckedProgram.get(program); if (checkedProgram !== undefined) { return checkedProgram; } checkedProgram = makeUnusedCheckedProgram(program, checkParameters); programToUnusedCheckedProgram.set(program, checkedProgram); return checkedProgram; } function makeUnusedCheckedProgram(program, checkParameters) { var originalOptions = program.getCompilerOptions(); var options = tslib_1.__assign({}, originalOptions, { noEmit: true, noUnusedLocals: true, noUnusedParameters: originalOptions.noUnusedParameters || checkParameters }); var sourceFilesByName = new Map(program.getSourceFiles().map(function (s) { return [getCanonicalFileName(s.fileName), s]; })); // tslint:disable object-literal-sort-keys return ts.createProgram(Array.from(sourceFilesByName.keys()), options, { fileExists: function (f) { return sourceFilesByName.has(getCanonicalFileName(f)); }, readFile: function (f) { return sourceFilesByName.get(getCanonicalFileName(f)).text; }, getSourceFile: function (f) { return sourceFilesByName.get(getCanonicalFileName(f)); }, getDefaultLibFileName: function () { return ts.getDefaultLibFileName(options); }, writeFile: function () { }, getCurrentDirectory: function () { return ""; }, getDirectories: function () { return []; }, getCanonicalFileName: getCanonicalFileName, useCaseSensitiveFileNames: function () { return ts.sys.useCaseSensitiveFileNames; }, getNewLine: function () { return "\n"; }, }); // tslint:enable object-literal-sort-keys // We need to be careful with file system case sensitivity function getCanonicalFileName(fileName) { return ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); } } var _a, _b;