├── .eslintrc.json ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── LICENSE ├── README.md ├── develop.ts ├── dt.ts ├── index.test.ts ├── index.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── testsource ├── dts-critic.d.ts ├── dts-critic.js ├── missingDefault.d.ts ├── missingDefault.js ├── missingDtsProperty.d.ts ├── missingDtsProperty.js ├── missingDtsSignature.d.ts ├── missingDtsSignature.js ├── missingExportEquals.d.ts ├── missingExportEquals.js ├── missingJsProperty.d.ts ├── missingJsProperty.js ├── missingJsSignatureExportEquals.d.ts ├── missingJsSignatureExportEquals.js ├── missingJsSignatureNoExportEquals.d.ts ├── missingJsSignatureNoExportEquals.js ├── noErrors.d.ts ├── noErrors.js ├── parseltongue.d.ts ├── tslib.d.ts ├── typescript.d.ts ├── webpackPropertyNames.d.ts └── webpackPropertyNames.js └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "warnOnUnsupportedTypeScriptVersion": false, 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "env": { 9 | "browser": false, 10 | "node": true, 11 | "es6": true 12 | }, 13 | "plugins": [ 14 | "@typescript-eslint", "jsdoc", "no-null", "import" 15 | ], 16 | "rules": { 17 | "@typescript-eslint/adjacent-overload-signatures": "error", 18 | "@typescript-eslint/array-type": "error", 19 | 20 | "camelcase": "off", 21 | "@typescript-eslint/camelcase": ["error", { "properties": "never", "allow": ["^[A-Za-z][a-zA-Za-z]+_[A-Za-z]+$"] }], 22 | 23 | "@typescript-eslint/class-name-casing": "error", 24 | "@typescript-eslint/consistent-type-definitions": ["error", "interface"], 25 | "@typescript-eslint/interface-name-prefix": "error", 26 | "@typescript-eslint/no-inferrable-types": "error", 27 | "@typescript-eslint/no-misused-new": "error", 28 | "@typescript-eslint/no-this-alias": "error", 29 | "@typescript-eslint/prefer-for-of": "error", 30 | "@typescript-eslint/prefer-function-type": "error", 31 | "@typescript-eslint/prefer-namespace-keyword": "error", 32 | 33 | "quotes": "off", 34 | "@typescript-eslint/quotes": ["error", "double", { "avoidEscape": true, "allowTemplateLiterals": true }], 35 | 36 | "semi": "off", 37 | "@typescript-eslint/semi": "error", 38 | 39 | "@typescript-eslint/triple-slash-reference": "error", 40 | "@typescript-eslint/type-annotation-spacing": "error", 41 | "@typescript-eslint/unified-signatures": "error", 42 | 43 | // eslint-plugin-import 44 | "import/no-extraneous-dependencies": ["error", { "optionalDependencies": false }], 45 | 46 | // eslint-plugin-no-null 47 | "no-null/no-null": "error", 48 | 49 | // eslint-plugin-jsdoc 50 | "jsdoc/check-alignment": "error", 51 | 52 | // eslint 53 | "brace-style": ["error", "stroustrup", { "allowSingleLine": true }], 54 | "constructor-super": "error", 55 | "curly": ["error", "multi-line"], 56 | "dot-notation": "error", 57 | "eqeqeq": "error", 58 | "linebreak-style": ["error", "windows"], 59 | "new-parens": "error", 60 | "no-caller": "error", 61 | "no-duplicate-case": "error", 62 | "no-duplicate-imports": "error", 63 | "no-empty": "error", 64 | "no-eval": "error", 65 | "no-extra-bind": "error", 66 | "no-fallthrough": "error", 67 | "no-new-func": "error", 68 | "no-new-wrappers": "error", 69 | "no-return-await": "error", 70 | "no-restricted-globals": ["error", 71 | { "name": "setTimeout" }, 72 | { "name": "clearTimeout" }, 73 | { "name": "setInterval" }, 74 | { "name": "clearInterval" }, 75 | { "name": "setImmediate" }, 76 | { "name": "clearImmediate" } 77 | ], 78 | "no-sparse-arrays": "error", 79 | "no-template-curly-in-string": "error", 80 | "no-throw-literal": "error", 81 | "no-trailing-spaces": "error", 82 | "no-undef-init": "error", 83 | "no-unsafe-finally": "error", 84 | "no-unused-expressions": ["error", { "allowTernary": true }], 85 | "no-unused-labels": "error", 86 | "no-var": "error", 87 | "object-shorthand": "error", 88 | "prefer-const": "error", 89 | "prefer-object-spread": "error", 90 | "quote-props": ["error", "consistent-as-needed"], 91 | "space-in-parens": "error", 92 | "unicode-bom": ["error", "never"], 93 | "use-isnan": "error" 94 | } 95 | } -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [10.x, 12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm run build --if-present 22 | - run: npm test 23 | env: 24 | CI: true 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | .vscode 64 | 65 | # TypeScript compiler output 66 | dist/ 67 | 68 | # Files downloaded during development 69 | sources/ 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nathan Shively-Sanders 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repo has moved: dts-critic is now part of DefinitelyTyped-tools 2 | 3 | It is not intended to be used on its own, but as part of the `@definitelytyped` set of packages. 4 | The source code has moved to https://github.com/microsoft/DefinitelyTyped-tools 5 | The new package name is `@definitelytyped/dts-critic`. 6 | If there is future demand for standalone usage, this repo should wrap `@definitelytyped/dts-critic` in a command-line interface. 7 | 8 | Checks a new dts against the Javascript sources and tells you what 9 | problems it has. 10 | 11 | # Usage 12 | 13 | Build the program: 14 | ```sh 15 | $ npm run build 16 | ``` 17 | 18 | Run the program using node: 19 | ```sh 20 | $ node dist/index.js --dts=path-to-d.ts [--js=path-to-source] [--mode=mode] [--debug] 21 | ``` 22 | 23 | If the d.ts path is to a file named `index.d.ts`, the name of the directory 24 | will be used as the package name instead. For example 25 | `~/dt/types/jquery/index.d.ts` will use `jquery` as the name. 26 | 27 | `path-to-source` is optional; if you leave it off, the code will 28 | check npm for a package with the same name as the d.ts. 29 | 30 | ## Mode 31 | 32 | You can run dts-critic in different modes that affect which checks will be performed: 33 | 1. `name-only`: dts-critic will check your package name and [DefinitelyTyped header] 34 | (https://github.com/Microsoft/definitelytyped-header-parser) (if present) against npm packages. 35 | For example, if your declaration is for an npm package called 'cool-js-package', it will check if a 36 | package named 'cool-js-package' actually exists in npm. 37 | 38 | 2. `code`: in addition to the checks performed in `name-only` mode, dts-critic will check if your 39 | declaration exports match the source JavaScript module exports. 40 | For example, if your declaration has a default export, it will check if the JavaScript module also 41 | has a default export. 42 | 43 | # Current checks 44 | 45 | ## Npm declaration 46 | If your declaration is for an npm package: 47 | 48 | 1. An npm package with the same name of your declaration's package must exist. 49 | 2. If your declaration has a [Definitely Typed header](https://github.com/Microsoft/definitelytyped-header-parser) 50 | and the header specifies a target version, the npm package must have 51 | a matching version. 52 | 3. If you are running under `code` mode, your declaration must also match the source JavaScript module. 53 | 54 | ## Non-npm declaration 55 | 57 | If your declaration is for a non-npm package (in other words, if your declaration has a 58 | [Definitely Typed header](https://github.com/Microsoft/definitelytyped-header-parser) *and* 59 | the header specifies that the declaration file is for a non-npm package): 60 | 61 | 1. An npm package with the same name of your declaration's package **cannot** exist. 62 | 3. If you are running under `code` mode *and* a path to the JavaScript source file was provided, your 63 | declaration must also match the source JavaScript module. 64 | 65 | # Planned work 66 | 67 | 1. Make sure your module structure fits the source. 68 | 2. Make sure your exported symbols match the source. 69 | 3. Make sure your types match the source types??? 70 | 6. Download source based on npm homepage (if it is github). 71 | 72 | Note that for real use on Definitely Typed, a lot of these checks need to be pretty loose. 73 | 74 | # Also 75 | 76 | ```sh 77 | $ node dist/dt.js 78 | ``` 79 | 80 | Will run dts-critic on every directory inside `../DefinitelyTyped` and 81 | print errors. 82 | 83 | # Contributing 84 | 85 | ## Testing 86 | 87 | The tests use the [Jest](https://jestjs.io/) framework. To build and execute the tests, run: 88 | 89 | ```sh 90 | $ npm run test 91 | ``` 92 | 93 | This will build the program and run jest. 94 | -------------------------------------------------------------------------------- /develop.ts: -------------------------------------------------------------------------------- 1 | import fs = require("fs"); 2 | import yargs = require("yargs"); 3 | import headerParser = require("@definitelytyped/header-parser"); 4 | import path = require("path"); 5 | import cp = require("child_process"); 6 | import { 7 | dtsCritic, 8 | dtToNpmName, 9 | getNpmInfo, 10 | parseExportErrorKind, 11 | CriticError, 12 | ExportErrorKind, 13 | Mode, 14 | checkSource, 15 | findDtsName, 16 | CheckOptions, 17 | parseMode} from "./index"; 18 | 19 | const sourcesDir = "sources"; 20 | const downloadsPath = path.join(sourcesDir, "dts-critic-internal/downloads.json"); 21 | const isNpmPath = path.join(sourcesDir, "dts-critic-internal/npm.json"); 22 | 23 | function getPackageDownloads(dtName: string): number { 24 | const npmName = dtToNpmName(dtName); 25 | const url = `https://api.npmjs.org/downloads/point/last-month/${npmName}`; 26 | const result = JSON.parse( 27 | cp.execFileSync( 28 | "curl", 29 | ["--silent", "-L", url], 30 | { encoding: "utf8" })) as { downloads?: number }; 31 | return result.downloads || 0; 32 | } 33 | 34 | interface DownloadsJson { [key: string]: number | undefined } 35 | 36 | function getAllPackageDownloads(dtPath: string): DownloadsJson { 37 | if (fs.existsSync(downloadsPath)) { 38 | return JSON.parse(fs.readFileSync(downloadsPath, { encoding: "utf8" })) as DownloadsJson; 39 | } 40 | 41 | initDir(path.dirname(downloadsPath)); 42 | const downloads: DownloadsJson = {}; 43 | const dtTypesPath = getDtTypesPath(dtPath); 44 | for (const item of fs.readdirSync(dtTypesPath)) { 45 | const d = getPackageDownloads(item); 46 | downloads[item] = d; 47 | } 48 | fs.writeFileSync(downloadsPath, JSON.stringify(downloads), { encoding: "utf8" }); 49 | 50 | return downloads; 51 | } 52 | 53 | function initDir(path: string): void { 54 | if (!fs.existsSync(path)) { 55 | fs.mkdirSync(path); 56 | } 57 | } 58 | 59 | function getDtTypesPath(dtBasePath: string): string { 60 | return path.join(dtBasePath, "types"); 61 | } 62 | 63 | function compareDownloads(downloads: DownloadsJson, package1: string, package2: string): number { 64 | const count1 = downloads[package1] || 0; 65 | const count2 = downloads[package2] || 0; 66 | return count1 - count2; 67 | } 68 | 69 | interface IsNpmJson { [key: string]: boolean | undefined } 70 | 71 | function getAllIsNpm(dtPath: string): IsNpmJson { 72 | if (fs.existsSync(isNpmPath)) { 73 | return JSON.parse(fs.readFileSync(isNpmPath, { encoding: "utf8" })) as IsNpmJson; 74 | } 75 | initDir(path.dirname(isNpmPath)); 76 | const isNpm: IsNpmJson = {}; 77 | const dtTypesPath = getDtTypesPath(dtPath); 78 | for (const item of fs.readdirSync(dtTypesPath)) { 79 | isNpm[item] = getNpmInfo(item).isNpm; 80 | } 81 | fs.writeFileSync(isNpmPath, JSON.stringify(isNpm), { encoding: "utf8" }); 82 | return isNpm; 83 | } 84 | 85 | function getPopularNpmPackages(count: number, dtPath: string): string[] { 86 | const dtPackages = getDtNpmPackages(dtPath); 87 | const downloads = getAllPackageDownloads(dtPath); 88 | dtPackages.sort((a, b) => compareDownloads(downloads, a, b)); 89 | return dtPackages.slice(dtPackages.length - count); 90 | } 91 | 92 | function getUnpopularNpmPackages(count: number, dtPath: string): string[] { 93 | const dtPackages = getDtNpmPackages(dtPath); 94 | const downloads = getAllPackageDownloads(dtPath); 95 | dtPackages.sort((a, b) => compareDownloads(downloads, a, b)); 96 | return dtPackages.slice(0, count); 97 | } 98 | 99 | function getDtNpmPackages(dtPath: string): string[] { 100 | const dtPackages = fs.readdirSync(getDtTypesPath(dtPath)); 101 | const isNpmJson = getAllIsNpm(dtPath); 102 | return dtPackages.filter(pkg => isNpmPackage(pkg, /* header */ undefined, isNpmJson)); 103 | } 104 | 105 | function getNonNpm(args: { dtPath: string }): void { 106 | const nonNpm: string[] = []; 107 | const dtTypesPath = getDtTypesPath(args.dtPath); 108 | const isNpmJson = getAllIsNpm(args.dtPath); 109 | for (const item of fs.readdirSync(dtTypesPath)) { 110 | const entry = path.join(dtTypesPath, item); 111 | const dts = fs.readFileSync(entry + "/index.d.ts", "utf8"); 112 | let header; 113 | try { 114 | header = headerParser.parseHeaderOrFail(dts); 115 | } 116 | catch (e) { 117 | header = undefined; 118 | } 119 | if (!isNpmPackage(item, header, isNpmJson)) { 120 | nonNpm.push(item); 121 | } 122 | } 123 | console.log(`List of non-npm packages on DT:\n${nonNpm.map(name => `DT name: ${name}\n`).join("")}`); 124 | } 125 | 126 | interface CommonArgs { 127 | dtPath: string, 128 | mode: string, 129 | enableError: string[] | undefined, 130 | debug: boolean, 131 | json: boolean, 132 | } 133 | 134 | function checkAll(args: CommonArgs): void { 135 | const dtPackages = fs.readdirSync(getDtTypesPath(args.dtPath)); 136 | checkPackages({ packages: dtPackages, ...args }); 137 | } 138 | 139 | function checkPopular(args: { count: number } & CommonArgs): void { 140 | checkPackages({ packages: getPopularNpmPackages(args.count, args.dtPath), ...args }); 141 | } 142 | 143 | function checkUnpopular(args: { count: number } & CommonArgs): void { 144 | checkPackages({ packages: getUnpopularNpmPackages(args.count, args.dtPath), ...args }); 145 | } 146 | 147 | function checkPackages(args: { packages: string[] } & CommonArgs): void { 148 | const results = args.packages.map(pkg => doCheck({ package: pkg, ...args })); 149 | printResults(results, args.json); 150 | } 151 | 152 | function checkPackage(args: { package: string } & CommonArgs): void { 153 | printResults([doCheck(args)], args.json); 154 | } 155 | 156 | function doCheck(args: { package: string, dtPath: string, mode: string, enableError: string[] | undefined, debug: boolean }): Result { 157 | const dtPackage = args.package; 158 | const opts = getOptions(args.mode, args.enableError || []); 159 | try { 160 | const dtsPath = path.join(getDtTypesPath(args.dtPath), dtPackage, "index.d.ts"); 161 | const errors = dtsCritic(dtsPath, /* sourcePath */ undefined, opts, args.debug); 162 | return { package: args.package, output: errors }; 163 | } 164 | catch (e) { 165 | return { package: args.package, output: e.toString() }; 166 | } 167 | } 168 | 169 | function getOptions(modeArg: string, enabledErrors: string[]): CheckOptions { 170 | const mode = parseMode(modeArg); 171 | if (!mode) { 172 | throw new Error(`Could not find mode named '${modeArg}'.`); 173 | } 174 | switch (mode) { 175 | case Mode.NameOnly: 176 | return { mode }; 177 | case Mode.Code: 178 | const errors = getEnabledErrors(enabledErrors); 179 | return { mode, errors }; 180 | } 181 | } 182 | 183 | function getEnabledErrors(errorNames: string[]): Map { 184 | const errors: ExportErrorKind[] = []; 185 | for (const name of errorNames) { 186 | const error = parseExportErrorKind(name); 187 | if (error === undefined) { 188 | throw new Error(`Could not find error named '${name}'.`); 189 | } 190 | errors.push(error); 191 | } 192 | return new Map(errors.map(err => [err, true])); 193 | } 194 | 195 | function checkFile(args: { jsFile: string, dtsFile: string, debug: boolean }): void { 196 | console.log(`\tChecking JS file ${args.jsFile} and declaration file ${args.dtsFile}`); 197 | try { 198 | const errors = checkSource(findDtsName(args.dtsFile), args.dtsFile, args.jsFile, new Map(), args.debug); 199 | console.log(formatErrors(errors)); 200 | } 201 | catch (e) { 202 | console.log(e); 203 | } 204 | } 205 | 206 | interface Result { 207 | package: string, 208 | output: CriticError[] | string, 209 | } 210 | 211 | function printResults(results: Result[], json: boolean): void { 212 | if (json) { 213 | console.log(JSON.stringify(results)); 214 | return; 215 | } 216 | 217 | for (const result of results) { 218 | console.log(`\tChecking package ${result.package} ...`); 219 | if (typeof result.output === "string") { 220 | console.log(`Exception:\n${result.output}`); 221 | } 222 | else { 223 | console.log(formatErrors(result.output)); 224 | } 225 | } 226 | } 227 | 228 | function formatErrors(errors: CriticError[]): string { 229 | const lines: string[] = []; 230 | for (const error of errors) { 231 | lines.push("Error: " + error.message); 232 | } 233 | if (errors.length === 0) { 234 | lines.push("No errors found! :)"); 235 | } 236 | return lines.join("\n"); 237 | } 238 | 239 | function isNpmPackage(name: string, header?: headerParser.Header, isNpmJson: IsNpmJson = {}): boolean { 240 | if (header && header.nonNpm) return false; 241 | const isNpm = isNpmJson[name]; 242 | if (isNpm !== undefined) { 243 | return isNpm; 244 | } 245 | return getNpmInfo(name).isNpm; 246 | } 247 | 248 | function main() { 249 | // eslint-disable-next-line no-unused-expressions 250 | yargs 251 | .usage("$0 ") 252 | .command("check-all", "Check source and declaration of all DT packages that are on NPM.", { 253 | dtPath: { 254 | type: "string", 255 | default: "../DefinitelyTyped", 256 | describe: "Path of DT repository cloned locally.", 257 | }, 258 | mode: { 259 | type: "string", 260 | required: true, 261 | choices: [Mode.NameOnly, Mode.Code], 262 | describe: "Mode that defines which group of checks will be made.", 263 | }, 264 | enableError: { 265 | type: "array", 266 | string: true, 267 | describe: "Enable checking for a specific export error." 268 | }, 269 | debug: { 270 | type: "boolean", 271 | default: false, 272 | describe: "Turn debug logging on.", 273 | }, 274 | json: { 275 | type: "boolean", 276 | default: false, 277 | describe: "Format output result as json." 278 | }, 279 | }, checkAll) 280 | .command("check-popular", "Check source and declaration of most popular DT packages that are on NPM.", { 281 | count: { 282 | alias: "c", 283 | type: "number", 284 | required: true, 285 | describe: "Number of packages to be checked.", 286 | }, 287 | dtPath: { 288 | type: "string", 289 | default: "../DefinitelyTyped", 290 | describe: "Path of DT repository cloned locally.", 291 | }, 292 | mode: { 293 | type: "string", 294 | required: true, 295 | choices: [Mode.NameOnly, Mode.Code], 296 | describe: "Mode that defines which group of checks will be made.", 297 | }, 298 | enableError: { 299 | type: "array", 300 | string: true, 301 | describe: "Enable checking for a specific export error." 302 | }, 303 | debug: { 304 | type: "boolean", 305 | default: false, 306 | describe: "Turn debug logging on.", 307 | }, 308 | json: { 309 | type: "boolean", 310 | default: false, 311 | describe: "Format output result as json." 312 | }, 313 | }, checkPopular) 314 | .command("check-unpopular", "Check source and declaration of least popular DT packages that are on NPM.", { 315 | count: { 316 | alias: "c", 317 | type: "number", 318 | required: true, 319 | describe: "Number of packages to be checked.", 320 | }, 321 | dtPath: { 322 | type: "string", 323 | default: "../DefinitelyTyped", 324 | describe: "Path of DT repository cloned locally.", 325 | }, 326 | mode: { 327 | type: "string", 328 | required: true, 329 | choices: [Mode.NameOnly, Mode.Code], 330 | describe: "Mode that defines which group of checks will be made.", 331 | }, 332 | enableError: { 333 | type: "array", 334 | string: true, 335 | describe: "Enable checking for a specific export error." 336 | }, 337 | debug: { 338 | type: "boolean", 339 | default: false, 340 | describe: "Turn debug logging on.", 341 | }, 342 | json: { 343 | type: "boolean", 344 | default: false, 345 | describe: "Format output result as json." 346 | }, 347 | }, checkUnpopular) 348 | .command("check-package", "Check source and declaration of a DT package.", { 349 | package: { 350 | alias: "p", 351 | type: "string", 352 | required: true, 353 | describe: "DT name of a package." 354 | }, 355 | dtPath: { 356 | type: "string", 357 | default: "../DefinitelyTyped", 358 | describe: "Path of DT repository cloned locally.", 359 | }, 360 | mode: { 361 | type: "string", 362 | required: true, 363 | choices: [Mode.NameOnly, Mode.Code], 364 | describe: "Mode that defines which group of checks will be made.", 365 | }, 366 | enableError: { 367 | type: "array", 368 | string: true, 369 | describe: "Enable checking for a specific export error." 370 | }, 371 | debug: { 372 | type: "boolean", 373 | default: false, 374 | describe: "Turn debug logging on.", 375 | }, 376 | json: { 377 | type: "boolean", 378 | default: false, 379 | describe: "Format output result as json." 380 | }, 381 | }, checkPackage) 382 | .command("check-file", "Check a JavaScript file and its matching declaration file.", { 383 | jsFile: { 384 | alias: "j", 385 | type: "string", 386 | required: true, 387 | describe: "Path of JavaScript file.", 388 | }, 389 | dtsFile: { 390 | alias: "d", 391 | type: "string", 392 | required: true, 393 | describe: "Path of declaration file.", 394 | }, 395 | debug: { 396 | type: "boolean", 397 | default: false, 398 | describe: "Turn debug logging on.", 399 | }, 400 | }, checkFile) 401 | .command("get-non-npm", "Get list of DT packages whose source package is not on NPM", { 402 | dtPath: { 403 | type: "string", 404 | default: "../DefinitelyTyped", 405 | describe: "Path of DT repository cloned locally.", 406 | }, 407 | }, getNonNpm) 408 | .demandCommand(1) 409 | .help() 410 | .argv; 411 | } 412 | main(); 413 | -------------------------------------------------------------------------------- /dt.ts: -------------------------------------------------------------------------------- 1 | import { dtsCritic as critic, ErrorKind } from "./index"; 2 | import fs = require("fs"); 3 | import stripJsonComments = require("strip-json-comments"); 4 | 5 | function hasNpmNamingLintRule(tslintPath: string): boolean { 6 | if (fs.existsSync(tslintPath)) { 7 | const tslint = JSON.parse(stripJsonComments(fs.readFileSync(tslintPath, "utf-8"))); 8 | if(tslint.rules && tslint.rules["npm-naming"] !== undefined) { 9 | return !!tslint.rules["npm-naming"]; 10 | } 11 | return true; 12 | } 13 | return false; 14 | } 15 | 16 | function addNpmNamingLintRule(tslintPath: string): void { 17 | if (fs.existsSync(tslintPath)) { 18 | const tslint = JSON.parse(stripJsonComments(fs.readFileSync(tslintPath, "utf-8"))); 19 | if (tslint.rules) { 20 | tslint.rules["npm-naming"] = false; 21 | } 22 | else { 23 | tslint.rules = { "npm-naming": false }; 24 | } 25 | fs.writeFileSync(tslintPath, JSON.stringify(tslint, undefined, 4), "utf-8"); 26 | } 27 | } 28 | 29 | function main() { 30 | for (const item of fs.readdirSync("../DefinitelyTyped/types")) { 31 | const entry = "../DefinitelyTyped/types/" + item; 32 | try { 33 | if (hasNpmNamingLintRule(entry + "/tslint.json")) { 34 | const errors = critic(entry + "/index.d.ts"); 35 | for (const error of errors) { 36 | switch (error.kind) { 37 | case ErrorKind.NoMatchingNpmPackage: 38 | console.log(`No matching npm package found for ` + item); 39 | // const re = /\/\/ Type definitions for/; 40 | // const s = fs.readFileSync(entry + '/index.d.ts', 'utf-8') 41 | // fs.writeFileSync(entry + '/index.d.ts', s.replace(re, '// Type definitions for non-npm package'), 'utf-8') 42 | break; 43 | case ErrorKind.NoDefaultExport: 44 | console.log("converting", item, "to export = ..."); 45 | const named = /export default function\s+(\w+\s*)\(/; 46 | const anon = /export default function\s*\(/; 47 | const id = /export default(\s+\w+);/; 48 | let s = fs.readFileSync(entry + "/index.d.ts", "utf-8"); 49 | s = s.replace(named, "export = $1;\ndeclare function $1("); 50 | s = s.replace(anon, "export = _default;\ndeclare function _default("); 51 | s = s.replace(id, "export =$1;"); 52 | fs.writeFileSync(entry + "/index.d.ts", s, "utf-8"); 53 | break; 54 | case ErrorKind.NoMatchingNpmVersion: 55 | const m = error.message.match(/in the header, ([0-9.]+),[\s\S]to match one on npm: ([0-9., ]+)\./); 56 | if (m) { 57 | const headerver = parseFloat(m[1]); 58 | const npmvers = m[2].split(",").map((s: string) => parseFloat(s.trim())); 59 | const fixto = npmvers.every((v: number) => headerver > v) ? -1.0 : Math.max(...npmvers); 60 | console.log(`npm-version:${item}:${m[1]}:${m[2]}:${fixto}`); 61 | addNpmNamingLintRule(entry + "/tslint.json"); 62 | } 63 | else { 64 | console.log("could not parse error message: ", error.message); 65 | } 66 | break; 67 | default: 68 | console.log(error.message); 69 | } 70 | } 71 | } 72 | } 73 | catch (e) { 74 | console.log("*** ERROR for " + item + " ***"); 75 | console.log(e); 76 | } 77 | } 78 | } 79 | main(); 80 | -------------------------------------------------------------------------------- /index.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | findDtsName, 3 | getNpmInfo, 4 | dtToNpmName, 5 | parseExportErrorKind, 6 | dtsCritic, 7 | checkSource, 8 | ErrorKind, 9 | ExportErrorKind } from "./index"; 10 | 11 | function suite(description: string, tests: { [s: string]: () => void; }) { 12 | describe(description, () => { 13 | for (const k in tests) { 14 | test(k, tests[k], 10 * 1000); 15 | } 16 | }); 17 | } 18 | 19 | suite("findDtsName", { 20 | absolutePath() { 21 | expect(findDtsName("~/dt/types/jquery/index.d.ts")).toBe("jquery"); 22 | }, 23 | relativePath() { 24 | expect(findDtsName("jquery/index.d.ts")).toBe("jquery"); 25 | }, 26 | currentDirectory() { 27 | expect(findDtsName("index.d.ts")).toBe("dts-critic"); 28 | }, 29 | relativeCurrentDirectory() { 30 | expect(findDtsName("./index.d.ts")).toBe("dts-critic"); 31 | }, 32 | emptyDirectory() { 33 | expect(findDtsName("")).toBe("dts-critic"); 34 | }, 35 | }); 36 | suite("getNpmInfo", { 37 | nonNpm() { 38 | expect(getNpmInfo("parseltongue")).toEqual({ isNpm: false }); 39 | }, 40 | npm() { 41 | expect(getNpmInfo("typescript")).toEqual({ 42 | isNpm: true, 43 | versions: expect.arrayContaining(["3.7.5"]), 44 | tags: expect.objectContaining({ latest: expect.stringContaining("") }), 45 | }); 46 | }, 47 | }); 48 | suite("dtToNpmName", { 49 | nonScoped() { 50 | expect(dtToNpmName("content-type")).toBe("content-type"); 51 | }, 52 | scoped() { 53 | expect(dtToNpmName("babel__core")).toBe("@babel/core"); 54 | }, 55 | }); 56 | suite("parseExportErrorKind", { 57 | existent() { 58 | expect(parseExportErrorKind("NoDefaultExport")).toBe(ErrorKind.NoDefaultExport); 59 | }, 60 | existentDifferentCase() { 61 | expect(parseExportErrorKind("JspropertyNotinDTS")).toBe(ErrorKind.JsPropertyNotInDts); 62 | }, 63 | nonexistent() { 64 | expect(parseExportErrorKind("FakeError")).toBe(undefined); 65 | } 66 | }); 67 | 68 | const allErrors: Map = new Map([ 69 | [ErrorKind.NeedsExportEquals, true], 70 | [ErrorKind.NoDefaultExport, true], 71 | [ErrorKind.JsSignatureNotInDts, true], 72 | [ErrorKind.DtsSignatureNotInJs, true], 73 | [ErrorKind.DtsPropertyNotInJs, true], 74 | [ErrorKind.JsPropertyNotInDts, true], 75 | ]); 76 | 77 | suite("checkSource", { 78 | noErrors() { 79 | expect(checkSource( 80 | "noErrors", 81 | "testsource/noErrors.d.ts", 82 | "testsource/noErrors.js", 83 | allErrors, 84 | false, 85 | )).toEqual([]); 86 | }, 87 | missingJsProperty() { 88 | expect(checkSource( 89 | "missingJsProperty", 90 | "testsource/missingJsProperty.d.ts", 91 | "testsource/missingJsProperty.js", 92 | allErrors, 93 | false, 94 | )).toEqual(expect.arrayContaining([ 95 | { 96 | kind: ErrorKind.JsPropertyNotInDts, 97 | message: `The declaration doesn't match the JavaScript module 'missingJsProperty'. Reason: 98 | The JavaScript module exports a property named 'foo', which is missing from the declaration module.` 99 | } 100 | ])); 101 | }, 102 | noMissingWebpackProperty() { 103 | expect(checkSource( 104 | "missingJsProperty", 105 | "testsource/webpackPropertyNames.d.ts", 106 | "testsource/webpackPropertyNames.js", 107 | allErrors, 108 | false, 109 | )).toHaveLength(0); 110 | }, 111 | missingDtsProperty() { 112 | expect(checkSource( 113 | "missingDtsProperty", 114 | "testsource/missingDtsProperty.d.ts", 115 | "testsource/missingDtsProperty.js", 116 | allErrors, 117 | false, 118 | )).toEqual(expect.arrayContaining([ 119 | { 120 | kind: ErrorKind.DtsPropertyNotInJs, 121 | message: `The declaration doesn't match the JavaScript module 'missingDtsProperty'. Reason: 122 | The declaration module exports a property named 'foo', which is missing from the JavaScript module.`, 123 | position: { 124 | start: 67, 125 | length: 11, 126 | }, 127 | } 128 | ])); 129 | }, 130 | missingDefaultExport() { 131 | expect(checkSource( 132 | "missingDefault", 133 | "testsource/missingDefault.d.ts", 134 | "testsource/missingDefault.js", 135 | allErrors, 136 | false, 137 | )).toEqual(expect.arrayContaining([ 138 | { 139 | kind: ErrorKind.NoDefaultExport, 140 | message: `The declaration doesn't match the JavaScript module 'missingDefault'. Reason: 141 | The declaration specifies 'export default' but the JavaScript source does not mention 'default' anywhere. 142 | 143 | The most common way to resolve this error is to use 'export =' syntax instead of 'export default'. 144 | To learn more about 'export =' syntax, see https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require.`, 145 | position: { 146 | start: 0, 147 | length: 32, 148 | }, 149 | } 150 | ])); 151 | }, 152 | missingJsSignatureExportEquals() { 153 | expect(checkSource( 154 | "missingJsSignatureExportEquals", 155 | "testsource/missingJsSignatureExportEquals.d.ts", 156 | "testsource/missingJsSignatureExportEquals.js", 157 | allErrors, 158 | false, 159 | )).toEqual(expect.arrayContaining([ 160 | { 161 | kind: ErrorKind.JsSignatureNotInDts, 162 | message: `The declaration doesn't match the JavaScript module 'missingJsSignatureExportEquals'. Reason: 163 | The JavaScript module can be called or constructed, but the declaration module cannot.`, 164 | } 165 | ])); 166 | }, 167 | missingJsSignatureNoExportEquals() { 168 | expect(checkSource( 169 | "missingJsSignatureNoExportEquals", 170 | "testsource/missingJsSignatureNoExportEquals.d.ts", 171 | "testsource/missingJsSignatureNoExportEquals.js", 172 | allErrors, 173 | false, 174 | )).toEqual(expect.arrayContaining([ 175 | { 176 | kind: ErrorKind.JsSignatureNotInDts, 177 | message: `The declaration doesn't match the JavaScript module 'missingJsSignatureNoExportEquals'. Reason: 178 | The JavaScript module can be called or constructed, but the declaration module cannot. 179 | 180 | The most common way to resolve this error is to use 'export =' syntax. 181 | To learn more about 'export =' syntax, see https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require.`, 182 | } 183 | ])); 184 | }, 185 | missingDtsSignature() { 186 | expect(checkSource( 187 | "missingDtsSignature", 188 | "testsource/missingDtsSignature.d.ts", 189 | "testsource/missingDtsSignature.js", 190 | allErrors, 191 | false, 192 | )).toEqual(expect.arrayContaining([ 193 | { 194 | kind: ErrorKind.DtsSignatureNotInJs, 195 | message: `The declaration doesn't match the JavaScript module 'missingDtsSignature'. Reason: 196 | The declaration module can be called or constructed, but the JavaScript module cannot.`, 197 | } 198 | ])); 199 | }, 200 | missingExportEquals() { 201 | expect(checkSource( 202 | "missingExportEquals", 203 | "testsource/missingExportEquals.d.ts", 204 | "testsource/missingExportEquals.js", 205 | allErrors, 206 | false, 207 | )).toEqual(expect.arrayContaining([ 208 | { 209 | kind: ErrorKind.NeedsExportEquals, 210 | message: `The declaration doesn't match the JavaScript module 'missingExportEquals'. Reason: 211 | The declaration should use 'export =' syntax because the JavaScript source uses 'module.exports =' syntax and 'module.exports' can be called or constructed. 212 | 213 | To learn more about 'export =' syntax, see https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require.`, 214 | } 215 | ])); 216 | }, 217 | }); 218 | suite("dtsCritic", { 219 | noErrors() { 220 | expect(dtsCritic("testsource/dts-critic.d.ts", "testsource/dts-critic.js")).toEqual([]); 221 | }, 222 | noMatchingNpmPackage() { 223 | expect(dtsCritic("testsource/parseltongue.d.ts")).toEqual([ 224 | { 225 | kind: ErrorKind.NoMatchingNpmPackage, 226 | message: `Declaration file must have a matching npm package. 227 | To resolve this error, either: 228 | 1. Change the name to match an npm package. 229 | 2. Add a Definitely Typed header with the first line 230 | 231 | 232 | // Type definitions for non-npm package parseltongue-browser 233 | 234 | Add -browser to the end of your name to make sure it doesn't conflict with existing npm packages.`, 235 | }, 236 | ]); 237 | }, 238 | noMatchingNpmVersion() { 239 | expect(dtsCritic("testsource/typescript.d.ts")).toEqual([ 240 | { 241 | kind: ErrorKind.NoMatchingNpmVersion, 242 | message: expect.stringContaining(`The types for 'typescript' must match a version that exists on npm. 243 | You should copy the major and minor version from the package on npm.`), 244 | }, 245 | ]); 246 | }, 247 | nonNpmHasMatchingPackage() { 248 | expect(dtsCritic("testsource/tslib.d.ts")).toEqual([ 249 | { 250 | kind: ErrorKind.NonNpmHasMatchingPackage, 251 | message: `The non-npm package 'tslib' conflicts with the existing npm package 'tslib'. 252 | Try adding -browser to the end of the name to get 253 | 254 | tslib-browser 255 | `, 256 | }, 257 | ]); 258 | } 259 | }); 260 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import yargs = require("yargs"); 2 | import headerParser = require("@definitelytyped/header-parser"); 3 | import fs = require("fs"); 4 | import os = require("os") 5 | import cp = require("child_process"); 6 | import path = require("path"); 7 | import semver = require("semver"); 8 | import rimraf = require("rimraf"); 9 | import { sync as commandExistsSync } from "command-exists"; 10 | import ts from "typescript"; 11 | import * as tmp from "tmp"; 12 | 13 | export enum ErrorKind { 14 | /** Declaration is marked as npm in header and has no matching npm package. */ 15 | NoMatchingNpmPackage = "NoMatchingNpmPackage", 16 | /** Declaration has no npm package matching specified version. */ 17 | NoMatchingNpmVersion = "NoMatchingNpmVersion", 18 | /** Declaration is not for an npm package, but has a name that conflicts with an existing npm package. */ 19 | NonNpmHasMatchingPackage = "NonNpmHasMatchingPackage", 20 | /** Declaration needs to use `export =` to match the JavaScript module's behavior. */ 21 | NeedsExportEquals = "NeedsExportEquals", 22 | /** Declaration has a default export, but JavaScript module does not have a default export. */ 23 | NoDefaultExport = "NoDefaultExport", 24 | /** JavaScript exports property not found in declaration exports. */ 25 | JsPropertyNotInDts = "JsPropertyNotInDts", 26 | /** Declaration exports property not found in JavaScript exports. */ 27 | DtsPropertyNotInJs = "DtsPropertyNotInJs", 28 | /** JavaScript module has signatures, but declaration module does not. */ 29 | JsSignatureNotInDts = "JsSignatureNotInDts", 30 | /** Declaration module has signatures, but JavaScript module does not. */ 31 | DtsSignatureNotInJs = "DtsSignatureNotInJs", 32 | } 33 | 34 | export enum Mode { 35 | /** Checks based only on the package name and on the declaration's DefinitelyTyped header. */ 36 | NameOnly = "name-only", 37 | /** Checks based on the source JavaScript code, in addition to the checks performed in name-only mode. */ 38 | Code = "code", 39 | } 40 | 41 | export function parseMode(mode: string): Mode | undefined { 42 | switch (mode) { 43 | case Mode.NameOnly: 44 | return Mode.NameOnly; 45 | case Mode.Code: 46 | return Mode.Code; 47 | } 48 | return undefined; 49 | } 50 | 51 | export type CheckOptions = NameOnlyOptions | CodeOptions; 52 | export interface NameOnlyOptions { 53 | mode: Mode.NameOnly, 54 | } 55 | export interface CodeOptions { 56 | mode: Mode.Code, 57 | errors: Map, 58 | } 59 | 60 | export type ExportErrorKind = ExportError["kind"]; 61 | 62 | const defaultOpts: CheckOptions = { mode: Mode.NameOnly }; 63 | 64 | export function dtsCritic(dtsPath: string, sourcePath?: string, options: CheckOptions = defaultOpts, debug = false): CriticError[] { 65 | if (!commandExistsSync("tar")) { 66 | throw new Error("You need to have tar installed to run dts-critic, you can get it from https://www.gnu.org/software/tar"); 67 | } 68 | if (!commandExistsSync("npm")) { 69 | throw new Error("You need to have npm installed to run dts-critic, you can get it from https://www.npmjs.com/get-npm"); 70 | } 71 | 72 | const dts = fs.readFileSync(dtsPath, "utf-8"); 73 | const header = parseDtHeader(dts); 74 | 75 | const name = findDtsName(dtsPath); 76 | const npmInfo = getNpmInfo(name); 77 | 78 | if (isNonNpm(header)) { 79 | const errors: CriticError[] = []; 80 | const nonNpmError = checkNonNpm(name, npmInfo); 81 | if (nonNpmError) { 82 | errors.push(nonNpmError); 83 | } 84 | 85 | if (sourcePath) { 86 | if (options.mode === Mode.Code) { 87 | errors.push(...checkSource(name, dtsPath, sourcePath, options.errors, debug)); 88 | } 89 | } 90 | else if (!module.parent) { 91 | console.log(`Warning: declaration provided is for a non-npm package. 92 | If you want to check the declaration against the JavaScript source code, you must provide a path to the source file.`); 93 | } 94 | 95 | return errors; 96 | } 97 | else { 98 | const npmVersion = checkNpm(name, npmInfo, header); 99 | if (typeof npmVersion !== "string") { 100 | return [npmVersion]; 101 | } 102 | 103 | if (options.mode === Mode.Code) { 104 | let sourceEntry; 105 | let packagePath; 106 | if (sourcePath) { 107 | sourceEntry = sourcePath; 108 | } 109 | else { 110 | const tempDirName = tmp.dirSync({ unsafeCleanup: true }).name 111 | packagePath = downloadNpmPackage(name, npmVersion, tempDirName) 112 | sourceEntry = require.resolve(path.resolve(packagePath)); 113 | } 114 | const errors = checkSource(name, dtsPath, sourceEntry, options.errors, debug); 115 | if (packagePath) { 116 | // Delete the source afterward to avoid running out of space 117 | rimraf.sync(packagePath) 118 | } 119 | return errors; 120 | } 121 | 122 | return []; 123 | } 124 | } 125 | 126 | function parseDtHeader(dts: string): headerParser.Header | undefined { 127 | try { 128 | return headerParser.parseHeaderOrFail(dts); 129 | } 130 | catch (e) { 131 | return undefined; 132 | } 133 | } 134 | 135 | function isNonNpm(header: headerParser.Header | undefined): boolean { 136 | return !!header && header.nonNpm; 137 | } 138 | 139 | export const defaultErrors: ExportErrorKind[] = [ErrorKind.NeedsExportEquals, ErrorKind.NoDefaultExport]; 140 | 141 | function main() { 142 | const argv = yargs. 143 | usage("$0 --dts path-to-d.ts [--js path-to-source] [--mode mode] [--debug]\n\nIf source-folder is not provided, I will look for a matching package on npm."). 144 | option("dts", { 145 | describe: "Path of declaration file to be critiqued.", 146 | type: "string", 147 | }). 148 | demandOption("dts", "Please provide a path to a d.ts file for me to critique."). 149 | option("js", { 150 | describe: "Path of JavaScript file to be used as source.", 151 | type: "string", 152 | }). 153 | option("mode", { 154 | describe: "Mode defines what checks will be performed.", 155 | type: "string", 156 | default: Mode.NameOnly, 157 | choices: [Mode.NameOnly, Mode.Code], 158 | }). 159 | option("debug", { 160 | describe: "Turn debug logging on.", 161 | type: "boolean", 162 | default: false, 163 | }). 164 | help(). 165 | argv; 166 | 167 | let opts; 168 | switch (argv.mode) { 169 | case Mode.NameOnly: 170 | opts = { mode: argv.mode }; 171 | break; 172 | case Mode.Code: 173 | opts = { mode: argv.mode, errors: new Map() }; 174 | } 175 | const errors = dtsCritic(argv.dts, argv.js, opts, argv.debug); 176 | if (errors.length === 0) { 177 | console.log("No errors!"); 178 | } 179 | else { 180 | for (const error of errors) { 181 | console.log("Error: " + error.message); 182 | } 183 | } 184 | } 185 | 186 | const npmNotFound = "E404"; 187 | 188 | export function getNpmInfo(name: string): NpmInfo { 189 | const npmName = dtToNpmName(name); 190 | const infoResult = cp.spawnSync( 191 | "npm", 192 | ["info", npmName, "--json", "--silent", "versions", "dist-tags"], 193 | { encoding: "utf8" }); 194 | const info = JSON.parse(infoResult.stdout || infoResult.stderr); 195 | if (info.error !== undefined) { 196 | const error = info.error as { code?: string, summary?: string }; 197 | if (error.code === npmNotFound) { 198 | return { isNpm: false }; 199 | } 200 | else { 201 | throw new Error(`Command 'npm info' for package ${npmName} returned an error. Reason: ${error.summary}.`); 202 | } 203 | } 204 | else if (infoResult.status !== 0) { 205 | throw new Error(`Command 'npm info' failed for package ${npmName} with status ${infoResult.status}.`); 206 | } 207 | return { 208 | isNpm: true, 209 | versions: info.versions as string[], 210 | tags: info["dist-tags"] as { [tag: string]: string | undefined } 211 | }; 212 | } 213 | 214 | /** 215 | * Checks DefinitelyTyped non-npm package. 216 | */ 217 | function checkNonNpm(name: string, npmInfo: NpmInfo): NonNpmError | undefined { 218 | if (npmInfo.isNpm && !isExistingSquatter(name)) { 219 | return { 220 | kind: ErrorKind.NonNpmHasMatchingPackage, 221 | message: `The non-npm package '${name}' conflicts with the existing npm package '${dtToNpmName(name)}'. 222 | Try adding -browser to the end of the name to get 223 | 224 | ${name}-browser 225 | ` 226 | }; 227 | } 228 | return undefined; 229 | } 230 | 231 | /** 232 | * Checks DefinitelyTyped npm package. 233 | * If all checks are successful, returns the npm version that matches the header. 234 | */ 235 | function checkNpm(name: string, npmInfo: NpmInfo, header: headerParser.Header | undefined): NpmError | string { 236 | if (!npmInfo.isNpm) { 237 | return { 238 | kind: ErrorKind.NoMatchingNpmPackage, 239 | message: `Declaration file must have a matching npm package. 240 | To resolve this error, either: 241 | 1. Change the name to match an npm package. 242 | 2. Add a Definitely Typed header with the first line 243 | 244 | 245 | // Type definitions for non-npm package ${name}-browser 246 | 247 | Add -browser to the end of your name to make sure it doesn't conflict with existing npm packages.` 248 | }; 249 | } 250 | const target = getHeaderVersion(header); 251 | const npmVersion = getMatchingVersion(target, npmInfo); 252 | if (!npmVersion) { 253 | const versions = npmInfo.versions; 254 | const verstring = versions.join(", "); 255 | const lateststring = versions[versions.length - 1]; 256 | const headerstring = target || "NO HEADER VERSION FOUND"; 257 | return { 258 | kind: ErrorKind.NoMatchingNpmVersion, 259 | message: `The types for '${name}' must match a version that exists on npm. 260 | You should copy the major and minor version from the package on npm. 261 | 262 | To resolve this error, change the version in the header, ${headerstring}, 263 | to match one on npm: ${verstring}. 264 | 265 | For example, if you're trying to match the latest version, use ${lateststring}.`, 266 | }; 267 | 268 | } 269 | return npmVersion; 270 | } 271 | 272 | function getHeaderVersion(header: headerParser.Header | undefined): string | undefined { 273 | if (!header) { 274 | return undefined; 275 | } 276 | if (header.libraryMajorVersion === 0 && header.libraryMinorVersion === 0) { 277 | return undefined; 278 | } 279 | return `${header.libraryMajorVersion}.${header.libraryMinorVersion}`; 280 | } 281 | 282 | /** 283 | * Finds an npm version that matches the target version specified, if it exists. 284 | * If the target version is undefined, returns the latest version. 285 | * The npm version returned might be a prerelease version. 286 | */ 287 | function getMatchingVersion(target: string | undefined, npmInfo: Npm): string | undefined { 288 | const versions = npmInfo.versions; 289 | if (target) { 290 | const matchingVersion = semver.maxSatisfying(versions, target, { includePrerelease: true }); 291 | return matchingVersion || undefined; 292 | } 293 | if (npmInfo.tags.latest) { 294 | return npmInfo.tags.latest; 295 | } 296 | return versions[versions.length - 1]; 297 | } 298 | 299 | /** 300 | * If dtsName is 'index' (as with DT) then look to the parent directory for the name. 301 | */ 302 | export function findDtsName(dtsPath: string) { 303 | const resolved = path.resolve(dtsPath); 304 | const baseName = path.basename(resolved, ".d.ts"); 305 | if (baseName && baseName !== "index") { 306 | return baseName; 307 | } 308 | return path.basename(path.dirname(resolved)); 309 | } 310 | 311 | /** Default path to store packages downloaded from npm. */ 312 | const sourceDir = path.resolve(path.join(__dirname, "..", "sources")); 313 | 314 | /** Returns path of downloaded npm package. */ 315 | function downloadNpmPackage(name: string, version: string, outDir: string): string { 316 | const npmName = dtToNpmName(name); 317 | const fullName = `${npmName}@${version}`; 318 | const cpOpts = { encoding: "utf8", maxBuffer: 100 * 1024 * 1024 } as const; 319 | const npmPack = cp.execFileSync("npm", ["pack", fullName, "--json", "--silent"], cpOpts).trim(); 320 | const tarballName = npmPack.endsWith(".tgz") ? npmPack : JSON.parse(npmPack)[0].filename as string; 321 | const outPath = path.join(outDir, name); 322 | initDir(outPath); 323 | const args = os.platform() === "darwin" 324 | ? ["-xz", "-f", tarballName, "-C", outPath] 325 | : ["-xz", "-f", tarballName, "-C", outPath, "--warning=none"]; 326 | cp.execFileSync("tar", args, cpOpts); 327 | fs.unlinkSync(tarballName); 328 | return path.join(outPath, getPackageDir(outPath)); 329 | } 330 | 331 | function getPackageDir(outPath: string): string { 332 | const dirs = fs.readdirSync(outPath, { encoding: "utf8", withFileTypes: true }); 333 | for (const dirent of dirs) { 334 | if (dirent.isDirectory()) { 335 | return dirent.name; 336 | } 337 | } 338 | return "package"; 339 | } 340 | 341 | function initDir(dirPath: string): void { 342 | if (!fs.existsSync(dirPath)) { 343 | fs.mkdirSync(dirPath, { recursive: true }); 344 | } 345 | } 346 | 347 | export function checkSource( 348 | name: string, 349 | dtsPath: string, 350 | srcPath: string, 351 | enabledErrors: Map, 352 | debug: boolean): ExportError[] { 353 | const diagnostics = checkExports(name, dtsPath, srcPath); 354 | if (debug) { 355 | console.log(formatDebug(name, diagnostics)); 356 | } 357 | 358 | return diagnostics.errors.filter(err => enabledErrors.get(err.kind) ?? defaultErrors.includes(err.kind)); 359 | } 360 | 361 | function formatDebug(name: string, diagnostics: ExportsDiagnostics): string { 362 | const lines: string[] = []; 363 | lines.push(`\tDiagnostics for package ${name}.`); 364 | lines.push("\tInferred source module structure:"); 365 | if (isSuccess(diagnostics.jsExportKind)) { 366 | lines.push(diagnostics.jsExportKind.result); 367 | } 368 | else { 369 | lines.push(`Could not infer type of JavaScript exports. Reason: ${diagnostics.jsExportKind.reason}`); 370 | } 371 | lines.push("\tInferred source export type:"); 372 | if (isSuccess(diagnostics.jsExportType)) { 373 | lines.push(formatType(diagnostics.jsExportType.result)); 374 | } 375 | else { 376 | lines.push(`Could not infer type of JavaScript exports. Reason: ${diagnostics.jsExportType.reason}`); 377 | } 378 | if (diagnostics.dtsExportKind) { 379 | lines.push("\tInferred declaration module structure:"); 380 | if (isSuccess(diagnostics.dtsExportKind)) { 381 | lines.push(diagnostics.dtsExportKind.result); 382 | } 383 | else { 384 | lines.push(`Could not infer type of declaration exports. Reason: ${diagnostics.dtsExportKind.reason}`); 385 | } 386 | } 387 | if (diagnostics.dtsExportType) { 388 | lines.push("\tInferred declaration export type:"); 389 | if (isSuccess(diagnostics.dtsExportType)) { 390 | lines.push(formatType(diagnostics.dtsExportType.result)); 391 | } 392 | else { 393 | lines.push(`Could not infer type of declaration exports. Reason: ${diagnostics.dtsExportType.reason}`); 394 | } 395 | } 396 | return lines.join("\n"); 397 | } 398 | 399 | function formatType(type: ts.Type): string { 400 | const lines: string[] = []; 401 | //@ts-ignore property `checker` of `ts.Type` is marked internal. The alternative is to have a TypeChecker parameter. 402 | const checker: ts.TypeChecker = type.checker; 403 | 404 | const properties = type.getProperties(); 405 | if (properties.length > 0) { 406 | lines.push("Type's properties:"); 407 | lines.push(...properties.map(p => p.getName())); 408 | } 409 | 410 | const signatures = type.getConstructSignatures().concat(type.getCallSignatures()); 411 | if (signatures.length > 0) { 412 | lines.push("Type's signatures:"); 413 | lines.push(...signatures.map(s => checker.signatureToString(s))); 414 | } 415 | lines.push(`Type string: ${checker.typeToString(type)}`); 416 | return lines.join("\n"); 417 | } 418 | 419 | const exportEqualsLink = "https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require"; 420 | 421 | /** 422 | * Checks exports of a declaration file against its JavaScript source. 423 | */ 424 | function checkExports(name: string, dtsPath: string, sourcePath: string): ExportsDiagnostics { 425 | const tscOpts = { 426 | allowJs: true, 427 | }; 428 | 429 | const jsProgram = ts.createProgram([sourcePath], tscOpts); 430 | const jsFileNode = jsProgram.getSourceFile(sourcePath); 431 | if (!jsFileNode) { 432 | throw new Error(`TS compiler could not find source file ${sourcePath}.`); 433 | } 434 | const jsChecker = jsProgram.getTypeChecker(); 435 | 436 | const errors: ExportError[] = []; 437 | const sourceDiagnostics = inspectJs(jsFileNode, jsChecker, name); 438 | 439 | const dtsDiagnostics = inspectDts(dtsPath, name); 440 | 441 | if (isSuccess(sourceDiagnostics.exportEquals) 442 | && sourceDiagnostics.exportEquals.result.judgement === ExportEqualsJudgement.Required 443 | && isSuccess(dtsDiagnostics.exportKind) 444 | && dtsDiagnostics.exportKind.result !== DtsExportKind.ExportEquals) { 445 | const error = { 446 | kind: ErrorKind.NeedsExportEquals, 447 | message: `The declaration doesn't match the JavaScript module '${name}'. Reason: 448 | The declaration should use 'export =' syntax because the JavaScript source uses 'module.exports =' syntax and ${sourceDiagnostics.exportEquals.result.reason}. 449 | 450 | To learn more about 'export =' syntax, see ${exportEqualsLink}.`, 451 | } as const; 452 | errors.push(error); 453 | } 454 | 455 | const compatibility = 456 | exportTypesCompatibility( 457 | name, 458 | sourceDiagnostics.exportType, 459 | dtsDiagnostics.exportType, 460 | dtsDiagnostics.exportKind); 461 | 462 | if (isSuccess(compatibility)) { 463 | errors.push(...compatibility.result); 464 | } 465 | 466 | if (dtsDiagnostics.defaultExport && !sourceDiagnostics.exportsDefault) { 467 | errors.push({ 468 | kind: ErrorKind.NoDefaultExport, 469 | position: dtsDiagnostics.defaultExport, 470 | message: `The declaration doesn't match the JavaScript module '${name}'. Reason: 471 | The declaration specifies 'export default' but the JavaScript source does not mention 'default' anywhere. 472 | 473 | The most common way to resolve this error is to use 'export =' syntax instead of 'export default'. 474 | To learn more about 'export =' syntax, see ${exportEqualsLink}.`, 475 | }); 476 | } 477 | 478 | return { 479 | jsExportKind: sourceDiagnostics.exportKind, 480 | jsExportType: sourceDiagnostics.exportType, 481 | dtsExportKind: dtsDiagnostics.exportKind, 482 | dtsExportType: dtsDiagnostics.exportType, 483 | errors }; 484 | } 485 | 486 | function inspectJs(sourceFile: ts.SourceFile, checker: ts.TypeChecker, packageName: string): JsExportsInfo { 487 | const exportKind = getJsExportKind(sourceFile); 488 | const exportType = getJSExportType(sourceFile, checker, exportKind); 489 | const exportsDefault = sourceExportsDefault(sourceFile, packageName); 490 | 491 | let exportEquals; 492 | if (isSuccess(exportType) && isSuccess(exportKind) && exportKind.result === JsExportKind.CommonJs) { 493 | exportEquals = moduleTypeNeedsExportEquals(exportType.result, checker); 494 | } 495 | else { 496 | exportEquals = mergeErrors(exportType, exportKind); 497 | } 498 | 499 | return { exportKind, exportType, exportEquals, exportsDefault }; 500 | } 501 | 502 | function getJsExportKind(sourceFile: ts.SourceFile): InferenceResult { 503 | // @ts-ignore property `commonJsModuleIndicator` of `ts.SourceFile` is marked internal. 504 | if (sourceFile.commonJsModuleIndicator) { 505 | return inferenceSuccess(JsExportKind.CommonJs); 506 | } 507 | // @ts-ignore property `externalModuleIndicator` of `ts.SourceFile` is marked internal. 508 | if (sourceFile.externalModuleIndicator) { 509 | return inferenceSuccess(JsExportKind.ES6); 510 | } 511 | return inferenceError("Could not infer export kind of source file."); 512 | } 513 | 514 | function getJSExportType( 515 | sourceFile: ts.SourceFile, 516 | checker: ts.TypeChecker, 517 | exportKind: InferenceResult): InferenceResult { 518 | if (isSuccess(exportKind)) { 519 | switch (exportKind.result) { 520 | case JsExportKind.CommonJs: { 521 | checker.getSymbolAtLocation(sourceFile); // TODO: get symbol in a safer way? 522 | //@ts-ignore property `symbol` of `ts.Node` is marked internal. 523 | const fileSymbol: ts.Symbol | undefined = sourceFile.symbol; 524 | if (!fileSymbol) { 525 | return inferenceError(`TS compiler could not find symbol for file node '${sourceFile.fileName}'.`); 526 | } 527 | const exportType = checker.getTypeOfSymbolAtLocation(fileSymbol, sourceFile); 528 | return inferenceSuccess(exportType); 529 | } 530 | case JsExportKind.ES6: { 531 | const fileSymbol = checker.getSymbolAtLocation(sourceFile); 532 | if (!fileSymbol) { 533 | return inferenceError(`TS compiler could not find symbol for file node '${sourceFile.fileName}'.`); 534 | } 535 | const exportType = checker.getTypeOfSymbolAtLocation(fileSymbol, sourceFile); 536 | return inferenceSuccess(exportType); 537 | } 538 | } 539 | } 540 | return inferenceError(`Could not infer type of exports because exports kind is undefined.`); 541 | } 542 | 543 | /** 544 | * Decide if a JavaScript source module could have a default export. 545 | */ 546 | function sourceExportsDefault(sourceFile: ts.SourceFile, name: string): boolean { 547 | const src = sourceFile.getFullText(sourceFile); 548 | return isRealExportDefault(name) 549 | || src.indexOf("default") > -1 550 | || src.indexOf("__esModule") > -1 551 | || src.indexOf("react-side-effect") > -1 552 | || src.indexOf("@flow") > -1 553 | || src.indexOf("module.exports = require") > -1; 554 | } 555 | 556 | function moduleTypeNeedsExportEquals(type: ts.Type, checker: ts.TypeChecker): InferenceResult { 557 | if (isBadType(type)) { 558 | return inferenceError(`Inferred type '${checker.typeToString(type)}' is not good enough to be analyzed.`); 559 | } 560 | 561 | const isObject = type.getFlags() & ts.TypeFlags.Object; 562 | // @ts-ignore property `isArrayLikeType` of `ts.TypeChecker` is marked internal. 563 | if (isObject && !hasSignatures(type) && !checker.isArrayLikeType(type)) { 564 | const judgement = ExportEqualsJudgement.NotRequired; 565 | const reason = "'module.exports' is an object which is neither a function, class, or array"; 566 | return inferenceSuccess({ judgement, reason }); 567 | } 568 | 569 | if (hasSignatures(type)) { 570 | const judgement = ExportEqualsJudgement.Required; 571 | const reason = "'module.exports' can be called or constructed"; 572 | return inferenceSuccess({ judgement, reason }); 573 | } 574 | 575 | const primitive = ts.TypeFlags.Boolean | ts.TypeFlags.String | ts.TypeFlags.Number; 576 | if (type.getFlags() & primitive) { 577 | const judgement = ExportEqualsJudgement.Required; 578 | const reason = `'module.exports' has primitive type ${checker.typeToString(type)}`; 579 | return inferenceSuccess({ judgement, reason }); 580 | } 581 | 582 | // @ts-ignore property `isArrayLikeType` of `ts.TypeChecker` is marked internal. 583 | if (checker.isArrayLikeType(type)) { 584 | const judgement = ExportEqualsJudgement.Required; 585 | const reason = `'module.exports' has array-like type ${checker.typeToString(type)}`; 586 | return inferenceSuccess({ judgement, reason }); 587 | } 588 | 589 | return inferenceError(`Could not analyze type '${checker.typeToString(type)}'.`); 590 | } 591 | 592 | function hasSignatures(type: ts.Type): boolean { 593 | return type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0; 594 | } 595 | 596 | function inspectDts(dtsPath: string, name: string): DtsExportDiagnostics { 597 | dtsPath = path.resolve(dtsPath); 598 | const program = createDtProgram(dtsPath); 599 | const sourceFile = program.getSourceFile(path.resolve(dtsPath)); 600 | if (!sourceFile) { 601 | throw new Error(`TS compiler could not find source file '${dtsPath}'.`); 602 | } 603 | const checker = program.getTypeChecker(); 604 | const symbolResult = getDtsModuleSymbol(sourceFile, checker, name); 605 | const exportKindResult = getDtsExportKind(sourceFile); 606 | const exportType = getDtsExportType(sourceFile, checker, symbolResult, exportKindResult); 607 | const defaultExport = getDtsDefaultExport(sourceFile, exportType); 608 | 609 | return { exportKind: exportKindResult, exportType, defaultExport }; 610 | } 611 | 612 | function createDtProgram(dtsPath: string): ts.Program { 613 | const dtsDir = path.dirname(dtsPath); 614 | const configPath = path.join(dtsDir, "tsconfig.json"); 615 | const { config } = ts.readConfigFile(configPath, p => fs.readFileSync(p, { encoding: "utf8" })); 616 | const parseConfigHost: ts.ParseConfigHost = { 617 | fileExists: fs.existsSync, 618 | readDirectory: ts.sys.readDirectory, 619 | readFile: file => fs.readFileSync(file, { encoding: "utf8" }), 620 | useCaseSensitiveFileNames: true, 621 | }; 622 | const parsed = ts.parseJsonConfigFileContent(config, parseConfigHost, path.resolve(dtsDir)); 623 | const host = ts.createCompilerHost(parsed.options, true); 624 | return ts.createProgram([path.resolve(dtsPath)], parsed.options, host); 625 | } 626 | 627 | function getDtsModuleSymbol(sourceFile: ts.SourceFile, checker: ts.TypeChecker, name: string): InferenceResult { 628 | if (matches(sourceFile, node => ts.isModuleDeclaration(node))) { 629 | const npmName = dtToNpmName(name); 630 | const moduleSymbol = checker.getAmbientModules().find(symbol => symbol.getName() === `"${npmName}"`); 631 | if (moduleSymbol) { 632 | return inferenceSuccess(moduleSymbol); 633 | } 634 | } 635 | 636 | const fileSymbol = checker.getSymbolAtLocation(sourceFile); 637 | if (fileSymbol && (fileSymbol.getFlags() & ts.SymbolFlags.ValueModule)) { 638 | return inferenceSuccess(fileSymbol); 639 | } 640 | 641 | return inferenceError(`Could not find module symbol for source file node.`); 642 | } 643 | 644 | function getDtsExportKind(sourceFile: ts.SourceFile): InferenceResult { 645 | if (matches(sourceFile, isExportEquals)) { 646 | return inferenceSuccess(DtsExportKind.ExportEquals); 647 | } 648 | if (matches(sourceFile, isExportConstruct)) { 649 | return inferenceSuccess(DtsExportKind.ES6Like); 650 | } 651 | return inferenceError("Could not infer export kind of declaration file."); 652 | } 653 | 654 | const exportEqualsSymbolName = "export="; 655 | 656 | function getDtsExportType( 657 | sourceFile: ts.SourceFile, 658 | checker: ts.TypeChecker, 659 | symbolResult: InferenceResult, 660 | exportKindResult: InferenceResult): InferenceResult { 661 | if (isSuccess(symbolResult) && isSuccess(exportKindResult)) { 662 | const symbol = symbolResult.result; 663 | const exportKind = exportKindResult.result; 664 | switch (exportKind) { 665 | case (DtsExportKind.ExportEquals): { 666 | const exportSymbol = symbol.exports!.get(exportEqualsSymbolName as ts.__String); 667 | if (!exportSymbol) { 668 | return inferenceError(`TS compiler could not find \`export=\` symbol.`); 669 | } 670 | const exportType = checker.getTypeOfSymbolAtLocation(exportSymbol, sourceFile); 671 | return inferenceSuccess(exportType); 672 | } 673 | case (DtsExportKind.ES6Like): { 674 | const exportType = checker.getTypeOfSymbolAtLocation(symbol, sourceFile); 675 | return inferenceSuccess(exportType); 676 | } 677 | } 678 | } 679 | 680 | return mergeErrors(symbolResult, exportKindResult); 681 | } 682 | 683 | /** 684 | * Returns the position of the default export, if it exists. 685 | */ 686 | function getDtsDefaultExport(sourceFile: ts.SourceFile, moduleType: InferenceResult): Position | undefined { 687 | if (isError(moduleType)) { 688 | const src = sourceFile.getFullText(sourceFile); 689 | const exportDefault = src.indexOf("export default"); 690 | if (exportDefault > -1 691 | && src.indexOf("export =") === -1 692 | && !/declare module ['"]/.test(src)) { 693 | return { 694 | start: exportDefault, 695 | length: "export default".length, 696 | }; 697 | } 698 | return undefined; 699 | } 700 | 701 | const exportDefault = moduleType.result.getProperty("default"); 702 | if (exportDefault) { 703 | return { 704 | start: exportDefault.declarations[0].getStart(), 705 | length: exportDefault.declarations[0].getWidth(), 706 | }; 707 | } 708 | return undefined; 709 | } 710 | 711 | const ignoredProperties = ["__esModule", "prototype", "default", "F", "G", "S", "P", "B", "W", "U", "R"]; 712 | 713 | function ignoreProperty(property: ts.Symbol): boolean { 714 | const name = property.getName(); 715 | return name.startsWith("_") || ignoredProperties.includes(name); 716 | } 717 | 718 | /* 719 | * Given the inferred type of the exports of both source and declaration, we make the following checks: 720 | * 1. If source type has call or construct signatures, then declaration type should also have call or construct signatures. 721 | * 2. If declaration type has call or construct signatures, then source type should also have call or construct signatures. 722 | * 3. If source type has a property named "foo", then declaration type should also have a property named "foo". 723 | * 4. If declaration type has a property named "foo", then source type should also have a property named "foo". 724 | * Checks (2) and (4) don't work well in practice and should not be used for linting/verification purposes, because 725 | * most of the times the error originates because the inferred type of the JavaScript source has missing information. 726 | * Those checks are useful for finding examples where JavaScript type inference could be improved. 727 | */ 728 | function exportTypesCompatibility( 729 | name: string, 730 | sourceType: InferenceResult, 731 | dtsType: InferenceResult, 732 | dtsExportKind: InferenceResult): InferenceResult { 733 | if (isError(sourceType)) { 734 | return inferenceError("Could not get type of exports of source module."); 735 | } 736 | if (isError(dtsType)) { 737 | return inferenceError("Could not get type of exports of declaration module."); 738 | } 739 | if (isBadType(sourceType.result)) { 740 | return inferenceError("Could not infer meaningful type of exports of source module."); 741 | } 742 | if (isBadType(dtsType.result)) { 743 | return inferenceError("Could not infer meaningful type of exports of declaration module."); 744 | } 745 | 746 | const errors: MissingExport[] = []; 747 | if (hasSignatures(sourceType.result) && !hasSignatures(dtsType.result)) { 748 | if (isSuccess(dtsExportKind) && dtsExportKind.result === DtsExportKind.ExportEquals) { 749 | errors.push({ 750 | kind: ErrorKind.JsSignatureNotInDts, 751 | message: `The declaration doesn't match the JavaScript module '${name}'. Reason: 752 | The JavaScript module can be called or constructed, but the declaration module cannot.`, 753 | }); 754 | } 755 | else { 756 | errors.push({ 757 | kind: ErrorKind.JsSignatureNotInDts, 758 | message: `The declaration doesn't match the JavaScript module '${name}'. Reason: 759 | The JavaScript module can be called or constructed, but the declaration module cannot. 760 | 761 | The most common way to resolve this error is to use 'export =' syntax. 762 | To learn more about 'export =' syntax, see ${exportEqualsLink}.`, 763 | }); 764 | } 765 | } 766 | 767 | if (hasSignatures(dtsType.result) && !hasSignatures(sourceType.result)) { 768 | errors.push({ 769 | kind: ErrorKind.DtsSignatureNotInJs, 770 | message: `The declaration doesn't match the JavaScript module '${name}'. Reason: 771 | The declaration module can be called or constructed, but the JavaScript module cannot.`, 772 | }); 773 | } 774 | 775 | const sourceProperties = sourceType.result.getProperties(); 776 | const dtsProperties = dtsType.result.getProperties(); 777 | for (const sourceProperty of sourceProperties) { 778 | // TODO: check `prototype` properties. 779 | if (ignoreProperty(sourceProperty)) continue; 780 | if (!dtsProperties.find(s => s.getName() === sourceProperty.getName())) { 781 | errors.push({ 782 | kind: ErrorKind.JsPropertyNotInDts, 783 | message: `The declaration doesn't match the JavaScript module '${name}'. Reason: 784 | The JavaScript module exports a property named '${sourceProperty.getName()}', which is missing from the declaration module.` 785 | }); 786 | } 787 | } 788 | 789 | for (const dtsProperty of dtsProperties) { 790 | // TODO: check `prototype` properties. 791 | if (ignoreProperty(dtsProperty)) continue; 792 | if (!sourceProperties.find(s => s.getName() === dtsProperty.getName())) { 793 | const error: MissingExport = { 794 | kind: ErrorKind.DtsPropertyNotInJs, 795 | message: `The declaration doesn't match the JavaScript module '${name}'. Reason: 796 | The declaration module exports a property named '${dtsProperty.getName()}', which is missing from the JavaScript module.` 797 | }; 798 | const declaration = dtsProperty.declarations && dtsProperty.declarations.length > 0 ? 799 | dtsProperty.declarations[0] : undefined; 800 | if (declaration) { 801 | error.position = { 802 | start: declaration.getStart(), 803 | length: declaration.getWidth(), 804 | }; 805 | } 806 | errors.push(error); 807 | } 808 | } 809 | 810 | return inferenceSuccess(errors); 811 | } 812 | 813 | function isBadType(type: ts.Type): boolean { 814 | return !!(type.getFlags() 815 | & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Undefined | ts.TypeFlags.Null)); 816 | } 817 | 818 | function isExportEquals(node: ts.Node): boolean { 819 | return ts.isExportAssignment(node) && !!node.isExportEquals; 820 | } 821 | 822 | function isExportConstruct(node: ts.Node): boolean { 823 | return ts.isExportAssignment(node) 824 | || ts.isExportDeclaration(node) 825 | || hasExportModifier(node); 826 | } 827 | 828 | function hasExportModifier(node: ts.Node): boolean { 829 | if (node.modifiers) { 830 | return node.modifiers.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword); 831 | } 832 | return false; 833 | } 834 | 835 | function matches(srcFile: ts.SourceFile, predicate: (n: ts.Node) => boolean): boolean { 836 | function matchesNode(node: ts.Node): boolean { 837 | if (predicate(node)) return true; 838 | const children = node.getChildren(srcFile); 839 | for (const child of children) { 840 | if (matchesNode(child)) return true; 841 | } 842 | return false; 843 | } 844 | return matchesNode(srcFile); 845 | } 846 | 847 | function isExistingSquatter(name: string) { 848 | return name === "atom" || 849 | name === "ember__string" || 850 | name === "fancybox" || 851 | name === "jsqrcode" || 852 | name === "node" || 853 | name === "geojson" || 854 | name === "titanium"; 855 | } 856 | 857 | function isRealExportDefault(name: string) { 858 | return name.indexOf("react-native") > -1 || 859 | name === "ember-feature-flags" || 860 | name === "material-ui-datatables"; 861 | } 862 | 863 | /** 864 | * Converts a package name from the name used in DT repository to the name used in npm. 865 | * @param baseName DT name of a package 866 | */ 867 | export function dtToNpmName(baseName: string) { 868 | if (/__/.test(baseName)) { 869 | return "@" + baseName.replace("__", "/"); 870 | } 871 | return baseName; 872 | } 873 | 874 | /** 875 | * @param error case-insensitive name of the error 876 | */ 877 | export function parseExportErrorKind(error: string): ExportErrorKind | undefined { 878 | error = error.toLowerCase(); 879 | switch (error) { 880 | case "needsexportequals": 881 | return ErrorKind.NeedsExportEquals; 882 | case "nodefaultexport": 883 | return ErrorKind.NoDefaultExport; 884 | case "jspropertynotindts": 885 | return ErrorKind.JsPropertyNotInDts; 886 | case "dtspropertynotinjs": 887 | return ErrorKind.DtsPropertyNotInJs; 888 | case "jssignaturenotindts": 889 | return ErrorKind.JsSignatureNotInDts; 890 | case "dtssignaturenotinjs": 891 | return ErrorKind.DtsSignatureNotInJs; 892 | } 893 | return undefined; 894 | } 895 | 896 | export interface CriticError { 897 | kind: ErrorKind, 898 | message: string, 899 | position?: Position, 900 | } 901 | 902 | interface NpmError extends CriticError { 903 | kind: ErrorKind.NoMatchingNpmPackage | ErrorKind.NoMatchingNpmVersion, 904 | } 905 | 906 | interface NonNpmError extends CriticError { 907 | kind: ErrorKind.NonNpmHasMatchingPackage, 908 | } 909 | 910 | interface ExportEqualsError extends CriticError { 911 | kind: ErrorKind.NeedsExportEquals, 912 | } 913 | 914 | interface DefaultExportError extends CriticError { 915 | kind: ErrorKind.NoDefaultExport, 916 | position: Position, 917 | } 918 | 919 | interface MissingExport extends CriticError { 920 | kind: ErrorKind.JsPropertyNotInDts| ErrorKind.DtsPropertyNotInJs | ErrorKind.JsSignatureNotInDts | ErrorKind.DtsSignatureNotInJs, 921 | } 922 | 923 | interface Position { 924 | start: number, 925 | length: number, 926 | } 927 | 928 | interface ExportsDiagnostics { 929 | jsExportKind: InferenceResult, 930 | jsExportType: InferenceResult, 931 | dtsExportKind: InferenceResult, 932 | dtsExportType: InferenceResult, 933 | errors: ExportError[], 934 | } 935 | 936 | type ExportError = ExportEqualsError | DefaultExportError | MissingExport; 937 | 938 | interface JsExportsInfo { 939 | exportKind: InferenceResult, 940 | exportType: InferenceResult, 941 | exportEquals: InferenceResult, 942 | exportsDefault: boolean, 943 | } 944 | 945 | enum JsExportKind { 946 | CommonJs = "CommonJs", 947 | ES6 = "ES6", 948 | }; 949 | 950 | interface ExportEqualsDiagnostics { 951 | judgement: ExportEqualsJudgement; 952 | reason: string; 953 | } 954 | 955 | enum ExportEqualsJudgement { 956 | Required = "Required", 957 | NotRequired = "Not required", 958 | } 959 | 960 | enum DtsExportKind { 961 | ExportEquals = "export =", 962 | ES6Like = "ES6-like", 963 | } 964 | 965 | interface DtsExportDiagnostics { 966 | exportKind: InferenceResult, 967 | exportType: InferenceResult, 968 | defaultExport?: Position, 969 | } 970 | 971 | type NpmInfo = NonNpm | Npm; 972 | 973 | interface NonNpm { 974 | isNpm: false 975 | } 976 | 977 | interface Npm { 978 | isNpm: true, 979 | versions: string[], 980 | tags: { [tag: string]: string | undefined }, 981 | } 982 | 983 | type InferenceResult = InferenceError | InferenceSuccess; 984 | 985 | enum InferenceResultKind { 986 | Error, 987 | Success, 988 | } 989 | 990 | interface InferenceError { 991 | kind: InferenceResultKind.Error; 992 | reason?: string, 993 | } 994 | 995 | interface InferenceSuccess { 996 | kind: InferenceResultKind.Success; 997 | result: T; 998 | } 999 | 1000 | function inferenceError(reason?: string): InferenceError { 1001 | return { kind: InferenceResultKind.Error, reason }; 1002 | } 1003 | 1004 | function inferenceSuccess(result: T): InferenceSuccess { 1005 | return { kind: InferenceResultKind.Success, result }; 1006 | } 1007 | 1008 | function isSuccess(inference: InferenceResult): inference is InferenceSuccess { 1009 | return inference.kind === InferenceResultKind.Success; 1010 | } 1011 | 1012 | function isError(inference: InferenceResult): inference is InferenceError { 1013 | return inference.kind === InferenceResultKind.Error; 1014 | } 1015 | 1016 | function mergeErrors(...results: (InferenceResult | string)[]): InferenceError { 1017 | const reasons: string[] = []; 1018 | for (const result of results) { 1019 | if (typeof result === "string") { 1020 | reasons.push(result); 1021 | } 1022 | else if (isError(result) && result.reason) { 1023 | reasons.push(result.reason); 1024 | } 1025 | } 1026 | return inferenceError(reasons.join(" ")); 1027 | } 1028 | 1029 | if (!module.parent) { 1030 | main(); 1031 | } 1032 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rootDir: "dist", 3 | moduleFileExtensions: [ 4 | "js", 5 | "jsx", 6 | "json", 7 | "node" 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dts-critic", 3 | "version": "3.3.11", 4 | "author": "Nathan Shively-Sanders", 5 | "description": "Checks a new .d.ts against the Javascript source and tells you what problems it has", 6 | "dependencies": { 7 | "@definitelytyped/header-parser": "latest", 8 | "command-exists": "^1.2.8", 9 | "rimraf": "^3.0.2", 10 | "semver": "^6.2.0", 11 | "tmp": "^0.2.1", 12 | "yargs": "^15.3.1" 13 | }, 14 | "peerDependencies": { 15 | "typescript": "*" 16 | }, 17 | "devDependencies": { 18 | "@types/command-exists": "^1.2.0", 19 | "@types/jest": "^24.0.0", 20 | "@types/node": "~10.17.0", 21 | "@types/rimraf": "^3.0.0", 22 | "@types/semver": "^6.0.1", 23 | "@types/strip-json-comments": "0.0.30", 24 | "@types/tmp": "^0.2.0", 25 | "@types/yargs": "^12.0.8", 26 | "@typescript-eslint/eslint-plugin": "^2.3.2", 27 | "@typescript-eslint/experimental-utils": "^2.3.2", 28 | "@typescript-eslint/parser": "^2.3.2", 29 | "eslint": "^6.5.1", 30 | "eslint-formatter-autolinkable-stylish": "^1.0.3", 31 | "eslint-plugin-import": "^2.18.2", 32 | "eslint-plugin-jsdoc": "^15.9.9", 33 | "eslint-plugin-no-null": "^1.0.2", 34 | "jest": "^24.7.1", 35 | "strip-json-comments": "^2.0.1", 36 | "typescript": "*" 37 | }, 38 | "main": "dist/index.js", 39 | "types": "dist/index.d.ts", 40 | "scripts": { 41 | "test": "npm run build && jest", 42 | "build": "tsc", 43 | "dt": "node dist/dt.js", 44 | "prepublishOnly": "npm run build && npm run test" 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "url": "git+https://github.com/sandersn/dts-critic.git" 49 | }, 50 | "keywords": [ 51 | "definitely", 52 | "typed", 53 | "refresh", 54 | "npm", 55 | "tag" 56 | ], 57 | "license": "MIT", 58 | "bugs": { 59 | "url": "https://github.com/sandersn/dts-critic/issues" 60 | }, 61 | "homepage": "https://github.com/sandersn/dts-critic#readme", 62 | "engines": { 63 | "node": ">=10.17.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /testsource/dts-critic.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for package dts-critic 2.0 2 | // Project: https://github.com/microsoft/TypeScript 3 | // Definitions by: TypeScript Bot 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 | 6 | declare function _default(): void; 7 | 8 | export = _default; -------------------------------------------------------------------------------- /testsource/dts-critic.js: -------------------------------------------------------------------------------- 1 | module.exports = function() {}; -------------------------------------------------------------------------------- /testsource/missingDefault.d.ts: -------------------------------------------------------------------------------- 1 | export default function(): void; -------------------------------------------------------------------------------- /testsource/missingDefault.js: -------------------------------------------------------------------------------- 1 | module.exports = function() {}; -------------------------------------------------------------------------------- /testsource/missingDtsProperty.d.ts: -------------------------------------------------------------------------------- 1 | export const a: () => void; 2 | export const b: number; 3 | export const foo: string; -------------------------------------------------------------------------------- /testsource/missingDtsProperty.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | a: () => {}, 3 | b: 0, 4 | }; -------------------------------------------------------------------------------- /testsource/missingDtsSignature.d.ts: -------------------------------------------------------------------------------- 1 | interface Exports { 2 | (): void, 3 | foo: () => {}, 4 | } 5 | 6 | declare const exp: Exports; 7 | export = exp; -------------------------------------------------------------------------------- /testsource/missingDtsSignature.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | foo: () => {}, 3 | }; -------------------------------------------------------------------------------- /testsource/missingExportEquals.d.ts: -------------------------------------------------------------------------------- 1 | export function foo(a: number): number; 2 | -------------------------------------------------------------------------------- /testsource/missingExportEquals.js: -------------------------------------------------------------------------------- 1 | function foo(a) { 2 | return a; 3 | } 4 | 5 | module.exports = foo; -------------------------------------------------------------------------------- /testsource/missingJsProperty.d.ts: -------------------------------------------------------------------------------- 1 | export const a: () => void; 2 | export const b: number; 3 | -------------------------------------------------------------------------------- /testsource/missingJsProperty.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | a: () => {}, 3 | b: 0, 4 | foo: "missing", 5 | }; -------------------------------------------------------------------------------- /testsource/missingJsSignatureExportEquals.d.ts: -------------------------------------------------------------------------------- 1 | interface Foo { 2 | bar: () => void, 3 | } 4 | declare const foo: Foo; 5 | 6 | export = foo; -------------------------------------------------------------------------------- /testsource/missingJsSignatureExportEquals.js: -------------------------------------------------------------------------------- 1 | module.exports = class Foo { 2 | bar() {} 3 | }; -------------------------------------------------------------------------------- /testsource/missingJsSignatureNoExportEquals.d.ts: -------------------------------------------------------------------------------- 1 | export default function(): void; -------------------------------------------------------------------------------- /testsource/missingJsSignatureNoExportEquals.js: -------------------------------------------------------------------------------- 1 | module.exports = () => {}; -------------------------------------------------------------------------------- /testsource/noErrors.d.ts: -------------------------------------------------------------------------------- 1 | export const a: number; 2 | export const b: string; -------------------------------------------------------------------------------- /testsource/noErrors.js: -------------------------------------------------------------------------------- 1 | exports.a = 42; 2 | exports.b = "forty-two"; -------------------------------------------------------------------------------- /testsource/parseltongue.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefinitelyTyped/dts-critic/fac2afa0b482ed4dcd4c2a687bedcacd66fb6536/testsource/parseltongue.d.ts -------------------------------------------------------------------------------- /testsource/tslib.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for non-npm package tslib 2 | // Project: https://github.com/microsoft/TypeScript 3 | // Definitions by: TypeScript Bot 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -------------------------------------------------------------------------------- /testsource/typescript.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for typescript 1200000.5 2 | // Project: https://github.com/microsoft/TypeScript 3 | // Definitions by: TypeScript Bot 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -------------------------------------------------------------------------------- /testsource/webpackPropertyNames.d.ts: -------------------------------------------------------------------------------- 1 | export var normal: string; 2 | -------------------------------------------------------------------------------- /testsource/webpackPropertyNames.js: -------------------------------------------------------------------------------- 1 | var $export = {}; 2 | // type bitmap 3 | $export.F = 1; // forced 4 | $export.G = 2; // global 5 | $export.S = 4; // static 6 | $export.P = 8; // proto 7 | $export.B = 16; // bind 8 | $export.W = 32; // wrap 9 | $export.U = 64; // safe 10 | $export.R = 128; // real proto method for `library` 11 | $export.normal = "hi"; 12 | module.exports = $export; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "target": "es2019", 6 | "module": "commonjs", 7 | "resolveJsonModule": true, 8 | "strict": true, 9 | "sourceMap": true, 10 | "outDir": "dist", 11 | "declaration": true, 12 | "esModuleInterop": true, 13 | "noImplicitReturns": true, 14 | }, 15 | "exclude": [ 16 | "dist/*", 17 | "sources/*", 18 | "testsource/*", 19 | ] 20 | } 21 | --------------------------------------------------------------------------------