diff --git a/packages/dtslint/test/checks.test.ts b/packages/dtslint/test/checks.test.ts
new file mode 100644
index 0000000000..a6241336ee
--- /dev/null
+++ b/packages/dtslint/test/checks.test.ts
@@ -0,0 +1,236 @@
+///
+import { CompilerOptionsRaw, checkTsconfig } from "../src/checks";
+
+describe("checkTsconfig additional coverage", () => {
+ const base: CompilerOptionsRaw = {
+ module: "commonjs",
+ lib: ["es6"],
+ noImplicitAny: true,
+ noImplicitThis: true,
+ strictNullChecks: true,
+ strictFunctionTypes: true,
+ types: [],
+ noEmit: true,
+ forceConsistentCasingInFileNames: true,
+ };
+
+ function config(overrides?: Partial, fileOverrides?: object) {
+ return { compilerOptions: { ...base, ...overrides }, files: ["index.d.ts", "test.ts"], ...fileOverrides };
+ }
+
+ describe("mustHave validations", () => {
+ it("errors when noEmit is false", () => {
+ const errors = checkTsconfig(config({ noEmit: false }));
+ expect(errors).toContainEqual(expect.stringContaining("noEmit"));
+ });
+
+ it("errors when noEmit is missing", () => {
+ const opts = { ...base };
+ delete (opts as any).noEmit;
+ expect(checkTsconfig({ compilerOptions: opts, files: ["index.d.ts", "test.ts"] })).toContainEqual(
+ expect.stringContaining("noEmit"),
+ );
+ });
+
+ it("errors when forceConsistentCasingInFileNames is false", () => {
+ const errors = checkTsconfig(config({ forceConsistentCasingInFileNames: false }));
+ expect(errors).toContainEqual(expect.stringContaining("forceConsistentCasingInFileNames"));
+ });
+
+ it("errors when types is not an empty array", () => {
+ const errors = checkTsconfig(config({ types: ["node"] }));
+ expect(errors).toContainEqual(expect.stringContaining("types"));
+ });
+ });
+
+ describe("lib validation", () => {
+ it("errors when lib is missing", () => {
+ const opts = { ...base };
+ delete (opts as any).lib;
+ const errors = checkTsconfig({ compilerOptions: opts, files: ["index.d.ts", "test.ts"] });
+ expect(errors).toContainEqual(expect.stringContaining('Must specify "lib"'));
+ });
+ });
+
+ describe("module validation", () => {
+ it("errors when module is an invalid value", () => {
+ const errors = checkTsconfig(config({ module: "esnext" }));
+ expect(errors).toContainEqual(
+ expect.stringContaining('When "module" is present, it must be set to "commonjs" or "node16"'),
+ );
+ });
+
+ it("allows module: commonjs", () => {
+ const errors = checkTsconfig(config({ module: "commonjs" }));
+ expect(errors).toEqual([]);
+ });
+
+ it("allows module: node16", () => {
+ const errors = checkTsconfig(config({ module: "node16" }));
+ expect(errors).toEqual([]);
+ });
+
+ it("allows module: COMMONJS (case insensitive)", () => {
+ const errors = checkTsconfig(config({ module: "COMMONJS" }));
+ expect(errors).toEqual([]);
+ });
+ });
+
+ describe("strict mode", () => {
+ it("allows strict: true and removes individual strict options", () => {
+ const strictOpts: CompilerOptionsRaw = {
+ module: "commonjs",
+ lib: ["es6"],
+ strict: true,
+ types: [],
+ noEmit: true,
+ forceConsistentCasingInFileNames: true,
+ };
+ const errors = checkTsconfig({ compilerOptions: strictOpts, files: ["index.d.ts", "test.ts"] });
+ expect(errors).toEqual([]);
+ });
+
+ it("errors when strict is false", () => {
+ const strictOpts: CompilerOptionsRaw = {
+ module: "commonjs",
+ lib: ["es6"],
+ strict: false,
+ types: [],
+ noEmit: true,
+ forceConsistentCasingInFileNames: true,
+ };
+ const errors = checkTsconfig({ compilerOptions: strictOpts, files: ["index.d.ts", "test.ts"] });
+ expect(errors).toContainEqual(expect.stringContaining('When "strict" is present, it must be set to `true`'));
+ });
+
+ it("throws when strict is true and noImplicitAny is also set", () => {
+ const strictOpts: CompilerOptionsRaw = {
+ module: "commonjs",
+ lib: ["es6"],
+ strict: true,
+ noImplicitAny: true,
+ types: [],
+ noEmit: true,
+ forceConsistentCasingInFileNames: true,
+ };
+ expect(() => checkTsconfig({ compilerOptions: strictOpts, files: ["index.d.ts", "test.ts"] })).toThrow(
+ TypeError,
+ );
+ });
+
+ it("throws when strict is true and strictNullChecks is also set", () => {
+ const strictOpts: CompilerOptionsRaw = {
+ module: "commonjs",
+ lib: ["es6"],
+ strict: true,
+ strictNullChecks: true,
+ types: [],
+ noEmit: true,
+ forceConsistentCasingInFileNames: true,
+ };
+ expect(() => checkTsconfig({ compilerOptions: strictOpts, files: ["index.d.ts", "test.ts"] })).toThrow(
+ TypeError,
+ );
+ });
+
+ it("errors when not strict and missing noImplicitAny", () => {
+ const opts = { ...base };
+ delete (opts as any).noImplicitAny;
+ const errors = checkTsconfig({ compilerOptions: opts, files: ["index.d.ts", "test.ts"] });
+ expect(errors).toContainEqual(expect.stringContaining("noImplicitAny"));
+ });
+
+ it("errors when not strict and missing noImplicitThis", () => {
+ const opts = { ...base };
+ delete (opts as any).noImplicitThis;
+ const errors = checkTsconfig({ compilerOptions: opts, files: ["index.d.ts", "test.ts"] });
+ expect(errors).toContainEqual(expect.stringContaining("noImplicitThis"));
+ });
+
+ it("errors when not strict and missing strictNullChecks", () => {
+ const opts = { ...base };
+ delete (opts as any).strictNullChecks;
+ const errors = checkTsconfig({ compilerOptions: opts, files: ["index.d.ts", "test.ts"] });
+ expect(errors).toContainEqual(expect.stringContaining("strictNullChecks"));
+ });
+
+ it("errors when not strict and missing strictFunctionTypes", () => {
+ const opts = { ...base };
+ delete (opts as any).strictFunctionTypes;
+ const errors = checkTsconfig({ compilerOptions: opts, files: ["index.d.ts", "test.ts"] });
+ expect(errors).toContainEqual(expect.stringContaining("strictFunctionTypes"));
+ });
+ });
+
+ describe("allowed optional compiler options", () => {
+ it("allows target", () => {
+ expect(checkTsconfig(config({ target: "es2020" }))).toEqual([]);
+ });
+
+ it("allows jsx", () => {
+ expect(checkTsconfig(config({ jsx: "react" }))).toEqual([]);
+ });
+
+ it("allows jsxFactory", () => {
+ expect(checkTsconfig(config({ jsxFactory: "h" }))).toEqual([]);
+ });
+
+ it("allows jsxImportSource", () => {
+ expect(checkTsconfig(config({ jsxImportSource: "preact" }))).toEqual([]);
+ });
+
+ it("allows experimentalDecorators", () => {
+ expect(checkTsconfig(config({ experimentalDecorators: true }))).toEqual([]);
+ });
+
+ it("allows noUnusedLocals", () => {
+ expect(checkTsconfig(config({ noUnusedLocals: true }))).toEqual([]);
+ });
+
+ it("allows noUnusedParameters", () => {
+ expect(checkTsconfig(config({ noUnusedParameters: true }))).toEqual([]);
+ });
+
+ it("allows esModuleInterop", () => {
+ expect(checkTsconfig(config({ esModuleInterop: true }))).toEqual([]);
+ });
+
+ it("allows allowSyntheticDefaultImports", () => {
+ expect(checkTsconfig(config({ allowSyntheticDefaultImports: true }))).toEqual([]);
+ });
+
+ it("allows noUncheckedIndexedAccess", () => {
+ expect(checkTsconfig(config({ noUncheckedIndexedAccess: true }))).toEqual([]);
+ });
+ });
+
+ describe("unknown compiler options", () => {
+ it("reports multiple unknown options", () => {
+ const errors = checkTsconfig(config({ someOption: true, anotherOption: false } as any));
+ expect(errors).toContainEqual(expect.stringContaining("someOption"));
+ expect(errors).toContainEqual(expect.stringContaining("anotherOption"));
+ });
+ });
+
+ describe("files validation edge cases", () => {
+ it("allows index.d.ts with ./index.d.ts prefix", () => {
+ expect(checkTsconfig({ compilerOptions: base, files: ["./index.d.ts", "./test.ts"] })).toEqual([]);
+ });
+
+ it("errors when files has only test files but no index.d.ts", () => {
+ const errors = checkTsconfig({ compilerOptions: base, files: ["test.ts"] });
+ expect(errors).toContainEqual(expect.stringContaining('"files" list must include "index.d.ts"'));
+ });
+
+ it("returns error for include even if files is present", () => {
+ const errors = checkTsconfig({ compilerOptions: base, include: ["**/*.ts"], files: ["index.d.ts"] });
+ expect(errors).toContainEqual(expect.stringContaining('Use "files" instead of "include"'));
+ });
+ });
+
+ describe("valid complete config produces no errors", () => {
+ it("passes with a well-formed config", () => {
+ expect(checkTsconfig(config())).toEqual([]);
+ });
+ });
+});
diff --git a/packages/dtslint/test/index.extra.test.ts b/packages/dtslint/test/index.extra.test.ts
new file mode 100644
index 0000000000..97407574da
--- /dev/null
+++ b/packages/dtslint/test/index.extra.test.ts
@@ -0,0 +1,30 @@
+///
+import { assertPackageIsNotDeprecated } from "../src/index";
+
+describe("assertPackageIsNotDeprecated extended", () => {
+ it("throws with informative message including package name", () => {
+ expect(() => assertPackageIsNotDeprecated("moment", '{ "packages": { "moment": {} } }')).toThrow(
+ "notNeededPackages.json has an entry for moment",
+ );
+ });
+
+ it("does not throw when notNeededPackages has other packages", () => {
+ expect(assertPackageIsNotDeprecated("lodash", '{ "packages": { "moment": {} } }')).toBeUndefined();
+ });
+
+ it("does not throw when packages object is empty", () => {
+ expect(assertPackageIsNotDeprecated("anything", '{ "packages": {} }')).toBeUndefined();
+ });
+
+ it("handles multiple packages in the list", () => {
+ const json = '{ "packages": { "foo": {}, "bar": {}, "baz": {} } }';
+ expect(() => assertPackageIsNotDeprecated("bar", json)).toThrow("notNeededPackages.json has an entry for bar");
+ expect(assertPackageIsNotDeprecated("qux", json)).toBeUndefined();
+ });
+
+ it("throw message mentions removing entry from notNeededPackages.json", () => {
+ expect(() => assertPackageIsNotDeprecated("test-pkg", '{ "packages": { "test-pkg": {} } }')).toThrow(
+ /remove its entry from notNeededPackages\.json/,
+ );
+ });
+});
diff --git a/packages/dtslint/test/util.test.ts b/packages/dtslint/test/util.test.ts
new file mode 100644
index 0000000000..965cf411d1
--- /dev/null
+++ b/packages/dtslint/test/util.test.ts
@@ -0,0 +1,68 @@
+///
+import { packageNameFromPath, packageDirectoryNameWithVersionFromPath } from "../src/util";
+
+describe("util", () => {
+ describe("packageNameFromPath", () => {
+ it("returns base name for a simple package path", () => {
+ expect(packageNameFromPath("/types/jquery")).toBe("jquery");
+ });
+
+ it("returns parent name when path ends with a version directory (v14)", () => {
+ expect(packageNameFromPath("/types/node/v14")).toBe("node");
+ });
+
+ it("returns parent name when path ends with a minor version directory (v0.69)", () => {
+ expect(packageNameFromPath("/types/react-native/v0.69")).toBe("react-native");
+ });
+
+ it("returns parent name when path ends with ts version directory (ts5.0)", () => {
+ expect(packageNameFromPath("/types/some-pkg/ts5.0")).toBe("some-pkg");
+ });
+
+ it("returns base name when path does not end with version pattern", () => {
+ expect(packageNameFromPath("/types/some-pkg/subdir")).toBe("subdir");
+ });
+
+ it("handles Windows-style paths", () => {
+ expect(packageNameFromPath("C:\\types\\jquery")).toBe("jquery");
+ });
+
+ it("handles Windows-style paths with version directory", () => {
+ expect(packageNameFromPath("C:\\types\\node\\v14")).toBe("node");
+ });
+
+ it("returns parent for version pattern v1", () => {
+ expect(packageNameFromPath("/types/lodash/v1")).toBe("lodash");
+ });
+
+ it("handles scoped-like package names", () => {
+ expect(packageNameFromPath("/types/bla__foo")).toBe("bla__foo");
+ });
+ });
+
+ describe("packageDirectoryNameWithVersionFromPath", () => {
+ it("returns just the package name when no version directory", () => {
+ expect(packageDirectoryNameWithVersionFromPath("/types/jquery")).toBe("jquery");
+ });
+
+ it("returns package/version when path ends with version directory", () => {
+ expect(packageDirectoryNameWithVersionFromPath("/types/node/v14")).toBe("node/v14");
+ });
+
+ it("returns package/version for minor version directories", () => {
+ expect(packageDirectoryNameWithVersionFromPath("/types/react-native/v0.69")).toBe("react-native/v0.69");
+ });
+
+ it("returns just the subdir name when last segment is not a version", () => {
+ expect(packageDirectoryNameWithVersionFromPath("/types/pkg/subdir")).toBe("subdir");
+ });
+
+ it("handles Windows-style paths without version", () => {
+ expect(packageDirectoryNameWithVersionFromPath("C:\\types\\jquery")).toBe("jquery");
+ });
+
+ it("handles Windows-style paths with version", () => {
+ expect(packageDirectoryNameWithVersionFromPath("C:\\types\\node\\v14")).toBe("node/v14");
+ });
+ });
+});