diff --git a/.changeset/serious-eels-think.md b/.changeset/serious-eels-think.md new file mode 100644 index 0000000000..cb718cad07 --- /dev/null +++ b/.changeset/serious-eels-think.md @@ -0,0 +1,9 @@ +--- +"@definitelytyped/definitions-parser": patch +"@definitelytyped/eslint-plugin": patch +"@definitelytyped/header-parser": patch +"@definitelytyped/publisher": patch +"@definitelytyped/dtslint": patch +--- + +Allow packages to test multiple tsconfigs by specifying list of tsconfigs in package.json diff --git a/packages/definitions-parser/test/utils.ts b/packages/definitions-parser/test/utils.ts index 66a587f3ac..6f7ecff79a 100644 --- a/packages/definitions-parser/test/utils.ts +++ b/packages/definitions-parser/test/utils.ts @@ -23,6 +23,7 @@ export function createTypingsVersionRaw( minimumTypeScriptVersion: "2.3", nonNpm: false, projects: ["zombo.com"], + tsconfigs: ["tsconfig.json"], }, typesVersions: [], license: License.MIT, diff --git a/packages/dtslint/src/checks.ts b/packages/dtslint/src/checks.ts index f6ce55db41..d6f232d59e 100644 --- a/packages/dtslint/src/checks.ts +++ b/packages/dtslint/src/checks.ts @@ -33,7 +33,7 @@ interface Tsconfig { exclude?: string[]; } -export function checkTsconfig(dirPath: string, config: Tsconfig): string[] { +export function checkTsconfig(config: Tsconfig): string[] { const errors = []; const mustHave = { noEmit: true, @@ -140,13 +140,13 @@ export function checkTsconfig(dirPath: string, config: Tsconfig): string[] { if (options.paths) { for (const key in options.paths) { if (options.paths[key].length !== 1) { - errors.push(`${dirPath}/tsconfig.json: "paths" must map each module specifier to only one file.`); + errors.push(`"paths" must map each module specifier to only one file.`); } const [target] = options.paths[key]; if (target !== "./index.d.ts") { const m = target.match(/^(?:..\/)+([^\/]+)\/(?:v\d+\.?\d*\/)?index.d.ts$/); if (!m || m[1] !== key) { - errors.push(`${dirPath}/tsconfig.json: "paths" must map '${key}' to ${key}'s index.d.ts.`); + errors.push(`"paths" must map '${key}' to ${key}'s index.d.ts.`); } } } diff --git a/packages/dtslint/src/index.ts b/packages/dtslint/src/index.ts index 34c3355822..791104c020 100644 --- a/packages/dtslint/src/index.ts +++ b/packages/dtslint/src/index.ts @@ -195,6 +195,7 @@ async function runTests( const tsVersion = tsLocal ? "local" : TypeScriptVersion.latest; const testTypesResult = await testTypesVersion( dirPath, + packageJson.tsconfigs, tsVersion, tsVersion, expectOnly, @@ -222,7 +223,15 @@ async function runTests( if (lows.length > 1) { console.log("testing from", low, "to", hi, "in", versionPath); } - const testTypesResult = await testTypesVersion(versionPath, low, hi, expectOnly, undefined, isLatest); + const testTypesResult = await testTypesVersion( + versionPath, + packageJson.tsconfigs, + low, + hi, + expectOnly, + undefined, + isLatest, + ); errors.push(...testTypesResult.errors); } } @@ -264,6 +273,7 @@ function next(v: TypeScriptVersion): TypeScriptVersion { async function testTypesVersion( dirPath: string, + tsconfigs: readonly string[], lowVersion: TsVersion, hiVersion: TsVersion, expectOnly: boolean, @@ -273,10 +283,15 @@ async function testTypesVersion( const errors = []; const checkExpectedFilesResult = checkExpectedFiles(dirPath, isLatest); errors.push(...checkExpectedFilesResult.errors); - const tsconfigErrors = checkTsconfig(dirPath, getCompilerOptions(dirPath)); - if (tsconfigErrors.length > 0) { - errors.push("\n\t* " + tsconfigErrors.join("\n\t* ")); + + for (const tsconfig of tsconfigs) { + const tsconfigPath = joinPaths(dirPath, tsconfig); + const tsconfigErrors = checkTsconfig(getCompilerOptions(tsconfigPath)); + if (tsconfigErrors.length > 0) { + errors.push("\n\t* " + tsconfigPath + ":\n\t* " + tsconfigErrors.join("\n\t* ")); + } } + const err = await lint(dirPath, lowVersion, hiVersion, isLatest, expectOnly, tsLocal); if (err) { errors.push(err); diff --git a/packages/dtslint/src/util.ts b/packages/dtslint/src/util.ts index 205f71cd30..60b596ffba 100644 --- a/packages/dtslint/src/util.ts +++ b/packages/dtslint/src/util.ts @@ -1,6 +1,6 @@ import { createGitHubStringSetGetter, joinPaths } from "@definitelytyped/utils"; import fs from "fs"; -import { basename, dirname, join } from "path"; +import { basename, dirname } from "path"; import stripJsonComments = require("strip-json-comments"); import * as ts from "typescript"; @@ -19,15 +19,14 @@ export function readJson(path: string) { return JSON.parse(stripJsonComments(text)); } -export function getCompilerOptions(dirPath: string): { +export function getCompilerOptions(tsconfigPath: string): { compilerOptions: ts.CompilerOptions; files?: string[]; includes?: string[]; excludes?: string[]; } { - const tsconfigPath = join(dirPath, "tsconfig.json"); if (!fs.existsSync(tsconfigPath)) { - throw new Error(`Need a 'tsconfig.json' file in ${dirPath}`); + throw new Error(`${tsconfigPath} does not exist`); } return readJson(tsconfigPath) as { compilerOptions: ts.CompilerOptions; diff --git a/packages/dtslint/test/index.test.ts b/packages/dtslint/test/index.test.ts index e68e6d39f4..af07e11ec7 100644 --- a/packages/dtslint/test/index.test.ts +++ b/packages/dtslint/test/index.test.ts @@ -20,132 +20,121 @@ describe("dtslint", () => { describe("checks", () => { describe("checkTsconfig", () => { it("disallows unknown compiler options", () => { - expect(checkTsconfig("test", based({ completelyInvented: true }))).toEqual([ + expect(checkTsconfig(based({ completelyInvented: true }))).toEqual([ "Unexpected compiler option completelyInvented", ]); }); it("allows exactOptionalPropertyTypes: true", () => { - expect(checkTsconfig("test", based({ exactOptionalPropertyTypes: true }))).toEqual([]); + expect(checkTsconfig(based({ exactOptionalPropertyTypes: true }))).toEqual([]); }); it("allows module: node16", () => { - expect(checkTsconfig("test", based({ module: "node16" }))).toEqual([]); + expect(checkTsconfig(based({ module: "node16" }))).toEqual([]); }); it("allows `paths`", () => { - expect(checkTsconfig("test", based({ paths: { boom: ["../boom/index.d.ts"] } }))).toEqual([]); + expect(checkTsconfig(based({ paths: { boom: ["../boom/index.d.ts"] } }))).toEqual([]); }); it("disallows missing `module`", () => { const compilerOptions = { ...base }; delete compilerOptions.module; - expect(checkTsconfig("test", { compilerOptions, files: ["index.d.ts", "base.test.ts"] })).toEqual([ + expect(checkTsconfig({ compilerOptions, files: ["index.d.ts", "base.test.ts"] })).toEqual([ 'Must specify "module" to `"module": "commonjs"` or `"module": "node16"`.', ]); }); it("disallows exactOptionalPropertyTypes: false", () => { - expect(checkTsconfig("test", based({ exactOptionalPropertyTypes: false }))).toEqual([ + expect(checkTsconfig(based({ exactOptionalPropertyTypes: false }))).toEqual([ 'When "exactOptionalPropertyTypes" is present, it must be set to `true`.', ]); }); it("allows paths: self-reference", () => { - expect(checkTsconfig("react-native", based({ paths: { "react-native": ["./index.d.ts"] } }))).toEqual([]); + expect(checkTsconfig(based({ paths: { "react-native": ["./index.d.ts"] } }))).toEqual([]); }); it("allows paths: matching ../reference/index.d.ts", () => { - expect( - checkTsconfig("reactive-dep", based({ paths: { "react-native": ["../react-native/index.d.ts"] } })), - ).toEqual([]); + expect(checkTsconfig(based({ paths: { "react-native": ["../react-native/index.d.ts"] } }))).toEqual([]); expect( checkTsconfig( - "reactive-dep", based({ paths: { "react-native": ["../react-native/index.d.ts"], react: ["../react/v16/index.d.ts"] } }), ), ).toEqual([]); }); it("forbids paths: mapping to multiple things", () => { expect( - checkTsconfig( - "reactive-dep", - based({ paths: { "react-native": ["./index.d.ts", "../react-native/v0.68/index.d.ts"] } }), - ), - ).toEqual([`reactive-dep/tsconfig.json: "paths" must map each module specifier to only one file.`]); + checkTsconfig(based({ paths: { "react-native": ["./index.d.ts", "../react-native/v0.68/index.d.ts"] } })), + ).toEqual([`"paths" must map each module specifier to only one file.`]); }); it("allows paths: matching ../reference/version/index.d.ts", () => { - expect(checkTsconfig("reactive-dep", based({ paths: { react: ["../react/v16/index.d.ts"] } }))).toEqual([]); - expect( - checkTsconfig("reactive-dep", based({ paths: { "react-native": ["../react-native/v0.69/index.d.ts"] } })), - ).toEqual([]); - expect( - checkTsconfig( - "reactive-dep/v1", - based({ paths: { "react-native": ["../../react-native/v0.69/index.d.ts"] } }), - ), - ).toEqual([]); + expect(checkTsconfig(based({ paths: { react: ["../react/v16/index.d.ts"] } }))).toEqual([]); + expect(checkTsconfig(based({ paths: { "react-native": ["../react-native/v0.69/index.d.ts"] } }))).toEqual([]); + expect(checkTsconfig(based({ paths: { "react-native": ["../../react-native/v0.69/index.d.ts"] } }))).toEqual( + [], + ); }); it("forbids paths: mapping to self-contained file", () => { - expect(checkTsconfig("rrrr", based({ paths: { "react-native": ["./other.d.ts"] } }))).toEqual([ - `rrrr/tsconfig.json: "paths" must map 'react-native' to react-native's index.d.ts.`, + expect(checkTsconfig(based({ paths: { "react-native": ["./other.d.ts"] } }))).toEqual([ + `"paths" must map 'react-native' to react-native's index.d.ts.`, ]); }); it("forbids paths: mismatching ../NOT/index.d.ts", () => { - expect(checkTsconfig("rrrr", based({ paths: { "react-native": ["../cocoa/index.d.ts"] } }))).toEqual([ - `rrrr/tsconfig.json: "paths" must map 'react-native' to react-native's index.d.ts.`, + expect(checkTsconfig(based({ paths: { "react-native": ["../cocoa/index.d.ts"] } }))).toEqual([ + `"paths" must map 'react-native' to react-native's index.d.ts.`, ]); }); it("forbids paths: mismatching ../react-native/NOT.d.ts", () => { - expect(checkTsconfig("rrrr", based({ paths: { "react-native": ["../react-native/other.d.ts"] } }))).toEqual([ - `rrrr/tsconfig.json: "paths" must map 'react-native' to react-native's index.d.ts.`, + expect(checkTsconfig(based({ paths: { "react-native": ["../react-native/other.d.ts"] } }))).toEqual([ + `"paths" must map 'react-native' to react-native's index.d.ts.`, ]); }); it("forbids paths: mismatching ../react-native/NOT/index.d.ts", () => { - expect( - checkTsconfig("rrrr", based({ paths: { "react-native": ["../react-native/deep/index.d.ts"] } })), - ).toEqual([`rrrr/tsconfig.json: "paths" must map 'react-native' to react-native's index.d.ts.`]); + expect(checkTsconfig(based({ paths: { "react-native": ["../react-native/deep/index.d.ts"] } }))).toEqual([ + `"paths" must map 'react-native' to react-native's index.d.ts.`, + ]); }); it("forbids paths: mismatching ../react-native/version/NOT/index.d.ts", () => { - expect( - checkTsconfig("rrrr", based({ paths: { "react-native": ["../react-native/v0.68/deep/index.d.ts"] } })), - ).toEqual([`rrrr/tsconfig.json: "paths" must map 'react-native' to react-native's index.d.ts.`]); + expect(checkTsconfig(based({ paths: { "react-native": ["../react-native/v0.68/deep/index.d.ts"] } }))).toEqual([ + `"paths" must map 'react-native' to react-native's index.d.ts.`, + ]); }); it("forbids paths: mismatching ../react-native/version/NOT.d.ts", () => { - expect( - checkTsconfig("rrrr", based({ paths: { "react-native": ["../react-native/v0.70/other.d.ts"] } })), - ).toEqual([`rrrr/tsconfig.json: "paths" must map 'react-native' to react-native's index.d.ts.`]); + expect(checkTsconfig(based({ paths: { "react-native": ["../react-native/v0.70/other.d.ts"] } }))).toEqual([ + `"paths" must map 'react-native' to react-native's index.d.ts.`, + ]); }); it("Forbids exclude", () => { - expect(checkTsconfig("exclude", { compilerOptions: base, exclude: ["**/node_modules"] })).toEqual([ + expect(checkTsconfig({ compilerOptions: base, exclude: ["**/node_modules"] })).toEqual([ `Use "files" instead of "exclude".`, ]); }); it("Forbids include", () => { - expect(checkTsconfig("include", { compilerOptions: base, include: ["**/node_modules"] })).toEqual([ + expect(checkTsconfig({ compilerOptions: base, include: ["**/node_modules"] })).toEqual([ `Use "files" instead of "include".`, ]); }); it("Requires files", () => { - expect(checkTsconfig("include", { compilerOptions: base })).toEqual([`Must specify "files".`]); + expect(checkTsconfig({ compilerOptions: base })).toEqual([`Must specify "files".`]); }); it("Requires files to contain index.d.ts", () => { - expect( - checkTsconfig("include", { compilerOptions: base, files: ["package-name.d.ts", "package-name.test.ts"] }), - ).toEqual([`"files" list must include "index.d.ts".`]); + expect(checkTsconfig({ compilerOptions: base, files: ["package-name.d.ts", "package-name.test.ts"] })).toEqual([ + `"files" list must include "index.d.ts".`, + ]); }); // it("Requires files to contain .[mc]ts file", () => { - // expect(checkTsconfig("include", { compilerOptions: base, files: ["index.d.ts"] })).toEqual([ + // expect(checkTsconfig({ compilerOptions: base, files: ["index.d.ts"] })).toEqual([ // `"files" list must include at least one ".ts", ".tsx", ".mts" or ".cts" file for testing.`, // ]); // }); it("Allows files to contain index.d.ts plus a .tsx", () => { - expect(checkTsconfig("include", { compilerOptions: base, files: ["index.d.ts", "tests.tsx"] })).toEqual([]); + expect(checkTsconfig({ compilerOptions: base, files: ["index.d.ts", "tests.tsx"] })).toEqual([]); }); it("Allows files to contain index.d.ts plus a .mts", () => { - expect(checkTsconfig("include", { compilerOptions: base, files: ["index.d.ts", "tests.mts"] })).toEqual([]); + expect(checkTsconfig({ compilerOptions: base, files: ["index.d.ts", "tests.mts"] })).toEqual([]); }); it("Allows files to contain index.d.ts plus a .cts", () => { - expect(checkTsconfig("include", { compilerOptions: base, files: ["index.d.ts", "tests.cts"] })).toEqual([]); + expect(checkTsconfig({ compilerOptions: base, files: ["index.d.ts", "tests.cts"] })).toEqual([]); }); it("Allows files to contain ./index.d.ts plus a ./.tsx", () => { - expect(checkTsconfig("include", { compilerOptions: base, files: ["./index.d.ts", "./tests.tsx"] })).toEqual([]); + expect(checkTsconfig({ compilerOptions: base, files: ["./index.d.ts", "./tests.tsx"] })).toEqual([]); }); it("Issues both errors on empty files list", () => { - expect(checkTsconfig("include", { compilerOptions: base, files: [] })).toEqual([ + expect(checkTsconfig({ compilerOptions: base, files: [] })).toEqual([ `"files" list must include "index.d.ts".`, // `"files" list must include at least one ".ts", ".tsx", ".mts" or ".cts" file for testing.`, ]); diff --git a/packages/eslint-plugin/src/rules/expect.ts b/packages/eslint-plugin/src/rules/expect.ts index 2964b4debc..730dde8079 100644 --- a/packages/eslint-plugin/src/rules/expect.ts +++ b/packages/eslint-plugin/src/rules/expect.ts @@ -6,7 +6,6 @@ import fs from "fs"; import { ReportDescriptorMessageData } from "@typescript-eslint/utils/ts-eslint"; type TSModule = typeof ts; -const builtinTypeScript = require("typescript") as TSModule; const rule = createRule({ name: "expect", @@ -20,9 +19,7 @@ const rule = createRule({ twoAssertions: "This line has 2 $ExpectType assertions.", failure: `TypeScript{{versionNameString}} expected type to be:\n {{expectedType}}\ngot:\n {{actualType}}`, diagnostic: `TypeScript{{versionNameString}} {{message}}`, - programContents: - `Program source files differ between TypeScript versions. This may be a dtslint bug.\n` + - `Expected to find a file '{{fileName}}' present in ${builtinTypeScript.versionMajorMinor}, but did not find it in ts@{{versionName}}.`, + noTsconfigMatch: `TypeScript{{versionNameString}} could not find a tsconfig that includes this file.`, noMatch: "Cannot match a node to this assertion. If this is a multiline function call, ensure the assertion is on the line above.", needInstall: `A module look-up failed, this often occurs when you need to run \`pnpm install\` on a dependent module before you can lint. @@ -33,29 +30,15 @@ Before you debug, first try running: Then re-run.`, }, - schema: [ - { - type: "object", - properties: { - versionsToTest: { - type: "array", - items: { - type: "object", - properties: { - versionName: { type: "string" }, - path: { type: "string" }, - }, - required: ["versionName", "path"], - additionalProperties: false, - }, - }, - }, - additionalProperties: false, - }, - ], + schema: [], }, defaultOptions: [{}], create(context) { + const pkg = findTypesPackage(context.filename); + if (!pkg) { + return {}; + } + const tsconfigPath = findUp(context.filename, (dir) => { const tsconfig = path.join(dir, "tsconfig.json"); return fs.existsSync(tsconfig) ? tsconfig : undefined; @@ -81,42 +64,76 @@ Then re-run.`, const fileName = parserServices.esTreeNodeToTSNodeMap.get(node).fileName; const getLocFromIndex = (index: number) => context.sourceCode.getLocFromIndex(index); - const toReport = new Map & { versions: Set }>(); - const reporter: Reporter = ({ versionName, messageId, data, loc }) => { - const key = JSON.stringify({ messageId, data, loc }); - let existing = toReport.get(key); - if (existing === undefined) { - toReport.set(key, (existing = { messageId, data, loc, versions: new Set() })); - } - existing.versions.add(versionName); - }; + const settings = getSettings(context); + let { versionsToTest } = settings; let reportDiagnostics = true; - let { versionsToTest } = getSettings(context); if (!versionsToTest) { // In the editor, just use the built-in install of TypeScript. versionsToTest = [{ versionName: "", path: require.resolve("typescript") }]; reportDiagnostics = false; } + let tsconfigs: readonly string[] = ["tsconfig.json"]; + let reportTsconfigName = false; + if (pkg.packageJson.tsconfigs) { + tsconfigs = pkg.packageJson.tsconfigs; + reportTsconfigName = true; + // If we're using alternative tsconfigs, the editor may not error on them. + reportDiagnostics = true; + } + + const toReport = new Map & { runs: Set }>(); + for (const version of versionsToTest) { - const ts = require(version.path) as TSModule; - const program = getProgram(tsconfigPath, ts, version.versionName, parserServices.program); - walk( - getLocFromIndex, - reporter, - fileName, - program, - ts, - version.versionName, - /*nextHigherVersion*/ undefined, - dirPath, - reportDiagnostics, - ); + let found = false; + for (const tsconfigPath of tsconfigs) { + const ts = require(version.path) as TSModule; + const program = getProgram(dirPath, tsconfigPath, ts, version.versionName, parserServices.program); + + const sourceFile = program.getSourceFile(fileName)!; + if (!sourceFile) { + continue; + } + + found = true; + + const report: Reporter = ({ messageId, data, loc }) => { + const key = JSON.stringify({ messageId, data, loc }); + let existing = toReport.get(key); + if (existing === undefined) { + toReport.set(key, (existing = { messageId, data, loc, runs: new Set() })); + } + existing.runs.add(`${version.versionName} ${tsconfigPath}`); + }; + + walk( + getLocFromIndex, + report, + fileName, + sourceFile, + program, + ts, + version.versionName, + /*nextHigherVersion*/ undefined, + dirPath, + reportDiagnostics, + ); + } + + if (!found) { + context.report({ + messageId: "noTsconfigMatch", + loc: zeroSourceLocation, + }); + } } - for (const { messageId, data, loc, versions } of toReport.values()) { - const versionNames = [...versions].sort().join(", "); + for (const { messageId, data, loc, runs } of toReport.values()) { + const versionNames = [...runs] + .sort() + .map((s) => (reportTsconfigName ? s : s.split(" ")[0]).trim()) + .join(", "); context.report({ messageId, data: { ...data, versionNameString: versionNames ? `@${versionNames}` : "" }, @@ -128,6 +145,8 @@ Then re-run.`, }, }); +type VersionAndTsconfig = `${string} ${string}`; + interface VersionToTest { readonly versionName: string; readonly path: string; @@ -138,18 +157,17 @@ interface Settings { } function getSettings(context: Parameters<(typeof rule)["create"]>[0]): Settings { - const dt = context.settings.dt; - if (!dt || typeof dt !== "object") { - return {}; + const dt = context.settings.dt ?? {}; + if (typeof dt !== "object") { + throw new Error("Invalid dt settings"); } - let versionsToTest = (dt as Record).versionsToTest; - versionsToTest ??= undefined; - if (!Array.isArray(versionsToTest)) { + const versionsToTest = (dt as Record).versionsToTest ?? undefined; + if (versionsToTest !== undefined && !Array.isArray(versionsToTest)) { throw new Error("Invalid versionsToTest"); } - for (const version of versionsToTest) { + for (const version of versionsToTest ?? []) { if (typeof version !== "object" || typeof version.versionName !== "string" || typeof version.path !== "string") { throw new Error("Invalid version to test"); } @@ -158,19 +176,26 @@ function getSettings(context: Parameters<(typeof rule)["create"]>[0]): Settings return { versionsToTest }; } -const programCache = new WeakMap>(); +const programCache = new WeakMap>(); /** Maps a ts.Program to one created with the version specified in `options`. */ -function getProgram(configFile: string, ts: TSModule, versionName: string, lintProgram: ts.Program): ts.Program { +function getProgram( + dirPath: string, + configFile: string, + ts: TSModule, + versionName: string, + lintProgram: ts.Program, +): ts.Program { let versionToProgram = programCache.get(lintProgram); if (versionToProgram === undefined) { - versionToProgram = new Map(); + versionToProgram = new Map(); programCache.set(lintProgram, versionToProgram); } - let newProgram = versionToProgram.get(versionName); + const cacheKey: VersionAndTsconfig = `${configFile} ${versionName}`; + let newProgram = versionToProgram.get(cacheKey); if (newProgram === undefined) { - newProgram = createProgram(configFile, ts); - versionToProgram.set(versionName, newProgram); + newProgram = createProgram(path.resolve(dirPath, configFile), ts); + versionToProgram.set(cacheKey, newProgram); } return newProgram; } @@ -202,7 +227,6 @@ function createProgram(configFile: string, ts: TSModule): ts.Program { type MessageIds = keyof (typeof rule)["meta"]["messages"]; interface ReporterInfo { - versionName: string; messageId: MessageIds; data?: ReportDescriptorMessageData; loc: Readonly; @@ -219,6 +243,7 @@ function walk( getLocFromIndex: (index: number) => Readonly, report: Reporter, fileName: string, + sourceFile: ts.SourceFile, program: ts.Program, ts: TSModule, versionName: string, @@ -226,17 +251,6 @@ function walk( dirPath: string, reportDiagnostics: boolean, ): void { - const sourceFile = program.getSourceFile(fileName)!; - if (!sourceFile) { - report({ - versionName, - messageId: "programContents", - data: { fileName, versionName }, - loc: zeroSourceLocation, - }); - return; - } - const checker = program.getTypeChecker(); if (reportDiagnostics) { @@ -263,7 +277,6 @@ function walk( if (dtRoot) { const dirPath = path.relative(dtRoot, path.dirname(packageInfo.dir)); report({ - versionName, messageId: "needInstall", data: { dirPath }, loc: zeroSourceLocation, @@ -280,13 +293,12 @@ function walk( const { typeAssertions, duplicates } = parseAssertions(sourceFile); for (const line of duplicates) { - addFailureAtLine(report, { versionName, messageId: "twoAssertions" }, line); + addFailureAtLine(report, { messageId: "twoAssertions" }, line); } const { unmetExpectations, unusedAssertions } = getExpectTypeFailures(sourceFile, typeAssertions, checker, ts); for (const { node, expected, actual } of unmetExpectations) { report({ - versionName, messageId: "failure", data: { expectedType: expected, @@ -302,7 +314,6 @@ function walk( addFailureAtLine( report, { - versionName, messageId: "noMatch", }, line - 1, @@ -314,7 +325,6 @@ function walk( if (diagnostic.file === sourceFile) { const msg = `${intro}\n${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`; report({ - versionName, messageId: "diagnostic", data: { message: msg }, loc: { @@ -324,7 +334,6 @@ function walk( }); } else { report({ - versionName, messageId: "diagnostic", data: { message: `${intro}\n${fileName}${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}` }, loc: zeroSourceLocation, diff --git a/packages/eslint-plugin/src/util.ts b/packages/eslint-plugin/src/util.ts index 6297f0899c..6ede893324 100644 --- a/packages/eslint-plugin/src/util.ts +++ b/packages/eslint-plugin/src/util.ts @@ -67,6 +67,7 @@ export interface PackageJSON { owners: string[]; dependencies?: Record; devDependencies?: Record; + tsconfigs?: string[]; } // TODO(jakebailey): pull this helper out to util package? diff --git a/packages/eslint-plugin/test/__file_snapshots__/types/expect-tsconfigs/expect-dom-tests.ts.lint b/packages/eslint-plugin/test/__file_snapshots__/types/expect-tsconfigs/expect-dom-tests.ts.lint new file mode 100644 index 0000000000..1c6cde52db --- /dev/null +++ b/packages/eslint-plugin/test/__file_snapshots__/types/expect-tsconfigs/expect-dom-tests.ts.lint @@ -0,0 +1,17 @@ +types/expect-tsconfigs/expect-dom-tests.ts + 6:16 error TypeScript@tsconfig.dom.json compile error: +Cannot find name 'WeakSet'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later @definitelytyped/expect + +✖ 1 problem (1 error, 0 warnings) + +==== types/expect-tsconfigs/expect-dom-tests.ts ==== + + // eslint-disable-next-line @definitelytyped/no-relative-import-in-test + import * as expect from "./"; + + const element: HTMLElement = expect.element; + + const weakSet: WeakSet<{}> = expect.weakSet; + ~~~~~~~ +!!! @definitelytyped/expect: TypeScript@tsconfig.dom.json compile error: +!!! : Cannot find name 'WeakSet'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later. diff --git a/packages/eslint-plugin/test/__file_snapshots__/types/expect-tsconfigs/expect-tests.ts.lint b/packages/eslint-plugin/test/__file_snapshots__/types/expect-tsconfigs/expect-tests.ts.lint new file mode 100644 index 0000000000..8a3d7eaf1b --- /dev/null +++ b/packages/eslint-plugin/test/__file_snapshots__/types/expect-tsconfigs/expect-tests.ts.lint @@ -0,0 +1,17 @@ +types/expect-tsconfigs/expect-tests.ts + 4:16 error TypeScript@tsconfig.no-dom.json compile error: +Cannot find name 'HTMLElement' @definitelytyped/expect + +✖ 1 problem (1 error, 0 warnings) + +==== types/expect-tsconfigs/expect-tests.ts ==== + + // eslint-disable-next-line @definitelytyped/no-relative-import-in-test + import * as expect from "./"; + + const element: HTMLElement = expect.element; + ~~~~~~~~~~~ +!!! @definitelytyped/expect: TypeScript@tsconfig.no-dom.json compile error: +!!! : Cannot find name 'HTMLElement'. + + const weakSet: WeakSet<{}> = expect.weakSet; diff --git a/packages/eslint-plugin/test/__file_snapshots__/types/expect-tsconfigs/index.d.ts.lint b/packages/eslint-plugin/test/__file_snapshots__/types/expect-tsconfigs/index.d.ts.lint new file mode 100644 index 0000000000..05dfa5e83a --- /dev/null +++ b/packages/eslint-plugin/test/__file_snapshots__/types/expect-tsconfigs/index.d.ts.lint @@ -0,0 +1,19 @@ +types/expect-tsconfigs/index.d.ts + 1:23 error TypeScript@tsconfig.no-dom.json compile error: +Cannot find name 'HTMLElement' @definitelytyped/expect + 3:23 error TypeScript@tsconfig.dom.json compile error: +Cannot find name 'WeakSet'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later @definitelytyped/expect + +✖ 2 problems (2 errors, 0 warnings) + +==== types/expect-tsconfigs/index.d.ts ==== + + export const element: HTMLElement; + ~~~~~~~~~~~ +!!! @definitelytyped/expect: TypeScript@tsconfig.no-dom.json compile error: +!!! : Cannot find name 'HTMLElement'. + + export const weakSet: WeakSet<{}>; + ~~~~~~~ +!!! @definitelytyped/expect: TypeScript@tsconfig.dom.json compile error: +!!! : Cannot find name 'WeakSet'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later. diff --git a/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/expect-dom-tests.ts b/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/expect-dom-tests.ts new file mode 100644 index 0000000000..c3cd14c3c3 --- /dev/null +++ b/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/expect-dom-tests.ts @@ -0,0 +1,6 @@ +// eslint-disable-next-line @definitelytyped/no-relative-import-in-test +import * as expect from "./"; + +const element: HTMLElement = expect.element; + +const weakSet: WeakSet<{}> = expect.weakSet; diff --git a/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/expect-tests.ts b/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/expect-tests.ts new file mode 100644 index 0000000000..c3cd14c3c3 --- /dev/null +++ b/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/expect-tests.ts @@ -0,0 +1,6 @@ +// eslint-disable-next-line @definitelytyped/no-relative-import-in-test +import * as expect from "./"; + +const element: HTMLElement = expect.element; + +const weakSet: WeakSet<{}> = expect.weakSet; diff --git a/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/index.d.ts b/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/index.d.ts new file mode 100644 index 0000000000..85cea8732f --- /dev/null +++ b/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/index.d.ts @@ -0,0 +1,3 @@ +export const element: HTMLElement; + +export const weakSet: WeakSet<{}>; diff --git a/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/package.json b/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/package.json new file mode 100644 index 0000000000..1981bc269b --- /dev/null +++ b/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/package.json @@ -0,0 +1,6 @@ +{ + "name": "@types/expect", + "version": "2.0.9999", + "owners": [], + "tsconfigs": ["tsconfig.dom.json", "tsconfig.no-dom.json"] +} diff --git a/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/tsconfig.dom.json b/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/tsconfig.dom.json new file mode 100644 index 0000000000..57ffbf7df3 --- /dev/null +++ b/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/tsconfig.dom.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "module": "commonjs", + "lib": [ + "es5", + "dom" + ], + "noImplicitAny": true, + "noImplicitThis": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "types": [], + "noEmit": true, + "forceConsistentCasingInFileNames": true + }, + "files": [ + "index.d.ts", + "expect-dom-tests.ts" + ] +} diff --git a/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/tsconfig.json b/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/tsconfig.json new file mode 100644 index 0000000000..a2f256cde2 --- /dev/null +++ b/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "lib": [ + "es6", + "dom" + ], + "noImplicitAny": true, + "noImplicitThis": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "types": [], + "noEmit": true, + "forceConsistentCasingInFileNames": true + }, + "files": [ + "index.d.ts", + "expect-tests.ts", + "expect-dom-tests.ts" + ] +} diff --git a/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/tsconfig.no-dom.json b/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/tsconfig.no-dom.json new file mode 100644 index 0000000000..e5dfa2b823 --- /dev/null +++ b/packages/eslint-plugin/test/fixtures/types/expect-tsconfigs/tsconfig.no-dom.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "commonjs", + "lib": [ + "es6" + ], + "noImplicitAny": true, + "noImplicitThis": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "types": [], + "noEmit": true, + "forceConsistentCasingInFileNames": true + }, + "files": [ + "index.d.ts", + "expect-tests.ts" + ] +} diff --git a/packages/header-parser/src/index.ts b/packages/header-parser/src/index.ts index ff7403d4e5..0a8e0b8b48 100644 --- a/packages/header-parser/src/index.ts +++ b/packages/header-parser/src/index.ts @@ -14,6 +14,7 @@ export interface Header { readonly minimumTypeScriptVersion: AllTypeScriptVersion; readonly projects: readonly string[]; readonly owners: readonly Owner[]; + readonly tsconfigs: readonly string[]; } // used in definitions-parser /** Standard package.json `contributor` */ @@ -68,6 +69,7 @@ export function validatePackageJson( case "nonNpm": case "nonNpmDescription": case "pnpm": + case "tsconfigs": break; case "typesVersions": case "types": @@ -123,6 +125,7 @@ export function validatePackageJson( let minimumTypeScriptVersion: AllTypeScriptVersion = TypeScriptVersion.lowest; let projects: string[] = []; let owners: Owner[] = []; + let tsconfigs: string[] = []; // let files: string[] = []; const nameResult = validateName(); const versionResult = validateVersion(); @@ -131,6 +134,7 @@ export function validatePackageJson( const projectsResult = validateProjects(); const ownersResult = validateOwners(); const licenseResult = getLicenseFromPackageJson(packageJson.license); + const tsconfigsResult = validateTsconfigs(); if (typeof nameResult === "object") { errors.push(...nameResult.errors); } else { @@ -165,6 +169,11 @@ export function validatePackageJson( if (Array.isArray(licenseResult)) { errors.push(...licenseResult); } + if ("errors" in tsconfigsResult) { + errors.push(...tsconfigsResult.errors); + } else { + tsconfigs = tsconfigsResult; + } if (errors.length) { return errors; } else { @@ -176,6 +185,7 @@ export function validatePackageJson( minimumTypeScriptVersion, projects, owners, + tsconfigs, }; } @@ -285,6 +295,35 @@ export function validatePackageJson( } return { errors }; } + function validateTsconfigs(): string[] | { errors: string[] } { + const errors: string[] = []; + if (packageJson.tsconfigs === undefined) { + return ["tsconfig.json"]; + } + if (!Array.isArray(packageJson.tsconfigs)) { + errors.push( + `${typesDirectoryName}'s package.json has bad "tsconfigs": must be an array of strings that point to the tsconfig file(s).`, + ); + } else { + for (const tsconfig of tsconfigs) { + if (typeof tsconfig !== "string") { + errors.push( + `${typesDirectoryName}'s package.json has bad "tsconfigs": must be an array of strings that point to the tsconfig file(s).`, + ); + continue; + } + if (tsconfig === "tsconfig.json") continue; + + if (!tsconfig.startsWith("tsconfig.") || !tsconfig.endsWith(".json")) { + errors.push( + `${typesDirectoryName}'s package.json has bad "tsconfigs": ${tsconfig} is not a valid tsconfig file name; should match "tsconfig.*.json"`, + ); + } + } + return packageJson.tsconfigs; + } + return { errors }; + } } export function getTypesVersions(dirPath: string): readonly TypeScriptVersion[] { diff --git a/packages/publisher/test/generate-packages.test.ts b/packages/publisher/test/generate-packages.test.ts index 7e37336342..5c63656d8b 100644 --- a/packages/publisher/test/generate-packages.test.ts +++ b/packages/publisher/test/generate-packages.test.ts @@ -27,6 +27,7 @@ function createRawPackage(license: License): TypingsDataRaw { minimumTypeScriptVersion: "3.2", projects: ["jquery.org"], nonNpm: false, + tsconfigs: ["tsconfig.json"], }, typesVersions: [], license,