diff --git a/.eslintrc.json b/.eslintrc.json index 819d042c25b16..2e4ab50888aeb 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -81,6 +81,7 @@ { "selector": "enumMember", "format": ["camelCase", "PascalCase"], "leadingUnderscore": "allow", "filter": { "regex": "^[A-Za-z]+_[A-Za-z]+$", "match": false } }, { "selector": "property", "format": null } ], + "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }], // Rules enabled in typescript-eslint configs that are not applicable here "@typescript-eslint/ban-ts-comment": "off", @@ -91,6 +92,7 @@ "@typescript-eslint/no-namespace": "off", "@typescript-eslint/no-non-null-asserted-optional-chain": "off", "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/ban-types": [ @@ -106,9 +108,6 @@ } ], - // Todo: For each of these, investigate whether we want to enable them ✨ - "@typescript-eslint/no-unused-vars": "off", - // Pending https://github.com/typescript-eslint/typescript-eslint/issues/4820 "@typescript-eslint/prefer-optional-chain": "off", @@ -125,6 +124,8 @@ "local/debug-assert": "error", "local/no-keywords": "error", "local/jsdoc-format": "error", + // Solely for marking things as used for the purposes of the no-unused-vars rule. + "local/mark-jsdoc-types-used": "error", // eslint-plugin-no-null "no-null/no-null": "error", diff --git a/scripts/eslint/rules/mark-jsdoc-types-used.cjs b/scripts/eslint/rules/mark-jsdoc-types-used.cjs new file mode 100644 index 0000000000000..570c0ff115b5b --- /dev/null +++ b/scripts/eslint/rules/mark-jsdoc-types-used.cjs @@ -0,0 +1,102 @@ +const { TSESTree, ESLintUtils } = require("@typescript-eslint/utils"); +const { createRule } = require("./utils.cjs"); +const ts = require("typescript"); + +/** + * @param {ts.Identifier} node + */ +function isBareIdentifier(node) { + if (ts.isPropertyAccessExpression(node.parent)) { + return node.parent.expression === node; + } + + if (ts.isQualifiedName(node.parent)) { + return node.parent.left === node; + } + + // TODO(jakebailey): surely I'm missing something here. + // Also, how does eslint deal with type space versus value space? Or does it not? + + return true; +} + +module.exports = createRule({ + name: "mark-jsdoc-types-used", + meta: { + docs: { + description: ``, + }, + messages: {}, + schema: [], + type: "problem", + fixable: "whitespace", + }, + defaultOptions: [], + + create(context) { + /** @type {(node: TSESTree.Node) => void} */ + const checkProgram = node => { + const parserServices = ESLintUtils.getParserServices(context, /*allowWithoutFullTypeInformation*/ true); + const ast = parserServices.esTreeNodeToTSNodeMap.get(node); + + // TODO(jakebailey): I almost guarantee this is overzealous and wrong in some way. + + /** + * @param {ts.Node} node + */ + function markIdentifiersUsed(node) { + if (ts.isIdentifier(node) && isBareIdentifier(node)) { + context.markVariableAsUsed(node.text); + return; + } + ts.forEachChild(node, markIdentifiersUsed); + } + + /** + * @param {ts.Node} node + */ + function visit(node) { + const jsDoc = ts.getJSDocTags(node); + for (const tag of jsDoc) { + if (ts.isJSDocTypeTag(tag)) { + markIdentifiersUsed(tag.typeExpression); + } + else if (ts.isJSDocTypedefTag(tag) || ts.isJSDocPropertyLikeTag(tag) || ts.isJSDocReturnTag(tag) || ts.isJSDocThrowsTag(tag)) { + if (tag.typeExpression) { + markIdentifiersUsed(tag.typeExpression); + } + } + else if (ts.isJSDocTemplateTag(tag)) { + if (tag.constraint) { + markIdentifiersUsed(tag.constraint); + } + // tag.typeParameters? + } + else if (ts.isJSDocEnumTag(tag)) { + markIdentifiersUsed(tag.typeExpression); + } + else if (ts.isJSDocUnknownTag(tag)) { + // Ignore. + // TODO(jakebailey): Something's wrong here, though, becuase we're getting @parameter tags as unknown. See new code in parser. + } + else if (ts.isJSDocOverrideTag(tag) || ts.isJSDocDeprecatedTag(tag)) { + // Ignore. + } + else if (ts.isJSDocSeeTag(tag)) { + markIdentifiersUsed(tag.tagName); + } + else { + throw new Error(`Unexpected node kind: ${ts.SyntaxKind[tag.kind]} ${tag.getText()}}`); + } + } + ts.forEachChild(node, visit); + } + + visit(ast); + }; + + return { + Program: checkProgram, + }; + }, +}); diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 9738920df7527..9b354bae8e037 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -286,7 +286,7 @@ export function filter(array: T[], f: (x: T) => boolean): T[]; /** @internal */ export function filter(array: readonly T[], f: (x: T) => x is U): readonly U[]; /** @internal */ -export function filter(array: readonly T[], f: (x: T) => boolean): readonly T[]; +export function filter(array: readonly T[], f: (x: T) => boolean): readonly T[]; /** @internal */ export function filter(array: T[] | undefined, f: (x: T) => x is U): U[] | undefined; /** @internal */ @@ -294,7 +294,7 @@ export function filter(array: T[] | undefined, f: (x: T) => boolean): T[] | u /** @internal */ export function filter(array: readonly T[] | undefined, f: (x: T) => x is U): readonly U[] | undefined; /** @internal */ -export function filter(array: readonly T[] | undefined, f: (x: T) => boolean): readonly T[] | undefined; +export function filter(array: readonly T[] | undefined, f: (x: T) => boolean): readonly T[] | undefined; /** @internal */ export function filter(array: readonly T[] | undefined, f: (x: T) => boolean): readonly T[] | undefined { if (array) { @@ -830,7 +830,7 @@ export function insertSorted(array: SortedArray, insert: T, compare: Compa } /** @internal */ -export function sortAndDeduplicate(array: readonly string[]): SortedReadonlyArray; +export function sortAndDeduplicate(array: readonly string[]): SortedReadonlyArray; /** @internal */ export function sortAndDeduplicate(array: readonly T[], comparer: Comparer, equalityComparer?: EqualityComparer): SortedReadonlyArray; /** @internal */ diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 7b7f0fcfa8143..6842fb1c8c71c 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -4131,6 +4131,7 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri writeSpace(); nextPos = emitTokenWithComment(SyntaxKind.AsKeyword, nextPos, writeKeyword, node); writeSpace(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars nextPos = emitTokenWithComment(SyntaxKind.NamespaceKeyword, nextPos, writeKeyword, node); writeSpace(); emit(node.name); diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index db767177462e8..eeff8c96c7bbf 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -348,7 +348,7 @@ interface PackageJson extends PackageJsonPathFields { version?: string; } -function readPackageJsonField>(jsonContent: PackageJson, fieldName: K, typeOfTag: "string", state: ModuleResolutionState): PackageJson[K] | undefined; +function readPackageJsonField>(jsonContent: PackageJson, fieldName: K, typeOfTag: "string", state: ModuleResolutionState): PackageJson[K] | undefined; function readPackageJsonField>(jsonContent: PackageJson, fieldName: K, typeOfTag: "object", state: ModuleResolutionState): PackageJson[K] | undefined; function readPackageJsonField(jsonContent: PackageJson, fieldName: K, typeOfTag: "string" | "object", state: ModuleResolutionState): PackageJson[K] | undefined { if (!hasProperty(jsonContent, fieldName)) { diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 2f411c435492a..e434a6bf49cd9 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -9043,6 +9043,7 @@ namespace Parser { case "arg": case "argument": case "param": + case "parameter": // TODO(jakebailey): why is this missing? return parseParameterOrPropertyTag(start, tagName, PropertyLikeParse.Parameter, margin); case "return": case "returns": diff --git a/src/compiler/watchPublic.ts b/src/compiler/watchPublic.ts index 420b59071266b..0a95aeafdab37 100644 --- a/src/compiler/watchPublic.ts +++ b/src/compiler/watchPublic.ts @@ -258,18 +258,18 @@ export interface ProgramHost { getModuleResolutionCache?(): ModuleResolutionCache | undefined; jsDocParsingMode?: JSDocParsingMode; -} -/** - * Internal interface used to wire emit through same host - * - * @internal - */ -export interface ProgramHost { + + // Internal interface used to wire emit through same host + // TODO: GH#18217 Optional methods are frequently asserted + /** @internal */ createDirectory?(path: string): void; + /** @internal */ writeFile?(path: string, data: string, writeByteOrderMark?: boolean): void; // For testing + /** @internal */ storeFilesChangingSignatureDuringEmit?: boolean; + /** @internal */ now?(): Date; } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index cf6c72efaca0a..969a11f969f9f 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -504,6 +504,7 @@ export function convertScriptKindName(scriptKindName: protocol.ScriptKindName) { /** @internal */ export function convertUserPreferences(preferences: protocol.UserPreferences): UserPreferences { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { lazyConfiguredProjectsFromExternalProject, ...userPreferences } = preferences; return userPreferences; } diff --git a/src/tsconfig-base.json b/src/tsconfig-base.json index 3b772f7d33f0f..34c76b9542b13 100644 --- a/src/tsconfig-base.json +++ b/src/tsconfig-base.json @@ -21,8 +21,6 @@ "useUnknownInCatchVariables": false, "noImplicitOverride": true, - "noUnusedLocals": true, - "noUnusedParameters": true, "allowUnusedLabels": false, "skipLibCheck": true,