├── .eslintrc.json ├── .github └── workflows │ ├── CI.yml │ └── Deploy.yml ├── .gitignore ├── CODEOWNERS ├── LICENSE ├── README.md ├── docs ├── dt-header.md ├── export-just-namespace.md ├── no-any-union.md ├── no-bad-reference.md ├── no-const-enum.md ├── no-dead-reference.md ├── no-declare-current-package.md ├── no-import-default-of-export-equals.md ├── no-outside-dependencies.md ├── no-padding.md ├── no-relative-import-in-test.md ├── no-self-import.md ├── no-single-declare-module.md ├── no-single-element-tuple-type.md ├── no-unnecessary-generics.md ├── no-useless-files.md ├── npm-naming.md ├── prefer-declare-function.md ├── redundant-undefined.md ├── strict-export-declare-modifiers.md ├── trim-file.md └── void-return.md ├── dt.json ├── dtslint-expect-only.json ├── dtslint.json ├── package-lock.json ├── package.json ├── src ├── .vscode │ └── launch.json ├── checks.ts ├── index.ts ├── lint.ts ├── rules │ ├── dtHeaderRule.ts │ ├── expectRule.ts │ ├── exportJustNamespaceRule.ts │ ├── noAnyUnionRule.ts │ ├── noBadReferenceRule.ts │ ├── noConstEnumRule.ts │ ├── noDeadReferenceRule.ts │ ├── noDeclareCurrentPackageRule.ts │ ├── noImportDefaultOfExportEqualsRule.ts │ ├── noOutsideDependenciesRule.ts │ ├── noPaddingRule.ts │ ├── noRedundantJsdoc2Rule.ts │ ├── noRelativeImportInTestRule.ts │ ├── noSelfImportRule.ts │ ├── noSingleDeclareModuleRule.ts │ ├── noSingleElementTupleTypeRule.ts │ ├── noUnnecessaryGenericsRule.ts │ ├── noUselessFilesRule.ts │ ├── npmNamingRule.ts │ ├── preferDeclareFunctionRule.ts │ ├── redundantUndefinedRule.ts │ ├── strictExportDeclareModifiersRule.ts │ ├── trimFileRule.ts │ └── voidReturnRule.ts ├── suggestions.ts ├── updateConfig.ts └── util.ts ├── test ├── dt-header │ ├── correct │ │ ├── tslint.json │ │ └── types │ │ │ └── foo │ │ │ ├── bar │ │ │ └── index.d.ts.lint │ │ │ ├── index.d.ts.lint │ │ │ ├── notIndex.d.ts.lint │ │ │ ├── v0.75 │ │ │ └── index.d.ts.lint │ │ │ └── v1 │ │ │ └── index.d.ts.lint │ └── wrong │ │ ├── tslint.json │ │ └── types │ │ ├── bad-url-username │ │ └── index.d.ts.lint │ │ ├── bad-url │ │ └── index.d.ts.lint │ │ ├── default-author-name │ │ └── index.d.ts.lint │ │ └── foo │ │ ├── index.d.ts.lint │ │ └── notIndex.d.ts.lint ├── expect │ ├── expectError.ts.lint │ ├── expectType.ts.lint │ ├── tsconfig.json │ └── tslint.json ├── export-just-namespace │ ├── ok.d.ts.lint │ ├── test.d.ts.lint │ └── tslint.json ├── no-any-union │ ├── test.d.ts.lint │ └── tslint.json ├── no-bad-reference │ ├── decl.d.ts.lint │ ├── test.ts.lint │ └── tslint.json ├── no-const-enum │ ├── test.ts.lint │ └── tslint.json ├── no-dead-reference │ ├── test.ts.lint │ └── tslint.json ├── no-import-default-of-export-equals │ ├── bad-ambient-modules │ │ ├── test.d.ts.lint │ │ ├── tsconfig.json │ │ └── tslint.json │ ├── bad-external-modules │ │ ├── a.d.ts │ │ ├── b.d.ts.lint │ │ ├── tsconfig.json │ │ └── tslint.json │ └── good-ambient-modules │ │ ├── test.d.ts.lint │ │ ├── tsconfig.json │ │ └── tslint.json ├── no-padding │ ├── test.ts.lint │ └── tslint.json ├── no-redundant-jsdoc2 │ ├── test.ts.lint │ └── tslint.json ├── no-relative-import-in-test │ ├── decl.d.ts.lint │ ├── declarationFile.d.ts │ ├── test.ts.lint │ ├── testFile.ts │ ├── tsconfig.json │ └── tslint.json ├── no-single-declare-module │ ├── augmentation.d.ts.lint │ ├── multiple.d.ts.lint │ ├── tslint.json │ ├── wildcard.d.ts.lint │ └── wrong.d.ts.lint ├── no-single-element-tuple-type │ ├── test.ts.lint │ └── tslint.json ├── no-unnecessary-generics │ ├── test.ts.lint │ ├── tsconfig.json │ └── tslint.json ├── no-useless-files │ ├── ok.ts.tlint │ ├── test.ts.lint │ └── tslint.json ├── npm-naming │ ├── code │ │ ├── tslint.json │ │ └── types │ │ │ └── dts-critic │ │ │ ├── index.d.ts │ │ │ └── index.d.ts.lint │ └── name │ │ ├── tslint.json │ │ └── types │ │ └── parseltongue │ │ ├── index.d.ts │ │ └── index.d.ts.lint ├── prefer-declare-function │ ├── test.d.ts.lint │ └── tslint.json ├── redundant-undefined │ ├── test.ts.lint │ └── tslint.json ├── strict-export-declare-modifiers │ ├── testAmbient.d.ts.lint │ ├── testModule.d.ts.lint │ ├── testModuleAutoExport.d.ts.lint │ ├── testModuleTs.ts.lint │ ├── testValues.d.ts.lint │ └── tslint.json ├── test.js ├── trim-file │ ├── test.ts.lint │ └── tslint.json └── void-return │ ├── test.ts.lint │ └── tslint.json ├── tsconfig.json └── tslint.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/eslint-recommended", 5 | "plugin:@typescript-eslint/recommended" 6 | ], 7 | "parser": "@typescript-eslint/parser", 8 | "plugins": ["@typescript-eslint"], 9 | "rules": { 10 | "@typescript-eslint/no-non-null-assertion": "off", 11 | "@typescript-eslint/explicit-module-boundary-types": "off", 12 | "@typescript-eslint/no-unused-vars": "off", 13 | "@typescript-eslint/no-var-requires": "off", 14 | "@typescript-eslint/no-explicit-any": "off" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request 5 | 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v1 13 | with: 14 | registry-url: "https://registry.npmjs.org" 15 | 16 | - run: "npm install" 17 | - run: "npm test" 18 | - run: "npm run lint" 19 | -------------------------------------------------------------------------------- /.github/workflows/Deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to npm 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | registry-url: "https://registry.npmjs.org" 18 | 19 | # Ensure everything is set up right 20 | - run: "npm install" 21 | - run: "npm test" 22 | 23 | - uses: orta/npm-should-deploy-action@main 24 | id: check 25 | 26 | - run: "npm publish" 27 | if: ${{ steps.check.outputs.deploy == 'true' }} 28 | env: 29 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | node_modules 3 | typescript-installs 4 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @sandersn -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 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: dtslint 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/dtslint`. 6 | 7 | `dtslint` tests a TypeScript declaration file for style and correctness. 8 | It will install `typescript` and `tslint` for you, so this is the only tool you need to test a type definition. 9 | 10 | Lint rules new to dtslint are documented in the [docs](docs) directory. 11 | 12 | # Just looking for ExpectType and ExpectError? 13 | 14 | [Use tsd instead](https://github.com/SamVerschueren/tsd). 15 | 16 | # Setup 17 | 18 | If you are working on DefinitelyTyped, read the [DefinitelyTyped README](https://github.com/DefinitelyTyped/DefinitelyTyped#readme). 19 | 20 | If you are writing the library in TypeScript, don't use `dtslint`. 21 | Use [`--declaration`](http://www.typescriptlang.org/docs/handbook/compiler-options.html) to have type definitions generated for you. 22 | 23 | If you are a library author, read below. 24 | 25 | 26 | ## Add types for a library (not on DefinitelyTyped) 27 | 28 | [`dts-gen`](https://github.com/Microsoft/dts-gen#readme) may help, but is not required. 29 | 30 | Create a `types` directory. (Name is arbitrary.) 31 | Add `"types": "types"` to your `package.json`. 32 | Read more on bundling types [here](http://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html). 33 | 34 | 35 | #### `types/index.d.ts` 36 | 37 | Only `index.d.ts` needs to be published to NPM. Other files are just for testing. 38 | Write your type definitions here. 39 | Refer to the [handbook](http://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html) or `dts-gen`'s templates for how to do this. 40 | 41 | 42 | #### `types/tsconfig.json` 43 | 44 | ```json5 45 | { 46 | "compilerOptions": { 47 | "module": "commonjs", 48 | "lib": ["es6"], 49 | "noImplicitAny": true, 50 | "noImplicitThis": true, 51 | "strictFunctionTypes": true, 52 | "strictNullChecks": true, 53 | "types": [], 54 | "noEmit": true, 55 | "forceConsistentCasingInFileNames": true, 56 | 57 | // If the library is an external module (uses `export`), this allows your test file to import "mylib" instead of "./index". 58 | // If the library is global (cannot be imported via `import` or `require`), leave this out. 59 | "baseUrl": ".", 60 | "paths": { "mylib": ["."] } 61 | } 62 | } 63 | ``` 64 | 65 | You may extend `"lib"` to, for example, `["es6", "dom"]` if you need those typings. 66 | You may also have to add `"target": "es6"` if using certain language features. 67 | 68 | 69 | #### `types/tslint.json` 70 | 71 | If you are using the default rules, this is optional. 72 | 73 | If present, this will override `dtslint`'s [default](https://github.com/Microsoft/dtslint/blob/master/dtslint.json) settings. 74 | You can specify new lint [rules](https://palantir.github.io/tslint/rules/), or disable some. An example: 75 | 76 | ```json5 77 | { 78 | "extends": "dtslint/dtslint.json", // Or "dtslint/dt.json" if on DefinitelyTyped 79 | "rules": { 80 | "semicolon": false, 81 | "indent": [true, "tabs"] 82 | } 83 | } 84 | ``` 85 | 86 | 87 | #### `types/test.ts` 88 | 89 | You can have any number of test files you want, with any names. See below on what to put in them. 90 | 91 | 92 | 93 | ## Write tests 94 | 95 | A test file should be a piece of sample code that tests using the library. Tests are type-checked, but not run. 96 | To assert that an expression is of a given type, use `$ExpectType`. 97 | To assert that an expression causes a compile error, use `$ExpectError`. 98 | (Assertions will be checked by the `expect` lint rule.) 99 | 100 | ```ts 101 | import { f } from "my-lib"; // f is(n: number) => void 102 | 103 | // $ExpectType void 104 | f(1); 105 | 106 | // Can also write the assertion on the same line. 107 | f(2); // $ExpectType void 108 | 109 | // $ExpectError 110 | f("one"); 111 | ``` 112 | 113 | 114 | ## Specify a TypeScript version 115 | 116 | Normally packages will be tested using TypeScript 2.0. 117 | To use a newer version, specify it by including a comment like so: 118 | 119 | ```ts 120 | // Minimum TypeScript Version: 2.1 121 | ``` 122 | 123 | For DefinitelyTyped packages, this should go just under the header (on line 5). 124 | For bundled typings, this can go on any line (but should be near the top). 125 | 126 | 127 | ## Run tests 128 | 129 | - `npm install --save-dev dtslint` 130 | - Add to your `package.json` `scripts`: `"dtslint": "dtslint types"` 131 | - `npm run dtslint` 132 | 133 | ### Options 134 | 135 | - `--localTs` 136 | 137 | Use your locally installed version of TypeScript. 138 | 139 | ```sh 140 | dtslint --localTs node_modules/typescript/lib types 141 | ``` 142 | - `--expectOnly` 143 | 144 | Disable all the lint rules except the one that checks for type correctness. 145 | 146 | ```sh 147 | dtslint --expectOnly types 148 | ``` 149 | 150 | 151 | # Contributing 152 | 153 | ## Build 154 | 155 | ```sh 156 | npm link . # Global 'dts-lint' should now refer to this. 157 | npm run watch 158 | ``` 159 | 160 | ## Test 161 | 162 | Use `npm run test` to run all tests. 163 | To run a single test: `node node_modules/tslint/bin/tslint --rules-dir bin/rules --test test/expect`. 164 | 165 | ## Publish 166 | 167 | 1. Change the version in the `package.json` 168 | 2. Push to master 169 | 170 | ## Code of Conduct 171 | 172 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 173 | 174 | ## FAQ 175 | I'm getting an error about a missing typescript install. 176 | ``` 177 | Error: Cannot find module '/node_modules/dtslint/typescript-installs/3.1/node_modules/typescript` 178 | ``` 179 | Your dependencies may be out of date. 180 | [@definitelytyped/typescript-versions](https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/typescript-versions) is the package that contains the list of TypeScript versions to install. 181 | 182 | Alternatively this error can be caused by concurrent dtslint invocations trampling each other's TypeScript installations, especially in the context of continuous integration, if dtslint is installed from scratch in each run. 183 | If for example you use [Lerna](https://github.com/lerna/lerna/tree/main/commands/run#readme), try running dtslint with [`lerna --concurrency 1 run ...`](https://github.com/lerna/lerna/tree/main/core/global-options#--concurrency). 184 | -------------------------------------------------------------------------------- /docs/dt-header.md: -------------------------------------------------------------------------------- 1 | # dt-header 2 | 3 | (This rule is specific to DefinitelyTyped.) 4 | 5 | Checks the format of DefinitelyTyped header comments. 6 | 7 | --- 8 | 9 | **Bad**: 10 | 11 | ```ts 12 | // Type definitions for foo v1.2.3 13 | ``` 14 | 15 | * Don't include `v` 16 | * Don't include a patch version 17 | 18 | **Good**: 19 | 20 | ```ts 21 | // Type definitions for foo 1.2 22 | ``` 23 | 24 | --- 25 | 26 | **Bad**: 27 | 28 | ```ts 29 | // Definitions by: My Name 30 | ``` 31 | 32 | **Good**: 33 | 34 | ```ts 35 | // Definitions by: My Name 36 | ``` 37 | 38 | * Prefer a GitHub username, not a personal web site. 39 | 40 | --- 41 | 42 | **Bad**: 43 | 44 | `foo/index.d.ts`: 45 | 46 | ```ts 47 | // Type definitions for abs 1.2 48 | // Project: https://github.com/foo/foo 49 | // Definitions by: My Name 50 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 51 | export { f } from "./subModule"; 52 | ``` 53 | 54 | `foo/subModule.d.ts`: 55 | 56 | ```ts 57 | // Type definitions for abs 1.2 58 | // Project: https://github.com/foo/foo 59 | // Definitions by: My Name 60 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 61 | export function f(): number; 62 | ``` 63 | 64 | `foo/ts3.1/index.d.ts`: 65 | ```ts 66 | // Type definitions for abs 1.2 67 | // Project: https://github.com/foo/foo 68 | // Definitions by: My Name 69 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 70 | export function f(): number; 71 | ``` 72 | 73 | 74 | **Good**: 75 | 76 | `foo/index.d.ts`: Same 77 | 78 | `foo/subModule.d.ts`: 79 | ```ts 80 | export function f(): number; 81 | ``` 82 | 83 | `foo/ts3.1/index.d.ts`: 84 | ```ts 85 | export function f(): number; 86 | ``` 87 | 88 | Don't repeat the header -- only do it in the index of the root. 89 | -------------------------------------------------------------------------------- /docs/export-just-namespace.md: -------------------------------------------------------------------------------- 1 | # export-just-namespace 2 | 3 | Declaring a namespace is unnecessary if that is the module's only content; just use ES6 export syntax instead. 4 | 5 | **Bad**: 6 | 7 | ```ts 8 | namespace MyLib { 9 | export function f(): number; 10 | } 11 | export = MyLib; 12 | ``` 13 | 14 | **Good**: 15 | 16 | ```ts 17 | export function f(): number; 18 | ``` 19 | 20 | **Also good**: 21 | 22 | ```ts 23 | namespace MyLib { 24 | export function f(): number; 25 | } 26 | function MyLib(): number; 27 | export = MyLib; 28 | ``` 29 | 30 | -------------------------------------------------------------------------------- /docs/no-any-union.md: -------------------------------------------------------------------------------- 1 | # no-any-union 2 | 3 | Forbids to include `any` in a union. When `any` is used in a union type, the resulting type is still `any`. 4 | 5 | **Bad**: 6 | 7 | ```ts 8 | function f(x: string | any): void; 9 | ``` 10 | 11 | **Good**: 12 | 13 | ```ts 14 | function f(x: string): void; 15 | ``` 16 | 17 | Or: 18 | ```ts 19 | function f(x: any): void; 20 | ``` 21 | 22 | Or: 23 | ```ts 24 | function f(x: string | object): void; 25 | ``` 26 | 27 | While the `string` portion of this type annotation may _look_ useful, it in fact offers no additional typechecking over simply using `any`. 28 | -------------------------------------------------------------------------------- /docs/no-bad-reference.md: -------------------------------------------------------------------------------- 1 | # no-bad-reference 2 | 3 | (This rule is specific to DefinitelyTyped.) 4 | Avoid using ``. 5 | 6 | **Bad**: 7 | 8 | ```ts 9 | /// 10 | import * as foo from "foo"; 11 | ``` 12 | 13 | **Good**: 14 | 15 | If "foo" is written in external module style (see `no-single-declare-module`), the import alone should work thanks to [module resolution](http://www.typescriptlang.org/docs/handbook/module-resolution.html): 16 | 17 | ```ts 18 | // TypeScript will look for a definition for "foo" using module resolution 19 | import * as foo from "foo"; 20 | ``` 21 | 22 | If not, use `` instead: 23 | 24 | ```ts 25 | /// 26 | ``` 27 | 28 | The only time `` should be necessary if for global (not module) libraries that are separated into multiple files; the index file must include references to the others to bring them into the compilation. 29 | -------------------------------------------------------------------------------- /docs/no-const-enum.md: -------------------------------------------------------------------------------- 1 | # no-const-enum 2 | 3 | Avoid using `const enum`s. These can't be used by JavaScript users or by TypeScript users with [`--isolatedModules`](https://www.typescriptlang.org/docs/handbook/compiler-options.html) enabled. 4 | 5 | **Bad**: 6 | 7 | ```ts 8 | const enum Bit { Off, On } 9 | export function f(b: Bit): void; 10 | ``` 11 | 12 | **Good**: 13 | 14 | ```ts 15 | export function f(b: 0 | 1): void; 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/no-dead-reference.md: -------------------------------------------------------------------------------- 1 | # no-dead-reference 2 | 3 | A `` comment should go at the top of a file -- otherwise it is just a normal comment. 4 | 5 | **Bad**: 6 | 7 | ```ts 8 | console.log("Hello world!"); 9 | /// 10 | ``` 11 | 12 | **Good**: 13 | 14 | ```ts 15 | /// 16 | console.log("Hello world!"); 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/no-declare-current-package.md: -------------------------------------------------------------------------------- 1 | # no-declare-current-package 2 | 3 | Avoid using `declare module`, and prefer to declare module contents in a file. 4 | 5 | **Bad**: 6 | 7 | ```ts 8 | // foo/index.d.ts 9 | declare module "foo" { 10 | export const x = 0; 11 | } 12 | ``` 13 | 14 | **Good**: 15 | 16 | ```ts 17 | // foo/index.d.ts 18 | export const x = 0; 19 | ``` 20 | 21 | **Bad**: 22 | 23 | ```ts 24 | // foo/index.d.ts 25 | declare module "foo/bar" { 26 | export const x = 0; 27 | } 28 | ``` 29 | 30 | **Good**: 31 | 32 | ```ts 33 | // foo/bar.d.ts 34 | export const x = 0; 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/no-import-default-of-export-equals.md: -------------------------------------------------------------------------------- 1 | # no-import-default-of-export-equals 2 | 3 | Don't use a default import of a package that uses `export =`. 4 | Users who do not have `--allowSyntheticDefaultExports` or `--esModuleInterop` will get different behavior. 5 | This rule only applies to definition files -- for test files you can use a default import if you prefer. 6 | 7 | **Bad**: 8 | 9 | ```ts 10 | // foo/index.d.ts 11 | declare interface I {} 12 | export = I; 13 | 14 | // bar/index.d.ts 15 | import I from "foo"; 16 | ``` 17 | 18 | **Good**: 19 | 20 | ```ts 21 | import I = require("foo"); 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/no-outside-dependencies.md: -------------------------------------------------------------------------------- 1 | # no-outside-dependencies 2 | 3 | Don't import from `DefinitelyTyped/node_modules`. 4 | 5 | **Bad**: 6 | 7 | ```ts 8 | import * as x from "x"; 9 | // where 'x' is defined only in `DefinitelyTyped/node_modules` 10 | ``` 11 | 12 | **Good**: 13 | 14 | Add a `package.json`: 15 | 16 | ```ts 17 | { 18 | "private": true, 19 | "dependencies": { 20 | "x": "^1.2.3" 21 | } 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/no-padding.md: -------------------------------------------------------------------------------- 1 | # no-padding 2 | 3 | Avoid blank lines before opening tokens or after closing tokens. 4 | 5 | **Bad**: 6 | 7 | ```ts 8 | function f() { 9 | 10 | return [ 11 | 12 | g( 13 | 14 | 0 15 | 16 | ) 17 | 18 | ]; 19 | 20 | } 21 | ``` 22 | 23 | **Good**: 24 | 25 | ```ts 26 | function f() { 27 | return [ 28 | g( 29 | 0 30 | ) 31 | ]; 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/no-relative-import-in-test.md: -------------------------------------------------------------------------------- 1 | # no-relative-import-in-test 2 | 3 | A test file should not contain relative imports; it should use a global import of the library using [module resolution](http://www.typescriptlang.org/docs/handbook/module-resolution.html). 4 | 5 | **Bad**: 6 | 7 | ```ts 8 | import foo from "./index.d.ts"; 9 | ``` 10 | 11 | **Good**: 12 | 13 | ```ts 14 | import foo from "foo"; 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/no-self-import.md: -------------------------------------------------------------------------------- 1 | # no-self-import 2 | 3 | A package should not import components of itself using a globally-qualified name; it should use relative imports instead. 4 | 5 | **Bad**: 6 | 7 | ```ts 8 | import foo from "this-package/foo.d.ts"; 9 | ``` 10 | 11 | **Good**: 12 | 13 | ```ts 14 | import foo from "./foo.d.ts"; 15 | ``` 16 | 17 | **Bad**: 18 | 19 | ```ts 20 | import myself from "this-package"; 21 | ``` 22 | 23 | **Good**: 24 | 25 | ```ts 26 | import myself from "."; 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/no-single-declare-module.md: -------------------------------------------------------------------------------- 1 | # no-single-declare-module 2 | 3 | `declare module` should typically be avoided. 4 | Instead, the file itself should be used as the declaration for the module. 5 | TypeScript uses [module resolution](http://www.typescriptlang.org/docs/handbook/module-resolution.html) to determine what files are associated with what modules. 6 | 7 | **Bad**: 8 | 9 | ```ts 10 | declare module "mylib" { 11 | function foo(): number; 12 | } 13 | ``` 14 | 15 | **Good**: 16 | 17 | ```ts 18 | export function foo(): number; 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/no-single-element-tuple-type.md: -------------------------------------------------------------------------------- 1 | # no-single-element-tuple-type 2 | 3 | Some users mistakenly write `[T]` when then intend to write an array type `T[]`. 4 | 5 | **Bad**: 6 | 7 | ```ts 8 | export const x: [T]; 9 | ``` 10 | 11 | **Good**: 12 | 13 | ```ts 14 | export const x: T[]; 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/no-unnecessary-generics.md: -------------------------------------------------------------------------------- 1 | # no-unnecessary-generics 2 | 3 | Forbids a function to use a generic type parameter only once. 4 | Generic type parameters allow you to relate the type of one thing to another; 5 | if they are used only once, they can be replaced with their type constraint. 6 | 7 | **Bad**: 8 | 9 | ```ts 10 | function logAnything(x: T): void; 11 | ``` 12 | 13 | **Good**: 14 | 15 | ```ts 16 | function logAnything(x: any): void; 17 | ``` 18 | 19 | --- 20 | 21 | **Bad**: 22 | 23 | ```ts 24 | function useLogger(logger: T): void; 25 | ``` 26 | 27 | **Good**: 28 | 29 | ```ts 30 | function useLogger(logger: Logger): void; 31 | ``` 32 | 33 | --- 34 | 35 | **Bad**: 36 | 37 | ```ts 38 | function clear(array: T[]): void; 39 | ``` 40 | 41 | **Good**: 42 | 43 | ```ts 44 | function clear(array: any[]): void; 45 | ``` 46 | 47 | --- 48 | 49 | `getMeAT(): T`: 50 | If a type parameter does not appear in the types of any parameters, you don't really have a generic function, you just have a disguised type assertion. 51 | Prefer to use a real type assertion, e.g. `getMeAT() as number`. 52 | Example where a type parameter is acceptable: `function id(value: T): T;`. 53 | Example where it is not acceptable: `function parseJson(json: string): T;`. 54 | Exception: `new Map()` is OK. 55 | 56 | **Bad**: 57 | 58 | ```ts 59 | function parse(): T; 60 | const x = parse(); 61 | ``` 62 | 63 | **Good**: 64 | 65 | 66 | ```ts 67 | function parse(): {}; 68 | const x = parse() as number; 69 | ``` 70 | -------------------------------------------------------------------------------- /docs/no-useless-files.md: -------------------------------------------------------------------------------- 1 | # no-useless-files 2 | 3 | Don't include empty files. 4 | 5 | **Bad**: 6 | 7 | ```ts 8 | ``` 9 | 10 | **Good**: 11 | 12 | ```ts 13 | export function something(): void; 14 | ``` 15 | -------------------------------------------------------------------------------- /docs/npm-naming.md: -------------------------------------------------------------------------------- 1 | # npm-naming 2 | 3 | (This rule is specific to DefinitelyTyped.) 4 | 5 | ## Name checks 6 | In 'name-only' mode, checks that the name of the type package matches a source package on npm. 7 | 8 | --- 9 | 10 | **Bad**: 11 | 12 | ```ts 13 | // Type definitions for browser-only-package 1.2 14 | ``` 15 | 16 | * If the package is really browser-only, you have to mark it with "non-npm package". 17 | * If the package actually has a matching npm package, you must use that name. 18 | 19 | **Good**: 20 | 21 | ```ts 22 | // Type definitions for non-npm package browser-only-package 1.2 23 | ``` 24 | 25 | --- 26 | 27 | **Bad**: 28 | 29 | ```ts 30 | // Type definitions for some-package 101.1 31 | ``` 32 | 33 | * The version number in the header must actually exist on npm for the source package. 34 | 35 | **Good**: 36 | 37 | ```ts 38 | // Type definitions for some-package 10.1 39 | ``` 40 | 41 | ## Code checks 42 | 43 | In 'code' mode, in addition to the name checks, this rule also checks that the source JavaScript code matches the declaration file for npm packages. 44 | 45 | --- 46 | 47 | **Bad**: 48 | 49 | `foo/index.d.ts`: 50 | 51 | ```ts 52 | declare function f(): void; 53 | export default f; 54 | ``` 55 | 56 | `foo/index.js`: 57 | 58 | ```js 59 | module.exports = function () { 60 | }; 61 | ``` 62 | 63 | * A CommonJs module.exports assignment is not really an export default, and the d.ts should use the [`export =`](https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require) syntax. 64 | * `export default` can only be used to export a CommonJs `module.exports =` when you have `esModuleInterop` turned on, which not everybody does. 65 | 66 | **Good**: 67 | 68 | `foo/index.d.ts`: 69 | 70 | ```ts 71 | declare function f(): void; 72 | export = f; 73 | ``` 74 | 75 | --- 76 | 77 | **Bad**: 78 | 79 | `foo/index.d.ts`: 80 | 81 | ```ts 82 | export class C {} 83 | ``` 84 | 85 | `foo/index.js`: 86 | 87 | ```js 88 | module.exports = class C {} 89 | ``` 90 | 91 | * The CommonJs module is a class, which means it can be constructed, like this: 92 | ```js 93 | var C = require('foo'); 94 | var x = new C(); 95 | ``` 96 | However, the way `class C` is exported in the d.ts file (using an export declaration) means it can only be used like this: 97 | ```ts 98 | var foo = require('foo'); 99 | var x = new foo.C(); 100 | ``` 101 | 102 | * The d.ts should use [`export =`](https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require) 103 | syntax to match the CommonJs module behavior. 104 | 105 | **Good**: 106 | 107 | `foo/index.d.ts`: 108 | 109 | ```ts 110 | declare class C {} 111 | export = C; 112 | ``` 113 | 114 | * If you need to use `export =` syntax as in the example above, and the source JavaScript also exports some properties, 115 | you might need to use [*declaration merging*](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-namespaces-with-classes-functions-and-enums) in your d.ts. Example: 116 | 117 | **JavaScript**: 118 | 119 | `foo/index.js`: 120 | 121 | ```js 122 | function foo() {}; 123 | foo.bar = "Exported property"; 124 | module.exports = foo; // module.exports is a function, but it also has a property called `bar` 125 | ``` 126 | 127 | **Declaration**: 128 | 129 | `foo/index.d.ts`: 130 | 131 | ```ts 132 | declare function foo(): void; 133 | declare namespace foo { 134 | var bar: string; 135 | } 136 | export = foo; 137 | ``` 138 | -------------------------------------------------------------------------------- /docs/prefer-declare-function.md: -------------------------------------------------------------------------------- 1 | # prefer-declare-function 2 | 3 | Prefer to declare a function using the `function` keyword instead of a variable of function type. 4 | 5 | **Bad**: 6 | 7 | ```ts 8 | export const f: () => number; 9 | ``` 10 | 11 | **Good**: 12 | 13 | ```ts 14 | export function f(): number; 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/redundant-undefined.md: -------------------------------------------------------------------------------- 1 | # redundant-undefined 2 | 3 | Avoid explicitly specifying `undefined` as a type for a parameter which is already optional. 4 | 5 | **Bad**: 6 | 7 | ```ts 8 | function f(s?: string | undefined): void {} 9 | ``` 10 | 11 | **Good**: 12 | 13 | ```ts 14 | function f(s?: string): void {} 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/strict-export-declare-modifiers.md: -------------------------------------------------------------------------------- 1 | # strict-export-declare-modifiers 2 | 3 | Avoid adding the `declare` keyword unnecessarily. 4 | Do add the `export` keyword unnecessarily, because sometimes it is necessary and we want to be consistent. 5 | 6 | **Bad**: 7 | 8 | ```ts 9 | export declare function f(): void; 10 | declare function g(): void; 11 | interface I {} 12 | ``` 13 | 14 | 15 | **Good**: 16 | 17 | ```ts 18 | export function f(): void; 19 | export function g(): void; 20 | export interface I {} 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/trim-file.md: -------------------------------------------------------------------------------- 1 | # trim-file 2 | 3 | Don't include blank lines at the beginning or end of a file. 4 | 5 | **Bad**: 6 | 7 | ```ts 8 | 9 | export function f(): number; 10 | 11 | ``` 12 | 13 | **Good**: 14 | 15 | ```ts 16 | export function f(): number; 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/void-return.md: -------------------------------------------------------------------------------- 1 | # void-return 2 | 3 | `void` should be used as a return type, but not as a parameter type. 4 | 5 | **Bad**: 6 | 7 | ```ts 8 | export function f(x: string | void): undefined; 9 | ``` 10 | 11 | **Good**: 12 | 13 | ```ts 14 | export function f(x: string | undefined): void; 15 | ``` 16 | -------------------------------------------------------------------------------- /dt.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./dtslint.json", 3 | "rules": { 4 | "dt-header": true, 5 | "no-bad-reference": true, 6 | "no-declare-current-package": true, 7 | "no-self-import": true, 8 | "no-outside-dependencies": true, 9 | 10 | "no-redundant-jsdoc": false, 11 | "no-redundant-jsdoc-2": true, 12 | 13 | "npm-naming": [true, { "mode": "code" }] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /dtslint-expect-only.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": "./bin/rules", 3 | "rules": { 4 | "expect": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /dtslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:all", 3 | "rulesDirectory": "./bin/rules", 4 | "rules": { 5 | // Custom rules 6 | "expect": true, 7 | "export-just-namespace": true, 8 | "no-bad-reference": true, 9 | "no-const-enum": true, 10 | "no-dead-reference": true, 11 | "no-import-default-of-export-equals": true, 12 | "no-padding": true, 13 | "redundant-undefined": true, 14 | "no-relative-import-in-test": true, 15 | "strict-export-declare-modifiers": true, 16 | "no-any-union": true, 17 | "no-single-declare-module": true, 18 | "no-unnecessary-generics": true, 19 | "no-useless-files": true, 20 | "prefer-declare-function": true, 21 | "trim-file": true, 22 | "unified-signatures": true, 23 | "void-return": true, 24 | "npm-naming": true, 25 | 26 | "comment-format": [true, "check-space"], // But not check-uppercase or check-lowercase 27 | "interface-name": [true, "never-prefix"], 28 | "max-line-length": [true, 200], 29 | "member-access": [true, "no-public"], 30 | "no-consecutive-blank-lines": true, 31 | "no-unnecessary-callback-wrapper": true, 32 | "no-namespace": [true, "allow-declarations"], 33 | "object-literal-key-quotes": [true, "as-needed"], 34 | "one-line": [ 35 | true, 36 | "check-catch", 37 | "check-finally", 38 | "check-else", 39 | "check-open-brace", 40 | "check-whitespace" 41 | ], 42 | "one-variable-per-declaration": [true, "ignore-for-loop"], 43 | "only-arrow-functions": [true, "allow-declarations", "allow-named-functions"], 44 | "prefer-template": [true, "allow-single-concat"], 45 | "whitespace": [ 46 | true, 47 | "check-branch", 48 | "check-decl", 49 | "check-operator", 50 | "check-module", 51 | "check-separator", 52 | "check-type", 53 | "check-typecast" 54 | ], 55 | 56 | // TODO? 57 | "align": false, // TODO 58 | "arrow-parens": false, 59 | "arrow-return-shorthand": true, // TODO: "multiline" 60 | "linebreak-style": false, // TODO 61 | "no-void-expression": [true, "ignore-arrow-function-shorthand"], 62 | "no-any": false, // TODO 63 | "no-floating-promises": false, // TODO: https://github.com/palantir/tslint/issues/2879 64 | "no-import-side-effect": false, 65 | "no-this-assignment": false, 66 | "no-unbound-method": false, // TODO? 67 | "no-unsafe-any": false, // TODO 68 | "no-restricted-globals": false, 69 | "number-literal-format": false, // TODO 70 | "promise-function-async": false, 71 | "restrict-plus-operands": false, // TODO 72 | "return-undefined": false, // TODO 73 | "switch-final-break": false, // TODO 74 | "prefer-method-signature": false, // TODO? 75 | 76 | // Pretty sure we don't want these 77 | "binary-expression-operand-order": false, 78 | "class-name": false, 79 | "completed-docs": false, 80 | "curly": false, 81 | "cyclomatic-complexity": false, 82 | "deprecation": false, 83 | "file-name-casing": false, 84 | "forin": false, 85 | "indent": false, 86 | "match-default-export-name": false, 87 | "max-classes-per-file": false, 88 | "max-file-line-count": false, 89 | "member-ordering": false, 90 | "newline-before-return": false, 91 | "newline-per-chained-call": false, 92 | "no-bitwise": false, 93 | "no-console": false, 94 | "no-default-export": false, 95 | "no-empty": false, 96 | "no-implicit-dependencies": false, // See https://github.com/palantir/tslint/issues/3364 97 | "no-inferred-empty-object-type": false, 98 | "no-magic-numbers": false, 99 | "no-non-null-assertion": false, 100 | "no-null-keyword": false, 101 | "no-parameter-properties": false, 102 | "no-parameter-reassignment": false, 103 | "no-reference": false, // But see no-bad-reference 104 | "no-require-imports": false, 105 | "no-shadowed-variable": false, 106 | "no-string-literal": false, 107 | "no-submodule-imports": false, 108 | "no-tautology-expression": false, 109 | "no-unused-expression": false, 110 | "no-unused-variable": false, 111 | "no-use-before-declare": false, 112 | "object-literal-sort-keys": false, 113 | "ordered-imports": false, 114 | "prefer-function-over-method": false, 115 | "quotemark": false, 116 | "strict-boolean-expressions": false, 117 | "strict-type-predicates": false, 118 | "switch-default": false, 119 | "trailing-comma": false, 120 | "triple-equals": [true, "allow-null-check"], 121 | "typedef": false, 122 | "type-literal-delimiter": false, 123 | "variable-name": false, 124 | "increment-decrement": false, 125 | "unnecessary-constructor": false, 126 | "unnecessary-else": false, 127 | "no-angle-bracket-type-assertion": false, 128 | "no-default-import": false, 129 | "callable-types": false 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dtslint", 3 | "version": "4.2.1", 4 | "description": "Runs tests on TypeScript definition files", 5 | "files": [ 6 | "bin", 7 | "dt.json", 8 | "dtslint.json", 9 | "dtslint-expect-only.json" 10 | ], 11 | "main": "bin", 12 | "bin": "./bin/index.js", 13 | "contributors": [ 14 | "Nathan Shively-Sanders (https://github.com/sandersn)", 15 | "Andy Hanson (https://github.com/andy-ms)", 16 | "Dan Vanderkam (https://github.com/danvk)" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/Microsoft/dtslint.git" 21 | }, 22 | "scripts": { 23 | "watch": "tsc --watch", 24 | "build": "tsc", 25 | "lint": "eslint --ext ts src", 26 | "test": "npm run build && node test/test.js", 27 | "prepublishOnly": "npm run build && npm run test && npm run lint" 28 | }, 29 | "dependencies": { 30 | "@definitelytyped/header-parser": "latest", 31 | "@definitelytyped/typescript-versions": "latest", 32 | "@definitelytyped/utils": "latest", 33 | "dts-critic": "latest", 34 | "fs-extra": "^6.0.1", 35 | "json-stable-stringify": "^1.0.1", 36 | "strip-json-comments": "^2.0.1", 37 | "tslint": "5.14.0", 38 | "tsutils": "^2.29.0", 39 | "yargs": "^15.1.0" 40 | }, 41 | "peerDependencies": { 42 | "typescript": ">= 3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.7.0-dev || >= 3.8.0-dev || >= 3.9.0-dev || >= 4.0.0-dev" 43 | }, 44 | "devDependencies": { 45 | "@types/fs-extra": "^5.0.2", 46 | "@types/json-stable-stringify": "^1.0.32", 47 | "@types/node": "14.0.x", 48 | "@types/strip-json-comments": "^0.0.28", 49 | "@types/yargs": "^15.0.3", 50 | "@typescript-eslint/eslint-plugin": "^4.11.1", 51 | "@typescript-eslint/parser": "^4.11.1", 52 | "eslint": "^7.16.0", 53 | "typescript": "next" 54 | }, 55 | "engines": { 56 | "node": ">=10.0.0" 57 | }, 58 | "license": "MIT" 59 | } 60 | -------------------------------------------------------------------------------- /src/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "attach", 10 | "name": "Attach", 11 | "port": 9229, 12 | "sourceMaps": true 13 | }, 14 | { 15 | "type": "node", 16 | "request": "launch", 17 | "name": "Launch Program", 18 | "program": "${file}" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /src/checks.ts: -------------------------------------------------------------------------------- 1 | import { makeTypesVersionsForPackageJson } from "@definitelytyped/header-parser"; 2 | import { TypeScriptVersion } from "@definitelytyped/typescript-versions"; 3 | import assert = require("assert"); 4 | import { pathExists } from "fs-extra"; 5 | import { join as joinPaths } from "path"; 6 | 7 | import { getCompilerOptions, readJson } from "./util"; 8 | 9 | export async function checkPackageJson( 10 | dirPath: string, 11 | typesVersions: ReadonlyArray, 12 | ): Promise { 13 | const pkgJsonPath = joinPaths(dirPath, "package.json"); 14 | const needsTypesVersions = typesVersions.length !== 0; 15 | if (!await pathExists(pkgJsonPath)) { 16 | if (needsTypesVersions) { 17 | throw new Error(`${dirPath}: Must have 'package.json' for "typesVersions"`); 18 | } 19 | return; 20 | } 21 | 22 | const pkgJson = await readJson(pkgJsonPath) as Record; 23 | 24 | if ((pkgJson as any).private !== true) { 25 | throw new Error(`${pkgJsonPath} should set \`"private": true\``); 26 | } 27 | 28 | if (needsTypesVersions) { 29 | assert.strictEqual((pkgJson as any).types, "index", `"types" in '${pkgJsonPath}' should be "index".`); 30 | const expected = makeTypesVersionsForPackageJson(typesVersions); 31 | assert.deepEqual((pkgJson as any).typesVersions, expected, 32 | `"typesVersions" in '${pkgJsonPath}' is not set right. Should be: ${JSON.stringify(expected, undefined, 4)}`); 33 | } 34 | 35 | for (const key in pkgJson) { // tslint:disable-line forin 36 | switch (key) { 37 | case "private": 38 | case "dependencies": 39 | case "license": 40 | case "imports": 41 | case "exports": 42 | case "type": 43 | // "private"/"typesVersions"/"types" checked above, "dependencies" / "license" checked by types-publisher, 44 | break; 45 | case "typesVersions": 46 | case "types": 47 | if (!needsTypesVersions) { 48 | throw new Error(`${pkgJsonPath} doesn't need to set "${key}" when no 'ts3.x' directories exist.`); 49 | } 50 | break; 51 | default: 52 | throw new Error(`${pkgJsonPath} should not include field ${key}`); 53 | } 54 | } 55 | } 56 | 57 | export interface DefinitelyTypedInfo { 58 | /** "../" or "../../" or "../../../". This should use '/' even on windows. */ 59 | readonly relativeBaseUrl: string; 60 | } 61 | export async function checkTsconfig(dirPath: string, dt: DefinitelyTypedInfo | undefined): Promise { 62 | const options = await getCompilerOptions(dirPath); 63 | 64 | if (dt) { 65 | const { relativeBaseUrl } = dt; 66 | 67 | const mustHave = { 68 | module: "commonjs", 69 | noEmit: true, 70 | forceConsistentCasingInFileNames: true, 71 | baseUrl: relativeBaseUrl, 72 | typeRoots: [relativeBaseUrl], 73 | types: [], 74 | }; 75 | 76 | for (const key of Object.getOwnPropertyNames(mustHave) as Array) { 77 | const expected = mustHave[key]; 78 | const actual = options[key]; 79 | if (!deepEquals(expected, actual)) { 80 | throw new Error(`Expected compilerOptions[${JSON.stringify(key)}] === ${JSON.stringify(expected)}`); 81 | } 82 | } 83 | 84 | for (const key in options) { // tslint:disable-line forin 85 | switch (key) { 86 | case "lib": 87 | case "noImplicitAny": 88 | case "noImplicitThis": 89 | case "strict": 90 | case "strictNullChecks": 91 | case "noUncheckedIndexedAccess": 92 | case "strictFunctionTypes": 93 | case "esModuleInterop": 94 | case "allowSyntheticDefaultImports": 95 | // Allow any value 96 | break; 97 | case "target": 98 | case "paths": 99 | case "jsx": 100 | case "jsxFactory": 101 | case "experimentalDecorators": 102 | case "noUnusedLocals": 103 | case "noUnusedParameters": 104 | // OK. "paths" checked further by types-publisher 105 | break; 106 | default: 107 | if (!(key in mustHave)) { 108 | throw new Error(`Unexpected compiler option ${key}`); 109 | } 110 | } 111 | } 112 | } 113 | 114 | if (!("lib" in options)) { 115 | throw new Error('Must specify "lib", usually to `"lib": ["es6"]` or `"lib": ["es6", "dom"]`.'); 116 | } 117 | 118 | if ("strict" in options) { 119 | if (options.strict !== true) { 120 | throw new Error('When "strict" is present, it must be set to `true`.'); 121 | } 122 | 123 | for (const key of ["noImplicitAny", "noImplicitThis", "strictNullChecks", "strictFunctionTypes"]) { 124 | if (key in options) { 125 | throw new TypeError(`Expected "${key}" to not be set when "strict" is \`true\`.`); 126 | } 127 | } 128 | } else { 129 | for (const key of ["noImplicitAny", "noImplicitThis", "strictNullChecks", "strictFunctionTypes"]) { 130 | if (!(key in options)) { 131 | throw new Error(`Expected \`"${key}": true\` or \`"${key}": false\`.`); 132 | } 133 | } 134 | } 135 | 136 | if (options.types && options.types.length) { 137 | throw new Error( 138 | 'Use `/// ` directives in source files and ensure ' + 139 | 'that the "types" field in your tsconfig is an empty array.'); 140 | } 141 | } 142 | 143 | function deepEquals(expected: unknown, actual: unknown): boolean { 144 | if (expected instanceof Array) { 145 | return actual instanceof Array 146 | && actual.length === expected.length 147 | && expected.every((e, i) => deepEquals(e, actual[i])); 148 | } else { 149 | return expected === actual; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { parseTypeScriptVersionLine } from "@definitelytyped/header-parser"; 4 | import { AllTypeScriptVersion, TypeScriptVersion } from "@definitelytyped/typescript-versions"; 5 | import assert = require("assert"); 6 | import { readdir, readFile, stat } from "fs-extra"; 7 | import { basename, dirname, join as joinPaths, resolve } from "path"; 8 | 9 | import { cleanTypeScriptInstalls, installAllTypeScriptVersions, installTypeScriptNext } from "@definitelytyped/utils"; 10 | import { checkPackageJson, checkTsconfig } from "./checks"; 11 | import { checkTslintJson, lint, TsVersion } from "./lint"; 12 | import { mapDefinedAsync, withoutPrefix } from "./util"; 13 | 14 | async function main(): Promise { 15 | const args = process.argv.slice(2); 16 | let dirPath = process.cwd(); 17 | let onlyTestTsNext = false; 18 | let expectOnly = false; 19 | let shouldListen = false; 20 | let lookingForTsLocal = false; 21 | let tsLocal: string | undefined; 22 | 23 | for (const arg of args) { 24 | if (lookingForTsLocal) { 25 | if (arg.startsWith("--")) { 26 | throw new Error("Looking for local path for TS, but got " + arg); 27 | } 28 | tsLocal = resolve(arg); 29 | lookingForTsLocal = false; 30 | continue; 31 | } 32 | switch (arg) { 33 | case "--installAll": 34 | console.log("Cleaning old installs and installing for all TypeScript versions..."); 35 | console.log("Working..."); 36 | await cleanTypeScriptInstalls(); 37 | await installAllTypeScriptVersions(); 38 | return; 39 | case "--localTs": 40 | lookingForTsLocal = true; 41 | break; 42 | case "--version": 43 | console.log(require("../package.json").version); 44 | return; 45 | case "--expectOnly": 46 | expectOnly = true; 47 | break; 48 | case "--onlyTestTsNext": 49 | onlyTestTsNext = true; 50 | break; 51 | // Only for use by types-publisher. 52 | // Listens for { path, onlyTestTsNext } messages and ouputs { path, status }. 53 | case "--listen": 54 | shouldListen = true; 55 | break; 56 | default: { 57 | if (arg.startsWith("--")) { 58 | console.error(`Unknown option '${arg}'`); 59 | usage(); 60 | process.exit(1); 61 | } 62 | 63 | const path = arg.indexOf("@") === 0 && arg.indexOf("/") !== -1 64 | // we have a scoped module, e.g. @bla/foo 65 | // which should be converted to bla__foo 66 | ? arg.substr(1).replace("/", "__") 67 | : arg; 68 | dirPath = joinPaths(dirPath, path); 69 | } 70 | } 71 | } 72 | if (lookingForTsLocal) { 73 | throw new Error("Path for --localTs was not provided."); 74 | } 75 | 76 | if (shouldListen) { 77 | listen(dirPath, tsLocal, onlyTestTsNext); 78 | } else { 79 | await installTypeScriptAsNeeded(tsLocal, onlyTestTsNext); 80 | await runTests(dirPath, onlyTestTsNext, expectOnly, tsLocal); 81 | } 82 | } 83 | 84 | async function installTypeScriptAsNeeded(tsLocal: string | undefined, onlyTestTsNext: boolean): Promise { 85 | if (tsLocal) return; 86 | if (onlyTestTsNext) { 87 | return installTypeScriptNext(); 88 | } 89 | return installAllTypeScriptVersions(); 90 | } 91 | 92 | function usage(): void { 93 | console.error("Usage: dtslint [--version] [--installAll] [--onlyTestTsNext] [--expectOnly] [--localTs path]"); 94 | console.error("Args:"); 95 | console.error(" --version Print version and exit."); 96 | console.error(" --installAll Cleans and installs all TypeScript versions."); 97 | console.error(" --expectOnly Run only the ExpectType lint rule."); 98 | console.error(" --onlyTestTsNext Only run with `typescript@next`, not with the minimum version."); 99 | console.error(" --localTs path Run with *path* as the latest version of TS."); 100 | console.error(""); 101 | console.error("onlyTestTsNext and localTs are (1) mutually exclusive and (2) test a single version of TS"); 102 | } 103 | 104 | function listen(dirPath: string, tsLocal: string | undefined, alwaysOnlyTestTsNext: boolean): void { 105 | // Don't await this here to ensure that messages sent during installation aren't dropped. 106 | const installationPromise = installTypeScriptAsNeeded(tsLocal, alwaysOnlyTestTsNext); 107 | process.on("message", async (message: unknown) => { 108 | const { path, onlyTestTsNext, expectOnly } = message as { path: string, onlyTestTsNext: boolean, expectOnly?: boolean }; 109 | 110 | await installationPromise; 111 | runTests(joinPaths(dirPath, path), onlyTestTsNext, !!expectOnly, tsLocal) 112 | .catch(e => e.stack) 113 | .then(maybeError => { 114 | process.send!({ path, status: maybeError === undefined ? "OK" : maybeError }); 115 | }) 116 | .catch(e => console.error(e.stack)); 117 | }); 118 | } 119 | 120 | async function runTests( 121 | dirPath: string, 122 | onlyTestTsNext: boolean, 123 | expectOnly: boolean, 124 | tsLocal: string | undefined, 125 | ): Promise { 126 | const isOlderVersion = /^v(0\.)?\d+$/.test(basename(dirPath)); 127 | 128 | const indexText = await readFile(joinPaths(dirPath, "index.d.ts"), "utf-8"); 129 | // If this *is* on DefinitelyTyped, types-publisher will fail if it can't parse the header. 130 | const dt = indexText.includes("// Type definitions for"); 131 | if (dt) { 132 | // Someone may have copied text from DefinitelyTyped to their type definition and included a header, 133 | // so assert that we're really on DefinitelyTyped. 134 | assertPathIsInDefinitelyTyped(dirPath); 135 | assertPathIsNotBanned(dirPath); 136 | } 137 | 138 | const typesVersions = await mapDefinedAsync(await readdir(dirPath), async name => { 139 | if (name === "tsconfig.json" || name === "tslint.json" || name === "tsutils") { return undefined; } 140 | const version = withoutPrefix(name, "ts"); 141 | if (version === undefined || !(await stat(joinPaths(dirPath, name))).isDirectory()) { return undefined; } 142 | 143 | if (!TypeScriptVersion.isTypeScriptVersion(version)) { 144 | throw new Error(`There is an entry named ${name}, but ${version} is not a valid TypeScript version.`); 145 | } 146 | if (!TypeScriptVersion.isRedirectable(version)) { 147 | throw new Error(`At ${dirPath}/${name}: TypeScript version directories only available starting with ts3.1.`); 148 | } 149 | return version; 150 | }); 151 | 152 | if (dt) { 153 | await checkPackageJson(dirPath, typesVersions); 154 | } 155 | 156 | const minVersion = maxVersion( 157 | getMinimumTypeScriptVersionFromComment(indexText), 158 | TypeScriptVersion.lowest) as TypeScriptVersion; 159 | if (onlyTestTsNext || tsLocal) { 160 | const tsVersion = tsLocal ? "local" : TypeScriptVersion.latest; 161 | await testTypesVersion(dirPath, tsVersion, tsVersion, isOlderVersion, dt, expectOnly, tsLocal, /*isLatest*/ true); 162 | } else { 163 | // For example, typesVersions of [3.2, 3.5, 3.6] will have 164 | // associated ts3.2, ts3.5, ts3.6 directories, for 165 | // <=3.2, <=3.5, <=3.6 respectively; the root level is for 3.7 and above. 166 | // so this code needs to generate ranges [lowest-3.2, 3.3-3.5, 3.6-3.6, 3.7-latest] 167 | const lows = [TypeScriptVersion.lowest, ...typesVersions.map(next)]; 168 | const his = [...typesVersions, TypeScriptVersion.latest]; 169 | assert.strictEqual(lows.length, his.length); 170 | for (let i = 0; i < lows.length; i++) { 171 | const low = maxVersion(minVersion, lows[i]); 172 | const hi = his[i]; 173 | assert( 174 | parseFloat(hi) >= parseFloat(low), 175 | `'// Minimum TypeScript Version: ${minVersion}' in header skips ts${hi} folder.`); 176 | const isLatest = hi === TypeScriptVersion.latest; 177 | const versionPath = isLatest ? dirPath : joinPaths(dirPath, `ts${hi}`); 178 | if (lows.length > 1) { 179 | console.log("testing from", low, "to", hi, "in", versionPath); 180 | } 181 | await testTypesVersion(versionPath, low, hi, isOlderVersion, dt, expectOnly, undefined, isLatest); 182 | } 183 | } 184 | } 185 | 186 | function maxVersion(v1: TypeScriptVersion | undefined, v2: TypeScriptVersion): TypeScriptVersion; 187 | function maxVersion(v1: AllTypeScriptVersion | undefined, v2: AllTypeScriptVersion): AllTypeScriptVersion; 188 | function maxVersion(v1: AllTypeScriptVersion | undefined, v2: AllTypeScriptVersion) { 189 | if (!v1) return v2; 190 | if (!v2) return v1; 191 | if (parseFloat(v1) >= parseFloat(v2)) return v1; 192 | return v2; 193 | } 194 | 195 | function next(v: TypeScriptVersion): TypeScriptVersion { 196 | const index = TypeScriptVersion.supported.indexOf(v); 197 | assert.notStrictEqual(index, -1); 198 | assert(index < TypeScriptVersion.supported.length); 199 | return TypeScriptVersion.supported[index + 1]; 200 | } 201 | 202 | async function testTypesVersion( 203 | dirPath: string, 204 | lowVersion: TsVersion, 205 | hiVersion: TsVersion, 206 | isOlderVersion: boolean, 207 | dt: boolean, 208 | expectOnly: boolean, 209 | tsLocal: string | undefined, 210 | isLatest: boolean, 211 | ): Promise { 212 | await checkTslintJson(dirPath, dt); 213 | await checkTsconfig(dirPath, dt 214 | ? { relativeBaseUrl: ".." + (isOlderVersion ? "/.." : "") + (isLatest ? "" : "/..") + "/" } 215 | : undefined); 216 | const err = await lint(dirPath, lowVersion, hiVersion, isLatest, expectOnly, tsLocal); 217 | if (err) { 218 | throw new Error(err); 219 | } 220 | } 221 | 222 | function assertPathIsInDefinitelyTyped(dirPath: string): void { 223 | const parent = dirname(dirPath); 224 | const types = /^v\d+(\.\d+)?$/.test(basename(dirPath)) ? dirname(parent) : parent; 225 | // TODO: It's not clear whether this assertion makes sense, and it's broken on Azure Pipelines 226 | // Re-enable it later if it makes sense. 227 | // const dt = dirname(types); 228 | // if (basename(dt) !== "DefinitelyTyped" || basename(types) !== "types") { 229 | if (basename(types) !== "types") { 230 | throw new Error("Since this type definition includes a header (a comment starting with `// Type definitions for`), " 231 | + "assumed this was a DefinitelyTyped package.\n" 232 | + "But it is not in a `DefinitelyTyped/types/xxx` directory: " 233 | + dirPath); 234 | } 235 | } 236 | 237 | function assertPathIsNotBanned(dirPath: string) { 238 | const basedir = basename(dirPath); 239 | if (/(^|\W)download($|\W)/.test(basedir) && 240 | basedir !== "download" && 241 | basedir !== "downloadjs" && 242 | basedir !== "s3-download-stream") { 243 | // Since npm won't release their banned-words list, we'll have to manually add to this list. 244 | throw new Error(`${dirPath}: Contains the word 'download', which is banned by npm.`); 245 | } 246 | } 247 | 248 | function getMinimumTypeScriptVersionFromComment(text: string): AllTypeScriptVersion | undefined { 249 | const match = text.match(/\/\/ (?:Minimum )?TypeScript Version: /); 250 | if (!match) { 251 | return undefined; 252 | } 253 | 254 | let line = text.slice(match.index, text.indexOf("\n", match.index)); 255 | if (line.endsWith("\r")) { 256 | line = line.slice(0, line.length - 1); 257 | } 258 | return parseTypeScriptVersionLine(line); 259 | } 260 | 261 | if (!module.parent) { 262 | main().catch(err => { 263 | console.error(err.stack); 264 | process.exit(1); 265 | }); 266 | } 267 | -------------------------------------------------------------------------------- /src/lint.ts: -------------------------------------------------------------------------------- 1 | import { TypeScriptVersion } from "@definitelytyped/typescript-versions"; 2 | import { typeScriptPath } from "@definitelytyped/utils"; 3 | import assert = require("assert"); 4 | import { pathExists } from "fs-extra"; 5 | import { dirname, join as joinPaths, normalize } from "path"; 6 | import { Configuration, ILinterOptions, Linter } from "tslint"; 7 | import * as TsType from "typescript"; 8 | type Configuration = typeof Configuration; 9 | type IConfigurationFile = Configuration.IConfigurationFile; 10 | 11 | import { getProgram, Options as ExpectOptions } from "./rules/expectRule"; 12 | 13 | import { readJson, withoutPrefix } from "./util"; 14 | 15 | export async function lint( 16 | dirPath: string, 17 | minVersion: TsVersion, 18 | maxVersion: TsVersion, 19 | isLatest: boolean, 20 | expectOnly: boolean, 21 | tsLocal: string | undefined): Promise { 22 | const tsconfigPath = joinPaths(dirPath, "tsconfig.json"); 23 | const lintProgram = Linter.createProgram(tsconfigPath); 24 | 25 | for (const version of [maxVersion, minVersion]) { 26 | const errors = testDependencies(version, dirPath, lintProgram, tsLocal); 27 | if (errors) { return errors; } 28 | } 29 | 30 | const lintOptions: ILinterOptions = { 31 | fix: false, 32 | formatter: "stylish", 33 | }; 34 | const linter = new Linter(lintOptions, lintProgram); 35 | const configPath = expectOnly ? joinPaths(__dirname, "..", "dtslint-expect-only.json") : getConfigPath(dirPath); 36 | const config = await getLintConfig(configPath, tsconfigPath, minVersion, maxVersion, tsLocal); 37 | 38 | for (const file of lintProgram.getSourceFiles()) { 39 | if (lintProgram.isSourceFileDefaultLibrary(file)) { continue; } 40 | 41 | const { fileName, text } = file; 42 | if (!fileName.includes("node_modules")) { 43 | const err = testNoTsIgnore(text) || testNoTslintDisables(text); 44 | if (err) { 45 | const { pos, message } = err; 46 | const place = file.getLineAndCharacterOfPosition(pos); 47 | return `At ${fileName}:${JSON.stringify(place)}: ${message}`; 48 | } 49 | } 50 | 51 | // External dependencies should have been handled by `testDependencies`; 52 | // typesVersions should be handled in a separate lint 53 | if (!isExternalDependency(file, dirPath, lintProgram) && 54 | (!isLatest || !isTypesVersionPath(fileName, dirPath))) { 55 | linter.lint(fileName, text, config); 56 | } 57 | } 58 | 59 | const result = linter.getResult(); 60 | return result.failures.length ? result.output : undefined; 61 | } 62 | 63 | function testDependencies( 64 | version: TsVersion, 65 | dirPath: string, 66 | lintProgram: TsType.Program, 67 | tsLocal: string | undefined, 68 | ): string | undefined { 69 | const tsconfigPath = joinPaths(dirPath, "tsconfig.json"); 70 | assert(version !== "local" || tsLocal); 71 | const ts: typeof TsType = require(typeScriptPath(version, tsLocal)); 72 | const program = getProgram(tsconfigPath, ts, version, lintProgram); 73 | const diagnostics = ts.getPreEmitDiagnostics(program).filter(d => !d.file || isExternalDependency(d.file, dirPath, program)); 74 | if (!diagnostics.length) { return undefined; } 75 | 76 | const showDiags = ts.formatDiagnostics(diagnostics, { 77 | getCanonicalFileName: f => f, 78 | getCurrentDirectory: () => dirPath, 79 | getNewLine: () => "\n", 80 | }); 81 | 82 | const message = `Errors in typescript@${version} for external dependencies:\n${showDiags}`; 83 | 84 | // Add an edge-case for someone needing to `npm install` in react when they first edit a DT module which depends on it - #226 85 | const cannotFindDepsDiags = diagnostics.find(d => d.code === 2307 && d.messageText.toString().includes("Cannot find module")); 86 | if (cannotFindDepsDiags && cannotFindDepsDiags.file) { 87 | const path = cannotFindDepsDiags.file.fileName; 88 | const typesFolder = dirname(path); 89 | 90 | return ` 91 | A module look-up failed, this often occurs when you need to run \`npm install\` on a dependent module before you can lint. 92 | 93 | Before you debug, first try running: 94 | 95 | npm install --prefix ${typesFolder} 96 | 97 | Then re-run. Full error logs are below. 98 | 99 | ${message}`; 100 | } else { 101 | return message; 102 | } 103 | } 104 | 105 | export function isExternalDependency(file: TsType.SourceFile, dirPath: string, program: TsType.Program): boolean { 106 | return !startsWithDirectory(file.fileName, dirPath) || program.isSourceFileFromExternalLibrary(file); 107 | } 108 | 109 | function normalizePath(file: string) { 110 | // replaces '\' with '/' and forces all DOS drive letters to be upper-case 111 | return normalize(file) 112 | .replace(/\\/g, "/") 113 | .replace(/^[a-z](?=:)/, c => c.toUpperCase()); 114 | } 115 | 116 | function isTypesVersionPath(fileName: string, dirPath: string) { 117 | const normalFileName = normalizePath(fileName); 118 | const normalDirPath = normalizePath(dirPath); 119 | const subdirPath = withoutPrefix(normalFileName, normalDirPath); 120 | return subdirPath && /^\/ts\d+\.\d/.test(subdirPath); 121 | } 122 | 123 | function startsWithDirectory(filePath: string, dirPath: string): boolean { 124 | const normalFilePath = normalizePath(filePath); 125 | const normalDirPath = normalizePath(dirPath).replace(/\/$/, ""); 126 | return normalFilePath.startsWith(normalDirPath + "/") || normalFilePath.startsWith(normalDirPath + "\\"); 127 | } 128 | 129 | interface Err { pos: number; message: string; } 130 | function testNoTsIgnore(text: string): Err | undefined { 131 | const tsIgnore = "ts-ignore"; 132 | const pos = text.indexOf(tsIgnore); 133 | return pos === -1 ? undefined : { pos, message: "'ts-ignore' is forbidden." }; 134 | } 135 | function testNoTslintDisables(text: string): Err | undefined { 136 | const tslintDisable = "tslint:disable"; 137 | let lastIndex = 0; 138 | // eslint-disable-next-line no-constant-condition 139 | while (true) { 140 | const pos = text.indexOf(tslintDisable, lastIndex); 141 | if (pos === -1) { 142 | return undefined; 143 | } 144 | const end = pos + tslintDisable.length; 145 | const nextChar = text.charAt(end); 146 | if (nextChar !== "-" && nextChar !== ":") { 147 | const message = "'tslint:disable' is forbidden. " + 148 | "('tslint:disable:rulename', tslint:disable-line' and 'tslint:disable-next-line' are allowed.)"; 149 | return { pos, message }; 150 | } 151 | lastIndex = end; 152 | } 153 | } 154 | 155 | export async function checkTslintJson(dirPath: string, dt: boolean): Promise { 156 | const configPath = getConfigPath(dirPath); 157 | const shouldExtend = `dtslint/${dt ? "dt" : "dtslint"}.json`; 158 | const validateExtends = (extend: string | string[]) => 159 | extend === shouldExtend || (!dt && Array.isArray(extend) && extend.some(val => val === shouldExtend)); 160 | 161 | if (!await pathExists(configPath)) { 162 | if (dt) { 163 | throw new Error( 164 | `On DefinitelyTyped, must include \`tslint.json\` containing \`{ "extends": "${shouldExtend}" }\`.\n` + 165 | "This was inferred as a DefinitelyTyped package because it contains a `// Type definitions for` header."); 166 | } 167 | return; 168 | } 169 | 170 | const tslintJson = await readJson(configPath); 171 | if (!validateExtends(tslintJson.extends)) { 172 | throw new Error(`If 'tslint.json' is present, it should extend "${shouldExtend}"`); 173 | } 174 | } 175 | 176 | function getConfigPath(dirPath: string): string { 177 | return joinPaths(dirPath, "tslint.json"); 178 | } 179 | 180 | async function getLintConfig( 181 | expectedConfigPath: string, 182 | tsconfigPath: string, 183 | minVersion: TsVersion, 184 | maxVersion: TsVersion, 185 | tsLocal: string | undefined, 186 | ): Promise { 187 | const configExists = await pathExists(expectedConfigPath); 188 | const configPath = configExists ? expectedConfigPath : joinPaths(__dirname, "..", "dtslint.json"); 189 | // Second param to `findConfiguration` doesn't matter, since config path is provided. 190 | const config = Configuration.findConfiguration(configPath, "").results; 191 | if (!config) { 192 | throw new Error(`Could not load config at ${configPath}`); 193 | } 194 | 195 | const expectRule = config.rules.get("expect"); 196 | if (!expectRule || expectRule.ruleSeverity !== "error") { 197 | throw new Error("'expect' rule should be enabled, else compile errors are ignored"); 198 | } 199 | if (expectRule) { 200 | const versionsToTest = 201 | range(minVersion, maxVersion).map(versionName => ({ versionName, path: typeScriptPath(versionName, tsLocal) })); 202 | const expectOptions: ExpectOptions = { tsconfigPath, versionsToTest }; 203 | expectRule.ruleArguments = [expectOptions]; 204 | } 205 | return config; 206 | } 207 | 208 | function range(minVersion: TsVersion, maxVersion: TsVersion): ReadonlyArray { 209 | if (minVersion === "local") { 210 | assert(maxVersion === "local"); 211 | return ["local"]; 212 | } 213 | if (minVersion === TypeScriptVersion.latest) { 214 | assert(maxVersion === TypeScriptVersion.latest); 215 | return [TypeScriptVersion.latest]; 216 | } 217 | assert(maxVersion !== "local"); 218 | 219 | const minIdx = TypeScriptVersion.shipped.indexOf(minVersion); 220 | assert(minIdx >= 0); 221 | if (maxVersion === TypeScriptVersion.latest) { 222 | return [...TypeScriptVersion.shipped.slice(minIdx), TypeScriptVersion.latest]; 223 | } 224 | const maxIdx = TypeScriptVersion.shipped.indexOf(maxVersion as TypeScriptVersion); 225 | assert(maxIdx >= minIdx); 226 | return TypeScriptVersion.shipped.slice(minIdx, maxIdx + 1); 227 | } 228 | 229 | export type TsVersion = TypeScriptVersion | "local"; 230 | -------------------------------------------------------------------------------- /src/rules/dtHeaderRule.ts: -------------------------------------------------------------------------------- 1 | import { renderExpected, validate } from "@definitelytyped/header-parser"; 2 | import * as Lint from "tslint"; 3 | import * as ts from "typescript"; 4 | import { failure, isMainFile } from "../util"; 5 | 6 | export class Rule extends Lint.Rules.AbstractRule { 7 | static metadata: Lint.IRuleMetadata = { 8 | ruleName: "dt-header", 9 | description: "Ensure consistency of DefinitelyTyped headers.", 10 | optionsDescription: "Not configurable.", 11 | options: null, 12 | type: "functionality", 13 | typescriptOnly: true, 14 | }; 15 | 16 | apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 17 | return this.applyWithFunction(sourceFile, walk); 18 | } 19 | } 20 | 21 | function walk(ctx: Lint.WalkContext): void { 22 | const { sourceFile } = ctx; 23 | const { text } = sourceFile; 24 | const lookFor = (search: string, explanation: string) => { 25 | const idx = text.indexOf(search); 26 | if (idx !== -1) { 27 | ctx.addFailureAt(idx, search.length, failure(Rule.metadata.ruleName, explanation)); 28 | } 29 | }; 30 | if (!isMainFile(sourceFile.fileName, /*allowNested*/ true)) { 31 | lookFor("// Type definitions for", "Header should only be in `index.d.ts` of the root."); 32 | lookFor("// TypeScript Version", "TypeScript version should be specified under header in `index.d.ts`."); 33 | lookFor("// Minimum TypeScript Version", "TypeScript version should be specified under header in `index.d.ts`."); 34 | return; 35 | } 36 | 37 | lookFor("// Definitions by: My Self", "Author name should be your name, not the default."); 38 | const error = validate(text); 39 | if (error) { 40 | ctx.addFailureAt(error.index, 1, failure( 41 | Rule.metadata.ruleName, 42 | `Error parsing header. Expected: ${renderExpected(error.expected)}.`)); 43 | } 44 | // Don't recurse, we're done. 45 | } 46 | -------------------------------------------------------------------------------- /src/rules/expectRule.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; 2 | import os = require("os"); 3 | import { basename, dirname, join, resolve as resolvePath } from "path"; 4 | import * as Lint from "tslint"; 5 | import * as TsType from "typescript"; 6 | import { last } from "../util"; 7 | 8 | type Program = TsType.Program; 9 | type SourceFile = TsType.SourceFile; 10 | 11 | // Based on https://github.com/danvk/typings-checker 12 | 13 | const cacheDir = join(os.homedir(), ".dts"); 14 | const perfDir = join(os.homedir(), ".dts", "perf"); 15 | 16 | export class Rule extends Lint.Rules.TypedRule { 17 | /* tslint:disable:object-literal-sort-keys */ 18 | static metadata: Lint.IRuleMetadata = { 19 | ruleName: "expect", 20 | description: "Asserts types with $ExpectType and presence of errors with $ExpectError.", 21 | optionsDescription: "Not configurable.", 22 | options: null, 23 | type: "functionality", 24 | typescriptOnly: true, 25 | requiresTypeInfo: true, 26 | }; 27 | /* tslint:enable:object-literal-sort-keys */ 28 | 29 | static FAILURE_STRING_DUPLICATE_ASSERTION = "This line has 2 $ExpectType assertions."; 30 | static FAILURE_STRING_ASSERTION_MISSING_NODE = "Can not match a node to this assertion."; 31 | static FAILURE_STRING_EXPECTED_ERROR = "Expected an error on this line, but found none."; 32 | 33 | static FAILURE_STRING(expectedVersion: string, expectedType: string, actualType: string): string { 34 | return `TypeScript@${expectedVersion} expected type to be:\n ${expectedType}\ngot:\n ${actualType}`; 35 | } 36 | 37 | applyWithProgram(sourceFile: SourceFile, lintProgram: Program): Lint.RuleFailure[] { 38 | const options = this.ruleArguments[0] as Options | undefined; 39 | if (!options) { 40 | return this.applyWithFunction(sourceFile, ctx => 41 | walk(ctx, lintProgram, TsType, "next", /*nextHigherVersion*/ undefined)); 42 | } 43 | 44 | const { tsconfigPath, versionsToTest } = options; 45 | 46 | const getFailures = ( 47 | { versionName, path }: VersionToTest, 48 | nextHigherVersion: string | undefined, 49 | writeOutput: boolean, 50 | ) => { 51 | const ts = require(path); 52 | const program = getProgram(tsconfigPath, ts, versionName, lintProgram); 53 | const failures = this.applyWithFunction(sourceFile, ctx => walk(ctx, program, ts, versionName, nextHigherVersion)); 54 | if (writeOutput) { 55 | const packageName = basename(dirname(tsconfigPath)); 56 | if (!packageName.match(/v\d+/) && !packageName.match(/ts\d\.\d/)) { 57 | const d = { 58 | [packageName]: { 59 | typeCount: (program as any).getTypeCount(), 60 | memory: ts.sys.getMemoryUsage ? ts.sys.getMemoryUsage() : 0, 61 | }, 62 | }; 63 | if (!existsSync(cacheDir)) { 64 | mkdirSync(cacheDir); 65 | } 66 | if (!existsSync(perfDir)) { 67 | mkdirSync(perfDir); 68 | } 69 | writeFileSync(join(perfDir, `${packageName}.json`), JSON.stringify(d)); 70 | } 71 | } 72 | return failures; 73 | }; 74 | 75 | const maxFailures = getFailures(last(versionsToTest), undefined, /*writeOutput*/ true); 76 | if (maxFailures.length) { 77 | return maxFailures; 78 | } 79 | 80 | // As an optimization, check the earliest version for errors; 81 | // assume that if it works on min and max, it works for everything in between. 82 | const minFailures = getFailures(versionsToTest[0], undefined, /*writeOutput*/ false); 83 | if (!minFailures.length) { 84 | return []; 85 | } 86 | 87 | // There are no failures in the max version, but there are failures in the min version. 88 | // Work backward to find the newest version with failures. 89 | for (let i = versionsToTest.length - 2; i >= 0; i--) { 90 | const failures = getFailures(versionsToTest[i], options.versionsToTest[i + 1].versionName, /*writeOutput*/ false); 91 | if (failures.length) { 92 | return failures; 93 | } 94 | } 95 | 96 | throw new Error(); // unreachable -- at least the min version should have failures. 97 | } 98 | } 99 | 100 | export interface Options { 101 | readonly tsconfigPath: string; 102 | // These should be sorted with oldest first. 103 | readonly versionsToTest: ReadonlyArray; 104 | } 105 | export interface VersionToTest { 106 | readonly versionName: string; 107 | readonly path: string; 108 | } 109 | 110 | const programCache = new WeakMap>(); 111 | /** Maps a tslint Program to one created with the version specified in `options`. */ 112 | export function getProgram(configFile: string, ts: typeof TsType, versionName: string, lintProgram: Program): Program { 113 | let versionToProgram = programCache.get(lintProgram); 114 | if (versionToProgram === undefined) { 115 | versionToProgram = new Map(); 116 | programCache.set(lintProgram, versionToProgram); 117 | } 118 | 119 | let newProgram = versionToProgram.get(versionName); 120 | if (newProgram === undefined) { 121 | newProgram = createProgram(configFile, ts); 122 | versionToProgram.set(versionName, newProgram); 123 | } 124 | return newProgram; 125 | } 126 | 127 | function createProgram(configFile: string, ts: typeof TsType): Program { 128 | const projectDirectory = dirname(configFile); 129 | const { config } = ts.readConfigFile(configFile, ts.sys.readFile); 130 | const parseConfigHost: TsType.ParseConfigHost = { 131 | fileExists: existsSync, 132 | readDirectory: ts.sys.readDirectory, 133 | readFile: file => readFileSync(file, "utf8"), 134 | useCaseSensitiveFileNames: true, 135 | }; 136 | const parsed = ts.parseJsonConfigFileContent(config, parseConfigHost, resolvePath(projectDirectory), {noEmit: true}); 137 | const host = ts.createCompilerHost(parsed.options, true); 138 | return ts.createProgram(parsed.fileNames, parsed.options, host); 139 | } 140 | 141 | function walk( 142 | ctx: Lint.WalkContext, 143 | program: Program, 144 | ts: typeof TsType, 145 | versionName: string, 146 | nextHigherVersion: string | undefined): void { 147 | const { fileName } = ctx.sourceFile; 148 | const sourceFile = program.getSourceFile(fileName)!; 149 | if (!sourceFile) { 150 | ctx.addFailure(0, 0, 151 | `Program source files differ between TypeScript versions. This may be a dtslint bug.\n` + 152 | `Expected to find a file '${fileName}' present in ${TsType.version}, but did not find it in ts@${versionName}.`); 153 | return; 154 | } 155 | 156 | const checker = program.getTypeChecker(); 157 | // Don't care about emit errors. 158 | const diagnostics = ts.getPreEmitDiagnostics(program, sourceFile); 159 | if (sourceFile.isDeclarationFile || !/\$Expect(Type|Error)/.test(sourceFile.text)) { 160 | // Normal file. 161 | for (const diagnostic of diagnostics) { 162 | addDiagnosticFailure(diagnostic); 163 | } 164 | return; 165 | } 166 | 167 | const { errorLines, typeAssertions, duplicates } = parseAssertions(sourceFile); 168 | 169 | for (const line of duplicates) { 170 | addFailureAtLine(line, Rule.FAILURE_STRING_DUPLICATE_ASSERTION); 171 | } 172 | 173 | const seenDiagnosticsOnLine = new Set(); 174 | 175 | for (const diagnostic of diagnostics) { 176 | const line = lineOfPosition(diagnostic.start!, sourceFile); 177 | seenDiagnosticsOnLine.add(line); 178 | if (!errorLines.has(line)) { 179 | addDiagnosticFailure(diagnostic); 180 | } 181 | } 182 | 183 | for (const line of errorLines) { 184 | if (!seenDiagnosticsOnLine.has(line)) { 185 | addFailureAtLine(line, Rule.FAILURE_STRING_EXPECTED_ERROR); 186 | } 187 | } 188 | 189 | const { unmetExpectations, unusedAssertions } = getExpectTypeFailures(sourceFile, typeAssertions, checker, ts); 190 | for (const { node, expected, actual } of unmetExpectations) { 191 | ctx.addFailureAtNode(node, Rule.FAILURE_STRING(versionName, expected, actual)); 192 | } 193 | for (const line of unusedAssertions) { 194 | addFailureAtLine(line, Rule.FAILURE_STRING_ASSERTION_MISSING_NODE); 195 | } 196 | 197 | function addDiagnosticFailure(diagnostic: TsType.Diagnostic): void { 198 | const intro = getIntro(); 199 | if (diagnostic.file === sourceFile) { 200 | const msg = `${intro}\n${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`; 201 | ctx.addFailureAt(diagnostic.start!, diagnostic.length!, msg); 202 | } else { 203 | ctx.addFailureAt(0, 0, `${intro}\n${fileName}${diagnostic.messageText}`); 204 | } 205 | } 206 | 207 | function getIntro(): string { 208 | if (nextHigherVersion === undefined) { 209 | return `TypeScript@${versionName} compile error: `; 210 | } else { 211 | const msg = `Compile error in typescript@${versionName} but not in typescript@${nextHigherVersion}.\n`; 212 | const explain = nextHigherVersion === "next" 213 | ? "TypeScript@next features not yet supported." 214 | : `Fix with a comment '// Minimum TypeScript Version: ${nextHigherVersion}' just under the header.`; 215 | return msg + explain; 216 | } 217 | } 218 | 219 | function addFailureAtLine(line: number, failure: string): void { 220 | const start = sourceFile.getPositionOfLineAndCharacter(line, 0); 221 | let end = start + sourceFile.text.split("\n")[line].length; 222 | if (sourceFile.text[end - 1] === "\r") { 223 | end--; 224 | } 225 | ctx.addFailure(start, end, `TypeScript@${versionName}: ${failure}`); 226 | } 227 | } 228 | 229 | interface Assertions { 230 | /** Lines with an $ExpectError. */ 231 | readonly errorLines: ReadonlySet; 232 | /** Map from a line number to the expected type at that line. */ 233 | readonly typeAssertions: Map; 234 | /** Lines with more than one assertion (these are errors). */ 235 | readonly duplicates: ReadonlyArray; 236 | } 237 | 238 | function parseAssertions(sourceFile: SourceFile): Assertions { 239 | const errorLines = new Set(); 240 | const typeAssertions = new Map(); 241 | const duplicates: number[] = []; 242 | 243 | const { text } = sourceFile; 244 | const commentRegexp = /\/\/(.*)/g; 245 | const lineStarts = sourceFile.getLineStarts(); 246 | let curLine = 0; 247 | 248 | // eslint-disable-next-line no-constant-condition 249 | while (true) { 250 | const commentMatch = commentRegexp.exec(text); 251 | if (commentMatch === null) { 252 | break; 253 | } 254 | // Match on the contents of that comment so we do nothing in a commented-out assertion, 255 | // i.e. `// foo; // $ExpectType number` 256 | const match = /^ \$Expect((Type (.*))|Error)$/.exec(commentMatch[1]); 257 | if (match === null) { 258 | continue; 259 | } 260 | const line = getLine(commentMatch.index); 261 | if (match[1] === "Error") { 262 | if (errorLines.has(line)) { 263 | duplicates.push(line); 264 | } 265 | errorLines.add(line); 266 | } else { 267 | const expectedType = match[3]; 268 | // Don't bother with the assertion if there are 2 assertions on 1 line. Just fail for the duplicate. 269 | if (typeAssertions.delete(line)) { 270 | duplicates.push(line); 271 | } else { 272 | typeAssertions.set(line, expectedType); 273 | } 274 | } 275 | } 276 | 277 | return { errorLines, typeAssertions, duplicates }; 278 | 279 | function getLine(pos: number): number { 280 | // advance curLine to be the line preceding 'pos' 281 | while (lineStarts[curLine + 1] <= pos) { 282 | curLine++; 283 | } 284 | // If this is the first token on the line, it applies to the next line. 285 | // Otherwise, it applies to the text to the left of it. 286 | return isFirstOnLine(text, lineStarts[curLine], pos) ? curLine + 1 : curLine; 287 | } 288 | } 289 | 290 | function isFirstOnLine(text: string, lineStart: number, pos: number): boolean { 291 | for (let i = lineStart; i < pos; i++) { 292 | if (text[i] !== " ") { 293 | return false; 294 | } 295 | } 296 | return true; 297 | } 298 | 299 | interface ExpectTypeFailures { 300 | /** Lines with an $ExpectType, but a different type was there. */ 301 | readonly unmetExpectations: ReadonlyArray<{ node: TsType.Node, expected: string, actual: string }>; 302 | /** Lines with an $ExpectType, but no node could be found. */ 303 | readonly unusedAssertions: Iterable; 304 | } 305 | 306 | function matchReadonlyArray(actual: string, expected: string) { 307 | if (!(/\breadonly\b/.test(actual) && /\bReadonlyArray\b/.test(expected))) return false; 308 | const readonlyArrayRegExp = /\bReadonlyArray>>> 312 | // A[]> 313 | 314 | let expectedPos = 0; 315 | let actualPos = 0; 316 | let depth = 0; 317 | while (expectedPos < expected.length && actualPos < actual.length) { 318 | const expectedChar = expected.charAt(expectedPos); 319 | const actualChar = actual.charAt(actualPos); 320 | if (expectedChar === actualChar) { 321 | expectedPos++; 322 | actualPos++; 323 | continue; 324 | } 325 | 326 | // check for end of readonly array 327 | if (depth > 0 && expectedChar === ">" && actualChar === "[" && actualPos < actual.length - 1 && 328 | actual.charAt(actualPos + 1) === "]") { 329 | depth--; 330 | expectedPos++; 331 | actualPos += 2; 332 | continue; 333 | } 334 | 335 | // check for start of readonly array 336 | readonlyArrayRegExp.lastIndex = expectedPos; 337 | readonlyModifierRegExp.lastIndex = actualPos; 338 | if (readonlyArrayRegExp.test(expected) && readonlyModifierRegExp.test(actual)) { 339 | depth++; 340 | expectedPos += 14; // "ReadonlyArray<".length; 341 | actualPos += 9; // "readonly ".length; 342 | continue; 343 | } 344 | 345 | return false; 346 | } 347 | 348 | return true; 349 | } 350 | 351 | function getExpectTypeFailures( 352 | sourceFile: SourceFile, 353 | typeAssertions: Map, 354 | checker: TsType.TypeChecker, 355 | ts: typeof TsType, 356 | ): ExpectTypeFailures { 357 | const unmetExpectations: Array<{ node: TsType.Node, expected: string, actual: string }> = []; 358 | // Match assertions to the first node that appears on the line they apply to. 359 | // `forEachChild` isn't available as a method in older TypeScript versions, so must use `ts.forEachChild` instead. 360 | ts.forEachChild(sourceFile, function iterate(node) { 361 | const line = lineOfPosition(node.getStart(sourceFile), sourceFile); 362 | const expected = typeAssertions.get(line); 363 | if (expected !== undefined) { 364 | // https://github.com/Microsoft/TypeScript/issues/14077 365 | if (node.kind === ts.SyntaxKind.ExpressionStatement) { 366 | node = (node as TsType.ExpressionStatement).expression; 367 | } 368 | 369 | const type = checker.getTypeAtLocation(getNodeForExpectType(node, ts)); 370 | 371 | const actual = type 372 | ? checker.typeToString(type, /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.NoTruncation) 373 | : ""; 374 | 375 | if (!expected.split(/\s*\|\|\s*/).some(s => actual === s || matchReadonlyArray(actual, s))) { 376 | unmetExpectations.push({ node, expected, actual }); 377 | } 378 | 379 | typeAssertions.delete(line); 380 | } 381 | 382 | ts.forEachChild(node, iterate); 383 | }); 384 | return { unmetExpectations, unusedAssertions: typeAssertions.keys() }; 385 | } 386 | 387 | function getNodeForExpectType(node: TsType.Node, ts: typeof TsType): TsType.Node { 388 | if (node.kind === ts.SyntaxKind.VariableStatement) { // ts2.0 doesn't have `isVariableStatement` 389 | const { declarationList: { declarations } } = node as TsType.VariableStatement; 390 | if (declarations.length === 1) { 391 | const { initializer } = declarations[0]; 392 | if (initializer) { 393 | return initializer; 394 | } 395 | } 396 | } 397 | return node; 398 | } 399 | 400 | function lineOfPosition(pos: number, sourceFile: SourceFile): number { 401 | return sourceFile.getLineAndCharacterOfPosition(pos).line; 402 | } 403 | -------------------------------------------------------------------------------- /src/rules/exportJustNamespaceRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | import { failure } from "../util"; 5 | 6 | export class Rule extends Lint.Rules.AbstractRule { 7 | static metadata: Lint.IRuleMetadata = { 8 | ruleName: "export-just-namespace", 9 | description: 10 | "Forbid to `export = foo` where `foo` is a namespace and isn't merged with a function/class/type/interface.", 11 | optionsDescription: "Not configurable.", 12 | options: null, 13 | type: "functionality", 14 | typescriptOnly: true, 15 | }; 16 | 17 | static FAILURE_STRING = failure( 18 | Rule.metadata.ruleName, 19 | "Instead of `export =`-ing a namespace, use the body of the namespace as the module body."); 20 | 21 | apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 22 | return this.applyWithFunction(sourceFile, walk); 23 | } 24 | } 25 | 26 | function walk(ctx: Lint.WalkContext): void { 27 | const { sourceFile: { statements } } = ctx; 28 | const exportEqualsNode = statements.find(isExportEquals) as ts.ExportAssignment | undefined; 29 | if (!exportEqualsNode) { 30 | return; 31 | } 32 | const expr = exportEqualsNode.expression; 33 | if (!ts.isIdentifier(expr)) { 34 | return; 35 | } 36 | const exportEqualsName = expr.text; 37 | 38 | if (exportEqualsName && isJustNamespace(statements, exportEqualsName)) { 39 | ctx.addFailureAtNode(exportEqualsNode, Rule.FAILURE_STRING); 40 | } 41 | } 42 | 43 | function isExportEquals(node: ts.Node): boolean { 44 | return ts.isExportAssignment(node) && !!node.isExportEquals; 45 | } 46 | 47 | /** Returns true if there is a namespace but there are no functions/classes with the name. */ 48 | function isJustNamespace(statements: ReadonlyArray, exportEqualsName: string): boolean { 49 | let anyNamespace = false; 50 | 51 | for (const statement of statements) { 52 | switch (statement.kind) { 53 | case ts.SyntaxKind.ModuleDeclaration: 54 | anyNamespace = anyNamespace || nameMatches((statement as ts.ModuleDeclaration).name); 55 | break; 56 | case ts.SyntaxKind.VariableStatement: 57 | if ((statement as ts.VariableStatement).declarationList.declarations.some(d => nameMatches(d.name))) { 58 | // OK. It's merged with a variable. 59 | return false; 60 | } 61 | break; 62 | case ts.SyntaxKind.FunctionDeclaration: 63 | case ts.SyntaxKind.ClassDeclaration: 64 | case ts.SyntaxKind.TypeAliasDeclaration: 65 | case ts.SyntaxKind.InterfaceDeclaration: 66 | if (nameMatches((statement as ts.DeclarationStatement).name)) { 67 | // OK. It's merged with a function/class/type/interface. 68 | return false; 69 | } 70 | break; 71 | default: 72 | } 73 | } 74 | 75 | return anyNamespace; 76 | 77 | function nameMatches(nameNode: ts.Node | undefined): boolean { 78 | return nameNode !== undefined && ts.isIdentifier(nameNode) && nameNode.text === exportEqualsName; 79 | } 80 | } 81 | 82 | /* 83 | Tests: 84 | 85 | OK: 86 | export = foo; 87 | declare namespace foo {} 88 | declare function foo(): void; // or interface, type, class 89 | 90 | Error: 91 | export = foo; 92 | declare namespace foo {} 93 | 94 | OK: (it's assumed to come from elsewhere) 95 | export = foo; 96 | */ 97 | -------------------------------------------------------------------------------- /src/rules/noAnyUnionRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | import { failure } from "../util"; 5 | 6 | export class Rule extends Lint.Rules.AbstractRule { 7 | static metadata: Lint.IRuleMetadata = { 8 | ruleName: "no-any-union", 9 | description: "Forbid a union to contain `any`", 10 | optionsDescription: "Not configurable.", 11 | options: null, 12 | type: "functionality", 13 | typescriptOnly: true, 14 | }; 15 | 16 | static FAILURE_STRING = failure( 17 | Rule.metadata.ruleName, 18 | "Including `any` in a union will override all other members of the union."); 19 | 20 | apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 21 | return this.applyWithFunction(sourceFile, walk); 22 | } 23 | } 24 | 25 | function walk(ctx: Lint.WalkContext): void { 26 | ctx.sourceFile.forEachChild(function recur(node) { 27 | if (node.kind === ts.SyntaxKind.AnyKeyword && ts.isUnionTypeNode(node.parent!)) { 28 | ctx.addFailureAtNode(node, Rule.FAILURE_STRING); 29 | } 30 | node.forEachChild(recur); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/rules/noBadReferenceRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | import { failure } from "../util"; 5 | 6 | export class Rule extends Lint.Rules.AbstractRule { 7 | static metadata: Lint.IRuleMetadata = { 8 | ruleName: "no-bad-reference", 9 | description: 'Forbid in any file, and forbid in test files.', 10 | optionsDescription: "Not configurable.", 11 | options: null, 12 | type: "functionality", 13 | typescriptOnly: true, 14 | }; 15 | 16 | static FAILURE_STRING = failure( 17 | Rule.metadata.ruleName, 18 | "Don't use to reference another package. Use an import or instead."); 19 | static FAILURE_STRING_REFERENCE_IN_TEST = failure( 20 | Rule.metadata.ruleName, 21 | "Don't use in test files. Use or include the file in 'tsconfig.json'."); 22 | 23 | apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 24 | return this.applyWithFunction(sourceFile, walk); 25 | } 26 | } 27 | 28 | function walk(ctx: Lint.WalkContext): void { 29 | const { sourceFile } = ctx; 30 | for (const ref of sourceFile.referencedFiles) { 31 | if (sourceFile.isDeclarationFile) { 32 | if (ref.fileName.startsWith("..")) { 33 | ctx.addFailure(ref.pos, ref.end, Rule.FAILURE_STRING); 34 | } 35 | } else { 36 | ctx.addFailure(ref.pos, ref.end, Rule.FAILURE_STRING_REFERENCE_IN_TEST); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/rules/noConstEnumRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | import { failure } from "../util"; 5 | 6 | export class Rule extends Lint.Rules.AbstractRule { 7 | static metadata: Lint.IRuleMetadata = { 8 | ruleName: "no-const-enum", 9 | description: "Forbid `const enum`", 10 | optionsDescription: "Not configurable.", 11 | options: null, 12 | type: "functionality", 13 | typescriptOnly: true, 14 | }; 15 | 16 | static FAILURE_STRING = failure( 17 | Rule.metadata.ruleName, 18 | "Use of `const enum`s is forbidden."); 19 | 20 | apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 21 | return this.applyWithFunction(sourceFile, walk); 22 | } 23 | } 24 | 25 | function walk(ctx: Lint.WalkContext): void { 26 | ctx.sourceFile.forEachChild(function recur(node) { 27 | if (ts.isEnumDeclaration(node) && node.modifiers && node.modifiers.some(m => m.kind === ts.SyntaxKind.ConstKeyword)) { 28 | ctx.addFailureAtNode(node.name, Rule.FAILURE_STRING); 29 | } 30 | node.forEachChild(recur); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/rules/noDeadReferenceRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | import { failure } from "../util"; 5 | 6 | export class Rule extends Lint.Rules.AbstractRule { 7 | static metadata: Lint.IRuleMetadata = { 8 | ruleName: "no-dead-reference", 9 | description: "Ensures that all `/// ` comments go at the top of the file.", 10 | rationale: "style", 11 | optionsDescription: "Not configurable.", 12 | options: null, 13 | type: "functionality", 14 | typescriptOnly: true, 15 | }; 16 | 17 | static FAILURE_STRING = failure( 18 | Rule.metadata.ruleName, 19 | "`/// ` directive must be at top of file to take effect."); 20 | 21 | apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 22 | return this.applyWithFunction(sourceFile, walk); 23 | } 24 | } 25 | 26 | function walk(ctx: Lint.WalkContext): void { 27 | const { sourceFile: { statements, text } } = ctx; 28 | if (!statements.length) { 29 | return; 30 | } 31 | 32 | // 'm' flag makes it multiline, so `^` matches the beginning of any line. 33 | // 'g' flag lets us set rgx.lastIndex 34 | const rgx = /^\s*(\/\/\/ ` before that is OK.) 37 | rgx.lastIndex = statements[0].getStart(); 38 | 39 | // eslint-disable-next-line no-constant-condition 40 | while (true) { 41 | const match = rgx.exec(text); 42 | if (match === null) { 43 | break; 44 | } 45 | 46 | const length = match[1].length; 47 | const start = match.index + match[0].length - length; 48 | ctx.addFailureAt(start, length, Rule.FAILURE_STRING); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/rules/noDeclareCurrentPackageRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | import { failure, getCommonDirectoryName } from "../util"; 5 | 6 | export class Rule extends Lint.Rules.TypedRule { 7 | static metadata: Lint.IRuleMetadata = { 8 | ruleName: "no-declare-current-package", 9 | description: "Don't use an ambient module declaration of the current package; use a normal module.", 10 | optionsDescription: "Not configurable.", 11 | options: null, 12 | type: "functionality", 13 | typescriptOnly: true, 14 | }; 15 | 16 | applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { 17 | if (!sourceFile.isDeclarationFile) { 18 | return []; 19 | } 20 | 21 | const packageName = getCommonDirectoryName(program.getRootFileNames()); 22 | return this.applyWithFunction(sourceFile, ctx => walk(ctx, packageName)); 23 | } 24 | } 25 | 26 | function walk(ctx: Lint.WalkContext, packageName: string): void { 27 | for (const statement of ctx.sourceFile.statements) { 28 | if (ts.isModuleDeclaration(statement) && ts.isStringLiteral(statement.name)) { 29 | const { text } = statement.name; 30 | if (text === packageName || text.startsWith(`${packageName}/`)) { 31 | const preferred = text === packageName ? '"index.d.ts"' : `"${text}.d.ts" or "${text}/index.d.ts`; 32 | ctx.addFailureAtNode(statement.name, failure( 33 | Rule.metadata.ruleName, 34 | `Instead of declaring a module with \`declare module "${text}"\`, ` + 35 | `write its contents in directly in ${preferred}.`)); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/rules/noImportDefaultOfExportEqualsRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | import { eachModuleStatement, failure, getModuleDeclarationStatements } from "../util"; 5 | 6 | export class Rule extends Lint.Rules.TypedRule { 7 | static metadata: Lint.IRuleMetadata = { 8 | ruleName: "no-import-default-of-export-equals", 9 | description: "Forbid a default import to reference an `export =` module.", 10 | optionsDescription: "Not configurable.", 11 | options: null, 12 | type: "functionality", 13 | typescriptOnly: true, 14 | }; 15 | 16 | static FAILURE_STRING(importName: string, moduleName: string): string { 17 | return failure( 18 | Rule.metadata.ruleName, 19 | `The module ${moduleName} uses \`export = \`. Import with \`import ${importName} = require(${moduleName})\`.`); 20 | } 21 | 22 | applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { 23 | return this.applyWithFunction(sourceFile, ctx => walk(ctx, program.getTypeChecker())); 24 | } 25 | } 26 | 27 | function walk(ctx: Lint.WalkContext, checker: ts.TypeChecker): void { 28 | eachModuleStatement(ctx.sourceFile, statement => { 29 | if (!ts.isImportDeclaration(statement)) { 30 | return; 31 | } 32 | const defaultName = statement.importClause && statement.importClause.name; 33 | if (!defaultName) { 34 | return; 35 | } 36 | const sym = checker.getSymbolAtLocation(statement.moduleSpecifier); 37 | if (sym && sym.declarations && sym.declarations.some(d => { 38 | const statements = getStatements(d); 39 | return statements !== undefined && statements.some(s => ts.isExportAssignment(s) && !!s.isExportEquals); 40 | })) { 41 | ctx.addFailureAtNode(defaultName, Rule.FAILURE_STRING(defaultName.text, statement.moduleSpecifier.getText())); 42 | } 43 | }); 44 | } 45 | 46 | function getStatements(decl: ts.Declaration): ReadonlyArray | undefined { 47 | return ts.isSourceFile(decl) ? decl.statements 48 | : ts.isModuleDeclaration(decl) ? getModuleDeclarationStatements(decl) 49 | : undefined; 50 | } 51 | -------------------------------------------------------------------------------- /src/rules/noOutsideDependenciesRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | import { failure } from "../util"; 5 | 6 | export class Rule extends Lint.Rules.TypedRule { 7 | static metadata: Lint.IRuleMetadata = { 8 | ruleName: "no-outside-dependencies", 9 | description: "Don't import things in `DefinitelyTyped/node_modules`.", 10 | optionsDescription: "Not configurable.", 11 | options: null, 12 | type: "functionality", 13 | typescriptOnly: true, 14 | }; 15 | 16 | applyWithProgram(_sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { 17 | if (seenPrograms.has(program)) { 18 | return []; 19 | } 20 | seenPrograms.add(program); 21 | 22 | const failures: Lint.RuleFailure[] = []; 23 | for (const sourceFile of program.getSourceFiles()) { 24 | const { fileName } = sourceFile; 25 | if (fileName.includes("/DefinitelyTyped/node_modules/") && !program.isSourceFileDefaultLibrary(sourceFile)) { 26 | const msg = failure( 27 | Rule.metadata.ruleName, 28 | `File ${fileName} comes from a \`node_modules\` but is not declared in this type's \`package.json\`. `); 29 | failures.push(new Lint.RuleFailure(sourceFile, 0, 1, msg, Rule.metadata.ruleName)); 30 | } 31 | } 32 | return failures; 33 | } 34 | } 35 | 36 | const seenPrograms = new WeakSet(); 37 | -------------------------------------------------------------------------------- /src/rules/noPaddingRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | import { failure } from "../util"; 5 | 6 | export class Rule extends Lint.Rules.AbstractRule { 7 | static metadata: Lint.IRuleMetadata = { 8 | ruleName: "no-padding", 9 | description: "Forbids a blank line after `(` / `[` / `{`, or before `)` / `]` / `}`.", 10 | optionsDescription: "Not configurable.", 11 | options: null, 12 | type: "style", 13 | typescriptOnly: true, 14 | }; 15 | 16 | static FAILURE_STRING(kind: "before" | "after", token: ts.SyntaxKind) { 17 | return failure( 18 | Rule.metadata.ruleName, 19 | `Don't leave a blank line ${kind} '${ts.tokenToString(token)}'.`); 20 | } 21 | 22 | apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 23 | return this.applyWithFunction(sourceFile, walk); 24 | } 25 | } 26 | 27 | function walk(ctx: Lint.WalkContext): void { 28 | const { sourceFile } = ctx; 29 | 30 | function fail(kind: "before" | "after", child: ts.Node): void { 31 | ctx.addFailureAtNode(child, Rule.FAILURE_STRING(kind, child.kind)); 32 | } 33 | 34 | sourceFile.forEachChild(function cb(node) { 35 | const children = node.getChildren(); 36 | for (let i = 0; i < children.length; i++) { 37 | const child = children[i]; 38 | switch (child.kind) { 39 | case ts.SyntaxKind.OpenParenToken: 40 | case ts.SyntaxKind.OpenBracketToken: 41 | case ts.SyntaxKind.OpenBraceToken: 42 | if (i < children.length - 1 && blankLineInBetween(child.getEnd(), children[i + 1].getStart())) { 43 | fail("after", child); 44 | } 45 | break; 46 | 47 | case ts.SyntaxKind.CloseParenToken: 48 | case ts.SyntaxKind.CloseBracketToken: 49 | case ts.SyntaxKind.CloseBraceToken: 50 | if (i > 0 && blankLineInBetween(child.getStart() - 1, children[i - 1].getEnd() - 1)) { 51 | fail("before", child); 52 | } 53 | break; 54 | 55 | default: 56 | cb(child); 57 | } 58 | } 59 | }); 60 | 61 | // Looks for two newlines (with nothing else in between besides whitespace) 62 | function blankLineInBetween(start: number, end: number): boolean { 63 | const step = start < end ? 1 : -1; 64 | let seenLine = false; 65 | for (let i = start; i !== end; i += step) { 66 | switch (sourceFile.text[i]) { 67 | case "\n": 68 | if (seenLine) { 69 | return true; 70 | } else { 71 | seenLine = true; 72 | } 73 | break; 74 | 75 | case " ": case "\t": case "\r": 76 | break; 77 | 78 | default: 79 | return false; 80 | } 81 | } 82 | 83 | return false; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/rules/noRedundantJsdoc2Rule.ts: -------------------------------------------------------------------------------- 1 | // Fixes temporarily moved here until they are published by tslint. 2 | 3 | import assert = require("assert"); 4 | import * as Lint from "tslint"; 5 | import { canHaveJsDoc, getJsDoc } from "tsutils"; 6 | import * as ts from "typescript"; 7 | 8 | export class Rule extends Lint.Rules.AbstractRule { 9 | /* tslint:disable:object-literal-sort-keys */ 10 | static metadata: Lint.IRuleMetadata = { 11 | ruleName: "no-redundant-jsdoc", 12 | description: "Forbids JSDoc which duplicates TypeScript functionality.", 13 | optionsDescription: "Not configurable.", 14 | options: null, 15 | optionExamples: [true], 16 | type: "style", 17 | typescriptOnly: true, 18 | }; 19 | /* tslint:enable:object-literal-sort-keys */ 20 | 21 | static readonly FAILURE_STRING_REDUNDANT_TYPE = 22 | "Type annotation in JSDoc is redundant in TypeScript code."; 23 | static readonly FAILURE_STRING_EMPTY = 24 | "JSDoc comment is empty."; 25 | static FAILURE_STRING_REDUNDANT_TAG(tagName: string): string { 26 | return `JSDoc tag '@${tagName}' is redundant in TypeScript code.`; 27 | } 28 | static FAILURE_STRING_NO_COMMENT(tagName: string): string { 29 | return `'@${tagName}' is redundant in TypeScript code if it has no description.`; 30 | } 31 | 32 | apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 33 | return this.applyWithFunction(sourceFile, walk); 34 | } 35 | } 36 | 37 | function walk(ctx: Lint.WalkContext): void { 38 | const { sourceFile } = ctx; 39 | // Intentionally exclude EndOfFileToken: it can have JSDoc, but it is only relevant in JavaScript files 40 | return sourceFile.statements.forEach(function cb(node: ts.Node): void { 41 | if (canHaveJsDoc(node)) { 42 | for (const jd of getJsDoc(node, sourceFile)) { 43 | const { tags } = jd; 44 | if (tags === undefined || tags.length === 0) { 45 | if (jd.comment === undefined) { 46 | ctx.addFailureAtNode( 47 | jd, 48 | Rule.FAILURE_STRING_EMPTY, 49 | Lint.Replacement.deleteFromTo(jd.getStart(sourceFile), jd.getEnd())); 50 | } 51 | } else { 52 | for (const tag of tags) { 53 | checkTag(tag); 54 | } 55 | } 56 | } 57 | } 58 | return ts.forEachChild(node, cb); 59 | }); 60 | 61 | function checkTag(tag: ts.JSDocTag): void { 62 | const jsdocSeeTag = (ts.SyntaxKind as any).JSDocSeeTag || 0; 63 | const jsdocDeprecatedTag = (ts.SyntaxKind as any).JSDocDeprecatedTag || 0; 64 | switch (tag.kind) { 65 | case jsdocSeeTag: 66 | case jsdocDeprecatedTag: 67 | case ts.SyntaxKind.JSDocAuthorTag: 68 | // @deprecated and @see always have meaning 69 | break; 70 | case ts.SyntaxKind.JSDocTag: { 71 | const { tagName } = tag; 72 | const { text } = tagName; 73 | // Allow "default" in an ambient context (since you can't write an initializer in an ambient context) 74 | if (redundantTags.has(text) && !(text === "default" && isInAmbientContext(tag))) { 75 | ctx.addFailureAtNode(tagName, Rule.FAILURE_STRING_REDUNDANT_TAG(text), removeTag(tag, sourceFile)); 76 | } 77 | break; 78 | } 79 | 80 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 81 | // @ts-ignore (fallthrough) 82 | case ts.SyntaxKind.JSDocTemplateTag: 83 | if (tag.comment !== "") { 84 | break; 85 | } 86 | // falls through 87 | 88 | case ts.SyntaxKind.JSDocPublicTag: 89 | case ts.SyntaxKind.JSDocPrivateTag: 90 | case ts.SyntaxKind.JSDocProtectedTag: 91 | case ts.SyntaxKind.JSDocClassTag: 92 | case ts.SyntaxKind.JSDocTypeTag: 93 | case ts.SyntaxKind.JSDocTypedefTag: 94 | case ts.SyntaxKind.JSDocReadonlyTag: 95 | case ts.SyntaxKind.JSDocPropertyTag: 96 | case ts.SyntaxKind.JSDocAugmentsTag: 97 | case ts.SyntaxKind.JSDocImplementsTag: 98 | case ts.SyntaxKind.JSDocCallbackTag: 99 | case ts.SyntaxKind.JSDocThisTag: 100 | case ts.SyntaxKind.JSDocEnumTag: 101 | 102 | // Always redundant 103 | ctx.addFailureAtNode( 104 | tag.tagName, 105 | Rule.FAILURE_STRING_REDUNDANT_TAG(tag.tagName.text), 106 | removeTag(tag, sourceFile)); 107 | break; 108 | 109 | case ts.SyntaxKind.JSDocReturnTag: 110 | case ts.SyntaxKind.JSDocParameterTag: { 111 | const { typeExpression, comment } = tag as ts.JSDocReturnTag | ts.JSDocParameterTag; 112 | const noComment = comment === ""; 113 | if (typeExpression !== undefined) { 114 | // If noComment, we will just completely remove it in the other fix 115 | const fix = noComment ? undefined : removeTypeExpression(typeExpression, sourceFile); 116 | ctx.addFailureAtNode(typeExpression, Rule.FAILURE_STRING_REDUNDANT_TYPE, fix); 117 | } 118 | if (noComment) { 119 | // Redundant if no documentation 120 | ctx.addFailureAtNode( 121 | tag.tagName, 122 | Rule.FAILURE_STRING_NO_COMMENT(tag.tagName.text), 123 | removeTag(tag, sourceFile)); 124 | } 125 | break; 126 | } 127 | 128 | default: 129 | throw new Error(`Unexpected tag kind: ${ts.SyntaxKind[tag.kind]}`); 130 | } 131 | } 132 | } 133 | 134 | function removeTag(tag: ts.JSDocTag, sourceFile: ts.SourceFile): Lint.Replacement | undefined { 135 | const { text } = sourceFile; 136 | const jsdoc = tag.parent; 137 | if (jsdoc.kind === ts.SyntaxKind.JSDocTypeLiteral) { 138 | return undefined; 139 | } 140 | 141 | if (jsdoc.comment === undefined && jsdoc.tags!.length === 1) { 142 | // This is the only tag -- remove the whole comment 143 | return Lint.Replacement.deleteFromTo(jsdoc.getStart(sourceFile), jsdoc.getEnd()); 144 | } 145 | 146 | let start = tag.getStart(sourceFile); 147 | assert(text[start] === "@"); 148 | start--; 149 | while (ts.isWhiteSpaceSingleLine(text.charCodeAt(start))) { 150 | start--; 151 | } 152 | if (text[start] !== "*") { 153 | return undefined; 154 | } 155 | 156 | let end = tag.getEnd(); 157 | 158 | // For some tags, like `@param`, `end` will be the start of the next tag. 159 | // For some tags, like `@name`, `end` will be before the start of the comment. 160 | // And `@typedef` doesn't end until the last `@property` tag attached to it ends. 161 | switch (tag.tagName.text) { 162 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 163 | // @ts-ignore (fallthrough) 164 | case "param": { 165 | const { isBracketed, isNameFirst, typeExpression } = tag as ts.JSDocParameterTag; 166 | if (!isBracketed && !(isNameFirst && typeExpression !== undefined)) { 167 | break; 168 | } 169 | // falls through 170 | } 171 | // eslint-disable-next-line no-fallthrough 172 | case "name": 173 | case "return": 174 | case "returns": 175 | case "interface": 176 | case "default": 177 | case "memberof": 178 | case "memberOf": 179 | case "method": 180 | case "type": 181 | case "class": 182 | case "property": 183 | case "function": 184 | end--; // Might end with "\n" (test with just `@return` with no comment or type) 185 | // For some reason, for "@name", "end" is before the start of the comment part of the tag. 186 | // Also for "param" if the name is optional as in `@param {number} [x]` 187 | while (!ts.isLineBreak(text.charCodeAt(end))) { 188 | end++; 189 | } 190 | end++; 191 | } 192 | while (ts.isWhiteSpaceSingleLine(text.charCodeAt(end))) { 193 | end++; 194 | } 195 | if (text[end] !== "*") { 196 | return undefined; 197 | } 198 | 199 | return Lint.Replacement.deleteFromTo(start, end); 200 | } 201 | 202 | function removeTypeExpression( 203 | typeExpression: ts.JSDocTypeExpression, 204 | sourceFile: ts.SourceFile, 205 | ): Lint.Replacement | undefined { 206 | const start = typeExpression.getStart(sourceFile); 207 | let end = typeExpression.getEnd(); 208 | const { text } = sourceFile; 209 | if (text[start] !== "{" || text[end - 1] !== "}") { 210 | // TypeScript parser messed up -- give up 211 | return undefined; 212 | } 213 | if (ts.isWhiteSpaceSingleLine(text.charCodeAt(end))) { 214 | end++; 215 | } 216 | return Lint.Replacement.deleteFromTo(start, end); 217 | } 218 | 219 | // TODO: improve once https://github.com/Microsoft/TypeScript/pull/17831 is in 220 | function isInAmbientContext(node: ts.Node): boolean { 221 | return ts.isSourceFile(node) 222 | ? node.isDeclarationFile 223 | : Lint.hasModifier(node.modifiers, ts.SyntaxKind.DeclareKeyword) || isInAmbientContext(node.parent!); 224 | } 225 | 226 | const redundantTags = new Set([ 227 | "abstract", 228 | "access", 229 | "class", 230 | "constant", 231 | "constructs", 232 | "default", 233 | "enum", 234 | "export", 235 | "exports", 236 | "function", 237 | "global", 238 | "inherits", 239 | "interface", 240 | "instance", 241 | "member", 242 | "method", 243 | "memberof", 244 | "memberOf", 245 | "mixes", 246 | "mixin", 247 | "module", 248 | "name", 249 | "namespace", 250 | "override", 251 | "property", 252 | "requires", 253 | "static", 254 | "this", 255 | ]); 256 | -------------------------------------------------------------------------------- /src/rules/noRelativeImportInTestRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | import { failure } from "../util"; 5 | 6 | export class Rule extends Lint.Rules.TypedRule { 7 | static metadata: Lint.IRuleMetadata = { 8 | ruleName: "no-relative-import-in-test", 9 | description: "Forbids test (non-declaration) files to use relative imports.", 10 | optionsDescription: "Not configurable.", 11 | options: null, 12 | type: "functionality", 13 | typescriptOnly: false, 14 | }; 15 | 16 | applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { 17 | if (sourceFile.isDeclarationFile) { 18 | return []; 19 | } 20 | 21 | return this.applyWithFunction(sourceFile, ctx => walk(ctx, program.getTypeChecker())); 22 | } 23 | } 24 | 25 | const FAILURE_STRING = failure( 26 | Rule.metadata.ruleName, 27 | "Test file should not use a relative import. Use a global import as if this were a user of the package."); 28 | 29 | function walk(ctx: Lint.WalkContext, checker: ts.TypeChecker): void { 30 | const { sourceFile } = ctx; 31 | 32 | for (const i of sourceFile.imports) { 33 | if (i.text.startsWith(".")) { 34 | const moduleSymbol = checker.getSymbolAtLocation(i); 35 | if (!moduleSymbol || !moduleSymbol.declarations) { 36 | continue; 37 | } 38 | 39 | for (const decl of moduleSymbol.declarations) { 40 | if (decl.kind === ts.SyntaxKind.SourceFile && (decl as ts.SourceFile).isDeclarationFile) { 41 | ctx.addFailureAtNode(i, FAILURE_STRING); 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | declare module "typescript" { 49 | interface SourceFile { 50 | imports: ReadonlyArray; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/rules/noSelfImportRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | import { failure, getCommonDirectoryName } from "../util"; 5 | 6 | export class Rule extends Lint.Rules.TypedRule { 7 | static metadata: Lint.IRuleMetadata = { 8 | ruleName: "no-self-import", 9 | description: "Forbids declaration files to import the current package using a global import.", 10 | optionsDescription: "Not configurable.", 11 | options: null, 12 | type: "functionality", 13 | typescriptOnly: false, 14 | }; 15 | 16 | applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { 17 | if (!sourceFile.isDeclarationFile) { 18 | return []; 19 | } 20 | 21 | const name = getCommonDirectoryName(program.getRootFileNames()); 22 | return this.applyWithFunction(sourceFile, ctx => walk(ctx, name)); 23 | } 24 | } 25 | 26 | const FAILURE_STRING = failure( 27 | Rule.metadata.ruleName, 28 | "Declaration file should not use a global import of itself. Use a relative import."); 29 | 30 | function walk(ctx: Lint.WalkContext, packageName: string): void { 31 | for (const i of ctx.sourceFile.imports) { 32 | if (i.text === packageName || i.text.startsWith(packageName + "/")) { 33 | ctx.addFailureAtNode(i, FAILURE_STRING); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/rules/noSingleDeclareModuleRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | import { failure } from "../util"; 5 | 6 | export class Rule extends Lint.Rules.AbstractRule { 7 | static metadata: Lint.IRuleMetadata = { 8 | ruleName: "no-single-declare-module", 9 | description: "Don't use an ambient module declaration if there's just one -- write it as a normal module.", 10 | rationale: "Cuts down on nesting", 11 | optionsDescription: "Not configurable.", 12 | options: null, 13 | type: "style", 14 | typescriptOnly: true, 15 | }; 16 | 17 | static FAILURE_STRING = failure( 18 | Rule.metadata.ruleName, 19 | "File has only 1 ambient module declaration. Move the contents outside the ambient module block, rename the file to match the ambient module name, and remove the block."); 20 | 21 | apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 22 | return this.applyWithFunction(sourceFile, walk); 23 | } 24 | } 25 | 26 | function walk(ctx: Lint.WalkContext): void { 27 | const { sourceFile } = ctx; 28 | 29 | // If it's an external module, any module declarations inside are augmentations. 30 | if (ts.isExternalModule(sourceFile)) { 31 | return; 32 | } 33 | 34 | let moduleDecl: ts.ModuleDeclaration | undefined; 35 | for (const statement of sourceFile.statements) { 36 | if (ts.isModuleDeclaration(statement) && ts.isStringLiteral(statement.name)) { 37 | if (statement.name.text.indexOf('*') !== -1) { 38 | // Ignore wildcard module declarations 39 | return; 40 | } 41 | 42 | if (moduleDecl === undefined) { 43 | moduleDecl = statement; 44 | } else { 45 | // Has more than 1 declaration 46 | return; 47 | } 48 | } 49 | } 50 | 51 | if (moduleDecl) { 52 | ctx.addFailureAtNode(moduleDecl, Rule.FAILURE_STRING); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/rules/noSingleElementTupleTypeRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | import { failure } from "../util"; 5 | 6 | export class Rule extends Lint.Rules.AbstractRule { 7 | static metadata: Lint.IRuleMetadata = { 8 | ruleName: "no-single-element-tuple-type", 9 | description: "Forbids `[T]`, which should be `T[]`.", 10 | optionsDescription: "Not configurable.", 11 | options: null, 12 | type: "functionality", 13 | typescriptOnly: true, 14 | }; 15 | 16 | apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 17 | return this.applyWithFunction(sourceFile, walk); 18 | } 19 | } 20 | 21 | function walk(ctx: Lint.WalkContext): void { 22 | const { sourceFile } = ctx; 23 | sourceFile.forEachChild(function cb(node) { 24 | if (ts.isTupleTypeNode(node) && (node.elements ?? (node as any).elementTypes).length === 1) { 25 | ctx.addFailureAtNode(node, failure( 26 | Rule.metadata.ruleName, 27 | "Type [T] is a single-element tuple type. You probably meant T[].")); 28 | } 29 | node.forEachChild(cb); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /src/rules/noUnnecessaryGenericsRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | import { failure } from "../util"; 5 | 6 | export class Rule extends Lint.Rules.TypedRule { 7 | static metadata: Lint.IRuleMetadata = { 8 | ruleName: "no-unnecessary-generics", 9 | description: "Forbids signatures using a generic parameter only once.", 10 | optionsDescription: "Not configurable.", 11 | options: null, 12 | type: "style", 13 | typescriptOnly: true, 14 | }; 15 | 16 | static FAILURE_STRING(typeParameter: string) { 17 | return failure( 18 | Rule.metadata.ruleName, 19 | `Type parameter ${typeParameter} is used only once.`); 20 | } 21 | 22 | static FAILURE_STRING_NEVER(typeParameter: string) { 23 | return failure( 24 | Rule.metadata.ruleName, 25 | `Type parameter ${typeParameter} is never used.`); 26 | } 27 | 28 | applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { 29 | return this.applyWithFunction(sourceFile, ctx => walk(ctx, program.getTypeChecker())); 30 | } 31 | } 32 | 33 | function walk(ctx: Lint.WalkContext, checker: ts.TypeChecker): void { 34 | const { sourceFile } = ctx; 35 | sourceFile.forEachChild(function cb(node) { 36 | if (ts.isFunctionLike(node)) { 37 | checkSignature(node); 38 | } 39 | node.forEachChild(cb); 40 | }); 41 | 42 | function checkSignature(sig: ts.SignatureDeclaration) { 43 | if (!sig.typeParameters) { 44 | return; 45 | } 46 | 47 | for (const tp of sig.typeParameters) { 48 | const typeParameter = tp.name.text; 49 | const res = getSoleUse(sig, assertDefined(checker.getSymbolAtLocation(tp.name)), checker); 50 | switch (res.type) { 51 | case "ok": 52 | break; 53 | case "sole": 54 | ctx.addFailureAtNode(res.soleUse, Rule.FAILURE_STRING(typeParameter)); 55 | break; 56 | case "never": 57 | ctx.addFailureAtNode(tp, Rule.FAILURE_STRING_NEVER(typeParameter)); 58 | break; 59 | default: 60 | assertNever(res); 61 | } 62 | } 63 | } 64 | } 65 | 66 | type Result = 67 | | { type: "ok" | "never" } 68 | | { type: "sole", soleUse: ts.Identifier }; 69 | function getSoleUse(sig: ts.SignatureDeclaration, typeParameterSymbol: ts.Symbol, checker: ts.TypeChecker): Result { 70 | const exit = {}; 71 | let soleUse: ts.Identifier | undefined; 72 | 73 | try { 74 | if (sig.typeParameters) { 75 | for (const tp of sig.typeParameters) { 76 | if (tp.constraint) { 77 | recur(tp.constraint); 78 | } 79 | } 80 | } 81 | for (const param of sig.parameters) { 82 | if (param.type) { 83 | recur(param.type); 84 | } 85 | } 86 | if (sig.type) { 87 | recur(sig.type); 88 | } 89 | } catch (err) { 90 | if (err === exit) { 91 | return { type: "ok" }; 92 | } 93 | throw err; 94 | } 95 | 96 | return soleUse ? { type: "sole", soleUse } : { type: "never" }; 97 | 98 | function recur(node: ts.TypeNode): void { 99 | if (ts.isIdentifier(node)) { 100 | if (checker.getSymbolAtLocation(node) === typeParameterSymbol) { 101 | if (soleUse === undefined) { 102 | soleUse = node; 103 | } else { 104 | throw exit; 105 | } 106 | } 107 | } else { 108 | node.forEachChild(recur); 109 | } 110 | } 111 | } 112 | 113 | function assertDefined(value: T | undefined): T { 114 | if (value === undefined) { 115 | throw new Error("unreachable"); 116 | } 117 | return value; 118 | } 119 | function assertNever(_: never) { 120 | throw new Error("unreachable"); 121 | } 122 | -------------------------------------------------------------------------------- /src/rules/noUselessFilesRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | import { failure } from "../util"; 5 | 6 | // Same functionality as https://github.com/palantir/tslint/pull/1654, but simpler implementation. 7 | // Remove when that PR is in. 8 | 9 | export class Rule extends Lint.Rules.AbstractRule { 10 | static metadata: Lint.IRuleMetadata = { 11 | ruleName: "no-useless-files", 12 | description: "Forbids files with no content.", 13 | optionsDescription: "Not configurable.", 14 | options: null, 15 | type: "functionality", 16 | typescriptOnly: false, 17 | }; 18 | 19 | static FAILURE_STRING = failure( 20 | Rule.metadata.ruleName, 21 | "File has no content."); 22 | 23 | apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 24 | const { statements, referencedFiles, typeReferenceDirectives } = sourceFile; 25 | if (statements.length + referencedFiles.length + typeReferenceDirectives.length !== 0) { 26 | return []; 27 | } 28 | 29 | return [new Lint.RuleFailure(sourceFile, 0, 1, Rule.FAILURE_STRING, this.ruleName)]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/rules/npmNamingRule.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CheckOptions as CriticOptions, 3 | CriticError, 4 | defaultErrors, 5 | dtsCritic as critic, 6 | ErrorKind, 7 | ExportErrorKind, 8 | Mode, 9 | parseExportErrorKind, 10 | parseMode } from "dts-critic"; 11 | import * as Lint from "tslint"; 12 | import * as ts from "typescript"; 13 | 14 | import { addSuggestion } from "../suggestions"; 15 | import { failure, isMainFile } from "../util"; 16 | 17 | /** Options as parsed from the rule configuration. */ 18 | type ConfigOptions = { 19 | mode: Mode.NameOnly, 20 | singleLine?: boolean, 21 | } | { 22 | mode: Mode.Code, 23 | errors: Array<[ExportErrorKind, boolean]>, 24 | singleLine?: boolean, 25 | }; 26 | 27 | type Options = CriticOptions & { singleLine?: boolean }; 28 | 29 | const defaultOptions: ConfigOptions = { 30 | mode: Mode.NameOnly, 31 | }; 32 | 33 | export class Rule extends Lint.Rules.AbstractRule { 34 | static metadata: Lint.IRuleMetadata = { 35 | ruleName: "npm-naming", 36 | description: "Ensure that package name and DefinitelyTyped header match npm package info.", 37 | optionsDescription: `An object with a \`mode\` property should be provided. 38 | If \`mode\` is '${Mode.Code}', then option \`errors\` can be provided. 39 | \`errors\` should be an array specifying which code checks should be enabled or disabled.`, 40 | options: { 41 | oneOf: [ 42 | { 43 | type: "object", 44 | properties: { 45 | "mode": { 46 | type: "string", 47 | enum: [Mode.NameOnly], 48 | }, 49 | "single-line": { 50 | description: "Whether to print error messages in a single line. Used for testing.", 51 | type: "boolean", 52 | }, 53 | "required": ["mode"], 54 | }, 55 | }, 56 | { 57 | type: "object", 58 | properties: { 59 | "mode": { 60 | type: "string", 61 | enum: [Mode.Code], 62 | }, 63 | "errors": { 64 | type: "array", 65 | items: { 66 | type: "array", 67 | items: [ 68 | { description: "Name of the check.", 69 | type: "string", 70 | enum: [ErrorKind.NeedsExportEquals, ErrorKind.NoDefaultExport] as ExportErrorKind[], 71 | }, 72 | { 73 | description: "Whether the check is enabled or disabled.", 74 | type: "boolean", 75 | }, 76 | ], 77 | minItems: 2, 78 | maxItems: 2, 79 | }, 80 | default: [], 81 | }, 82 | "single-line": { 83 | description: "Whether to print error messages in a single line. Used for testing.", 84 | type: "boolean", 85 | }, 86 | "required": ["mode"], 87 | }, 88 | }, 89 | ], 90 | }, 91 | optionExamples: [ 92 | true, 93 | [true, { mode: Mode.NameOnly }], 94 | [ 95 | true, 96 | { 97 | mode: Mode.Code, 98 | errors: [[ErrorKind.NeedsExportEquals, true], [ErrorKind.NoDefaultExport, false]], 99 | }, 100 | ], 101 | ], 102 | type: "functionality", 103 | typescriptOnly: true, 104 | }; 105 | 106 | apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 107 | return this.applyWithFunction(sourceFile, walk, toCriticOptions(parseOptions(this.ruleArguments))); 108 | } 109 | } 110 | 111 | function parseOptions(args: unknown[]): ConfigOptions { 112 | if (args.length === 0) { 113 | return defaultOptions; 114 | } 115 | 116 | const arg = args[0] as { [prop: string]: unknown } | null | undefined; 117 | if (arg == null) { 118 | return defaultOptions; 119 | } 120 | 121 | if (!arg.mode || typeof arg.mode !== "string") { 122 | return defaultOptions; 123 | } 124 | 125 | const mode = parseMode(arg.mode); 126 | if (!mode) { 127 | return defaultOptions; 128 | } 129 | 130 | const singleLine = !!arg["single-line"]; 131 | 132 | switch (mode) { 133 | case Mode.NameOnly: 134 | return { mode, singleLine }; 135 | case Mode.Code: 136 | if (!arg.errors || !Array.isArray(arg.errors)) { 137 | return { mode, errors: [], singleLine }; 138 | } 139 | return { mode, errors: parseEnabledErrors(arg.errors), singleLine }; 140 | } 141 | } 142 | 143 | function parseEnabledErrors(errors: unknown[]): Array<[ExportErrorKind, boolean]> { 144 | const enabledChecks: Array<[ExportErrorKind, boolean]> = []; 145 | for (const tuple of errors) { 146 | if (Array.isArray(tuple) 147 | && tuple.length === 2 148 | && typeof tuple[0] === "string" 149 | && typeof tuple[1] === "boolean") { 150 | const error = parseExportErrorKind(tuple[0]); 151 | if (error) { 152 | enabledChecks.push([error, tuple[1]]); 153 | } 154 | } 155 | } 156 | return enabledChecks; 157 | } 158 | 159 | function toCriticOptions(options: ConfigOptions): Options { 160 | switch (options.mode) { 161 | case Mode.NameOnly: 162 | return options; 163 | case Mode.Code: 164 | return { ...options, errors: new Map(options.errors) }; 165 | } 166 | } 167 | 168 | function walk(ctx: Lint.WalkContext): void { 169 | const { sourceFile } = ctx; 170 | const { text } = sourceFile; 171 | const lookFor = (search: string, explanation: string) => { 172 | const idx = text.indexOf(search); 173 | if (idx !== -1) { 174 | ctx.addFailureAt(idx, search.length, failure(Rule.metadata.ruleName, explanation)); 175 | } 176 | }; 177 | if (isMainFile(sourceFile.fileName, /*allowNested*/ false)) { 178 | try { 179 | const optionsWithSuggestions = toOptionsWithSuggestions(ctx.options); 180 | const diagnostics = critic(sourceFile.fileName, /* sourcePath */ undefined, optionsWithSuggestions); 181 | const errors = filterErrors(diagnostics, ctx); 182 | for (const error of errors) { 183 | switch (error.kind) { 184 | case ErrorKind.NoMatchingNpmPackage: 185 | case ErrorKind.NoMatchingNpmVersion: 186 | case ErrorKind.NonNpmHasMatchingPackage: 187 | lookFor("// Type definitions for", errorMessage(error, ctx.options)); 188 | break; 189 | case ErrorKind.DtsPropertyNotInJs: 190 | case ErrorKind.DtsSignatureNotInJs: 191 | case ErrorKind.JsPropertyNotInDts: 192 | case ErrorKind.JsSignatureNotInDts: 193 | case ErrorKind.NeedsExportEquals: 194 | case ErrorKind.NoDefaultExport: 195 | if (error.position) { 196 | ctx.addFailureAt( 197 | error.position.start, 198 | error.position.length, 199 | failure(Rule.metadata.ruleName, errorMessage(error, ctx.options))); 200 | } else { 201 | ctx.addFailure(0, 1, failure(Rule.metadata.ruleName, errorMessage(error, ctx.options))); 202 | } 203 | break; 204 | } 205 | } 206 | } catch (e) { 207 | // We're ignoring exceptions. 208 | } 209 | } 210 | // Don't recur, we're done. 211 | } 212 | 213 | const enabledSuggestions: ExportErrorKind[] = [ 214 | ErrorKind.JsPropertyNotInDts, 215 | ErrorKind.JsSignatureNotInDts, 216 | ]; 217 | 218 | function toOptionsWithSuggestions(options: CriticOptions): CriticOptions { 219 | if (options.mode === Mode.NameOnly) { 220 | return options; 221 | } 222 | const optionsWithSuggestions = { mode: options.mode, errors: new Map(options.errors) }; 223 | enabledSuggestions.forEach(err => optionsWithSuggestions.errors.set(err, true)); 224 | return optionsWithSuggestions; 225 | } 226 | 227 | function filterErrors(diagnostics: CriticError[], ctx: Lint.WalkContext): CriticError[] { 228 | const errors: CriticError[] = []; 229 | diagnostics.forEach(diagnostic => { 230 | if (isSuggestion(diagnostic, ctx.options)) { 231 | addSuggestion(ctx, diagnostic.message, diagnostic.position?.start, diagnostic.position?.length); 232 | } else { 233 | errors.push(diagnostic); 234 | } 235 | }); 236 | return errors; 237 | } 238 | 239 | function isSuggestion(diagnostic: CriticError, options: Options): boolean { 240 | return options.mode === Mode.Code 241 | && (enabledSuggestions as ErrorKind[]).includes(diagnostic.kind) 242 | && !(options.errors as Map).get(diagnostic.kind); 243 | } 244 | 245 | function tslintDisableOption(error: ErrorKind): string { 246 | switch (error) { 247 | case ErrorKind.NoMatchingNpmPackage: 248 | case ErrorKind.NoMatchingNpmVersion: 249 | case ErrorKind.NonNpmHasMatchingPackage: 250 | return `false`; 251 | case ErrorKind.NoDefaultExport: 252 | case ErrorKind.NeedsExportEquals: 253 | case ErrorKind.JsSignatureNotInDts: 254 | case ErrorKind.JsPropertyNotInDts: 255 | case ErrorKind.DtsSignatureNotInJs: 256 | case ErrorKind.DtsPropertyNotInJs: 257 | return JSON.stringify([true, { mode: Mode.Code, errors: [[error, false]]}]); 258 | } 259 | } 260 | 261 | function errorMessage(error: CriticError, opts: Options): string { 262 | const message = error.message + 263 | `\nIf you won't fix this error now or you think this error is wrong, 264 | you can disable this check by adding the following options to your project's tslint.json file under "rules": 265 | 266 | "npm-naming": ${tslintDisableOption(error.kind)} 267 | `; 268 | if (opts.singleLine) { 269 | return message.replace(/(\r\n|\n|\r|\t)/gm, " "); 270 | } 271 | 272 | return message; 273 | } 274 | 275 | /** 276 | * Given npm-naming lint failures, returns a rule configuration that prevents such failures. 277 | */ 278 | export function disabler(failures: Lint.IRuleFailureJson[]): false | [true, ConfigOptions] { 279 | const disabledErrors = new Set(); 280 | for (const ruleFailure of failures) { 281 | if (ruleFailure.ruleName !== "npm-naming") { 282 | throw new Error(`Expected failures of rule "npm-naming", found failures of rule ${ruleFailure.ruleName}.`); 283 | } 284 | const message = ruleFailure.failure; 285 | // Name errors. 286 | if (message.includes("must have a matching npm package") 287 | || message.includes("must match a version that exists on npm") 288 | || message.includes("conflicts with the existing npm package")) { 289 | return false; 290 | } 291 | // Code errors. 292 | if (message.includes("declaration should use 'export =' syntax")) { 293 | disabledErrors.add(ErrorKind.NeedsExportEquals); 294 | } else if (message.includes("declaration specifies 'export default' but the JavaScript source \ 295 | does not mention 'default' anywhere")) { 296 | disabledErrors.add(ErrorKind.NoDefaultExport); 297 | } else { 298 | return [true, { mode: Mode.NameOnly }]; 299 | } 300 | } 301 | 302 | if ((defaultErrors as ExportErrorKind[]).every(error => disabledErrors.has(error))) { 303 | return [true, { mode: Mode.NameOnly }]; 304 | } 305 | const errors: Array<[ExportErrorKind, boolean]> = []; 306 | disabledErrors.forEach(error => errors.push([error, false])); 307 | return [true, { mode: Mode.Code, errors }]; 308 | } 309 | -------------------------------------------------------------------------------- /src/rules/preferDeclareFunctionRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | import { eachModuleStatement, failure } from "../util"; 5 | 6 | export class Rule extends Lint.Rules.AbstractRule { 7 | static metadata: Lint.IRuleMetadata = { 8 | ruleName: "prefer-declare-function", 9 | description: "Forbids `export const x = () => void`.", 10 | optionsDescription: "Not configurable.", 11 | options: null, 12 | type: "style", 13 | typescriptOnly: true, 14 | }; 15 | 16 | static FAILURE_STRING = failure( 17 | Rule.metadata.ruleName, 18 | "Use a function declaration instead of a variable of function type."); 19 | 20 | apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 21 | return this.applyWithFunction(sourceFile, walk); 22 | } 23 | } 24 | 25 | function walk(ctx: Lint.WalkContext): void { 26 | eachModuleStatement(ctx.sourceFile, statement => { 27 | if (ts.isVariableStatement(statement)) { 28 | for (const varDecl of statement.declarationList.declarations) { 29 | if (varDecl.type !== undefined && varDecl.type.kind === ts.SyntaxKind.FunctionType) { 30 | ctx.addFailureAtNode(varDecl, Rule.FAILURE_STRING); 31 | } 32 | } 33 | } 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/rules/redundantUndefinedRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | import { failure } from "../util"; 5 | 6 | export class Rule extends Lint.Rules.AbstractRule { 7 | static metadata: Lint.IRuleMetadata = { 8 | ruleName: "redundant-undefined", 9 | description: "Forbids optional parameters to include an explicit `undefined` in their type; requires it in optional properties.", 10 | optionsDescription: "Not configurable.", 11 | options: null, 12 | type: "style", 13 | typescriptOnly: true, 14 | }; 15 | 16 | apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 17 | return this.applyWithFunction(sourceFile, walk); 18 | } 19 | } 20 | 21 | function walk(ctx: Lint.WalkContext): void { 22 | if (ctx.sourceFile.fileName.includes('node_modules')) return; 23 | ctx.sourceFile.forEachChild(function recur(node) { 24 | if (node.kind === ts.SyntaxKind.UndefinedKeyword 25 | && ts.isUnionTypeNode(node.parent!) 26 | && isOptionalParameter(node.parent!.parent!)) { 27 | ctx.addFailureAtNode( 28 | node, 29 | failure( 30 | Rule.metadata.ruleName, 31 | `Parameter is optional, so no need to include \`undefined\` in the type.`)); 32 | } 33 | node.forEachChild(recur); 34 | }); 35 | } 36 | 37 | function isOptionalParameter(node: ts.Node): boolean { 38 | return node.kind === ts.SyntaxKind.Parameter 39 | && (node as ts.ParameterDeclaration).questionToken !== undefined; 40 | } 41 | -------------------------------------------------------------------------------- /src/rules/strictExportDeclareModifiersRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | import { failure } from "../util"; 5 | 6 | export class Rule extends Lint.Rules.AbstractRule { 7 | static metadata: Lint.IRuleMetadata = { 8 | ruleName: "strict-export-declare-modifiers", 9 | description: "Enforces strict rules about where the 'export' and 'declare' modifiers may appear.", 10 | optionsDescription: "Not configurable.", 11 | options: null, 12 | type: "style", 13 | typescriptOnly: true, 14 | }; 15 | 16 | apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 17 | return this.applyWithFunction(sourceFile, walk); 18 | } 19 | } 20 | 21 | function walk(ctx: Lint.WalkContext): void { 22 | const { sourceFile } = ctx; 23 | const isExternal = sourceFile.isDeclarationFile 24 | && !sourceFile.statements.some( 25 | s => s.kind === ts.SyntaxKind.ExportAssignment || 26 | s.kind === ts.SyntaxKind.ExportDeclaration && !!(s as ts.ExportDeclaration).exportClause) 27 | && ts.isExternalModule(sourceFile); 28 | 29 | for (const node of sourceFile.statements) { 30 | if (isExternal) { 31 | checkInExternalModule(node, isAutomaticExport(sourceFile)); 32 | } else { 33 | checkInOther(node, sourceFile.isDeclarationFile); 34 | } 35 | 36 | if (isModuleDeclaration(node) && (sourceFile.isDeclarationFile || isDeclare(node))) { 37 | checkModule(node); 38 | } 39 | } 40 | 41 | function checkInExternalModule(node: ts.Statement, autoExportEnabled: boolean) { 42 | // Ignore certain node kinds (these can't have 'export' or 'default' modifiers) 43 | switch (node.kind) { 44 | case ts.SyntaxKind.ImportDeclaration: 45 | case ts.SyntaxKind.ImportEqualsDeclaration: 46 | case ts.SyntaxKind.ExportDeclaration: 47 | case ts.SyntaxKind.NamespaceExportDeclaration: 48 | return; 49 | } 50 | 51 | // `declare global` and `declare module "foo"` OK. `declare namespace N` not OK, should be `export namespace`. 52 | if (!isDeclareGlobalOrExternalModuleDeclaration(node)) { 53 | if (isDeclare(node)) { 54 | fail(mod(node, ts.SyntaxKind.DeclareKeyword), "'declare' keyword is redundant here."); 55 | } 56 | if (autoExportEnabled && !isExport(node)) { 57 | fail( 58 | (node as ts.DeclarationStatement).name || node, 59 | "All declarations in this module are exported automatically. " + 60 | "Prefer to explicitly write 'export' for clarity. " + 61 | "If you have a good reason not to export this declaration, " + 62 | "add 'export {}' to the module to shut off automatic exporting."); 63 | } 64 | } 65 | } 66 | 67 | function checkInOther(node: ts.Statement, inDeclarationFile: boolean): void { 68 | // Compiler will enforce presence of 'declare' where necessary. But types do not need 'declare'. 69 | if (isDeclare(node)) { 70 | if (isExport(node) && inDeclarationFile || 71 | node.kind === ts.SyntaxKind.InterfaceDeclaration || 72 | node.kind === ts.SyntaxKind.TypeAliasDeclaration) { 73 | fail(mod(node, ts.SyntaxKind.DeclareKeyword), "'declare' keyword is redundant here."); 74 | } 75 | } 76 | } 77 | 78 | function fail(node: ts.Node, reason: string): void { 79 | ctx.addFailureAtNode(node, failure(Rule.metadata.ruleName, reason)); 80 | } 81 | 82 | function mod(node: ts.Statement, kind: ts.SyntaxKind): ts.Node { 83 | return node.modifiers!.find(m => m.kind === kind)!; 84 | } 85 | 86 | function checkModule(moduleDeclaration: ts.ModuleDeclaration): void { 87 | const body = moduleDeclaration.body; 88 | if (!body) { 89 | return; 90 | } 91 | 92 | switch (body.kind) { 93 | case ts.SyntaxKind.ModuleDeclaration: 94 | checkModule(body); 95 | break; 96 | case ts.SyntaxKind.ModuleBlock: 97 | checkBlock(body, isAutomaticExport(moduleDeclaration)); 98 | break; 99 | } 100 | } 101 | 102 | function checkBlock(block: ts.ModuleBlock, autoExportEnabled: boolean): void { 103 | for (const s of block.statements) { 104 | // Compiler will error for 'declare' here anyway, so just check for 'export'. 105 | if (isExport(s) && autoExportEnabled && !isDefault(s)) { 106 | fail(mod(s, ts.SyntaxKind.ExportKeyword), 107 | "'export' keyword is redundant here because " + 108 | "all declarations in this module are exported automatically. " + 109 | "If you have a good reason to export some declarations and not others, " + 110 | "add 'export {}' to the module to shut off automatic exporting."); 111 | } 112 | 113 | if (isModuleDeclaration(s)) { 114 | checkModule(s); 115 | } 116 | } 117 | } 118 | } 119 | 120 | function isDeclareGlobalOrExternalModuleDeclaration(node: ts.Node): boolean { 121 | return isModuleDeclaration(node) && ( 122 | node.name.kind === ts.SyntaxKind.StringLiteral || 123 | node.name.kind === ts.SyntaxKind.Identifier && node.name.text === "global"); 124 | } 125 | 126 | function isModuleDeclaration(node: ts.Node): node is ts.ModuleDeclaration { 127 | return node.kind === ts.SyntaxKind.ModuleDeclaration; 128 | } 129 | 130 | function isDeclare(node: ts.Node): boolean { 131 | return Lint.hasModifier(node.modifiers, ts.SyntaxKind.DeclareKeyword); 132 | } 133 | 134 | function isExport(node: ts.Node): boolean { 135 | return Lint.hasModifier(node.modifiers, ts.SyntaxKind.ExportKeyword); 136 | } 137 | 138 | function isDefault(node: ts.Node): boolean { 139 | return Lint.hasModifier(node.modifiers, ts.SyntaxKind.DefaultKeyword); 140 | } 141 | 142 | // tslint:disable-next-line:max-line-length 143 | // Copied from https://github.com/Microsoft/TypeScript/blob/dd9b8cab34a3e389e924d768eb656cf50656f582/src/compiler/binder.ts#L1571-L1581 144 | function hasExportDeclarations(node: ts.SourceFile | ts.ModuleDeclaration): boolean { 145 | const body = node.kind === ts.SyntaxKind.SourceFile ? node : node.body; 146 | if (body && (body.kind === ts.SyntaxKind.SourceFile || body.kind === ts.SyntaxKind.ModuleBlock)) { 147 | for (const stat of (body as ts.BlockLike).statements) { 148 | if (stat.kind === ts.SyntaxKind.ExportDeclaration || stat.kind === ts.SyntaxKind.ExportAssignment) { 149 | return true; 150 | } 151 | } 152 | } 153 | return false; 154 | } 155 | 156 | function isAutomaticExport(node: ts.SourceFile | ts.ModuleDeclaration): boolean { 157 | // We'd like to just test ts.NodeFlags.ExportContext, but we don't run the 158 | // binder, so that flag won't be set, so duplicate the logic instead. :( 159 | // 160 | // ts.NodeFlags.Ambient is @internal, but all modules that get here should 161 | // be ambient. 162 | return !hasExportDeclarations(node); 163 | } 164 | -------------------------------------------------------------------------------- /src/rules/trimFileRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | import { failure } from "../util"; 5 | 6 | export class Rule extends Lint.Rules.AbstractRule { 7 | static metadata: Lint.IRuleMetadata = { 8 | ruleName: "trim-file", 9 | description: "Forbids leading/trailing blank lines in a file. Allows file to end in '\n'.", 10 | optionsDescription: "Not configurable.", 11 | options: null, 12 | type: "style", 13 | typescriptOnly: false, 14 | }; 15 | 16 | static FAILURE_STRING_LEADING = failure(Rule.metadata.ruleName, "File should not begin with a blank line."); 17 | static FAILURE_STRING_TRAILING = failure( 18 | Rule.metadata.ruleName, 19 | "File should not end with a blank line. (Ending in one newline OK, ending in two newlines not OK.)"); 20 | 21 | apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 22 | return this.applyWithFunction(sourceFile, walk); 23 | } 24 | } 25 | 26 | function walk(ctx: Lint.WalkContext): void { 27 | const { sourceFile: { text } } = ctx; 28 | if (text.startsWith("\r") || text.startsWith("\n")) { 29 | ctx.addFailureAt(0, 0, Rule.FAILURE_STRING_LEADING); 30 | } 31 | 32 | if (text.endsWith("\n\n") || text.endsWith("\r\n\r\n")) { 33 | const start = text.endsWith("\r\n") ? text.length - 2 : text.length - 1; 34 | ctx.addFailureAt(start, 0, Rule.FAILURE_STRING_TRAILING); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/rules/voidReturnRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | import { failure } from "../util"; 5 | 6 | export class Rule extends Lint.Rules.AbstractRule { 7 | static metadata: Lint.IRuleMetadata = { 8 | ruleName: "void-return", 9 | description: "`void` may only be used as a return type.", 10 | rationale: "style", 11 | optionsDescription: "Not configurable.", 12 | options: null, 13 | type: "style", 14 | typescriptOnly: true, 15 | }; 16 | 17 | static FAILURE_STRING = failure( 18 | Rule.metadata.ruleName, 19 | "Use the `void` type for return types only. Otherwise, use `undefined`."); 20 | 21 | apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 22 | return this.applyWithFunction(sourceFile, walk); 23 | } 24 | } 25 | 26 | function walk(ctx: Lint.WalkContext): void { 27 | ctx.sourceFile.forEachChild(function cb(node) { 28 | if (node.kind === ts.SyntaxKind.VoidKeyword && !mayContainVoid(node.parent!) && !isReturnType(node)) { 29 | ctx.addFailureAtNode(node, Rule.FAILURE_STRING); 30 | } else { 31 | node.forEachChild(cb); 32 | } 33 | }); 34 | } 35 | 36 | function mayContainVoid({ kind }: ts.Node): boolean { 37 | switch (kind) { 38 | case ts.SyntaxKind.TypeReference: 39 | case ts.SyntaxKind.ExpressionWithTypeArguments: 40 | case ts.SyntaxKind.NewExpression: 41 | case ts.SyntaxKind.CallExpression: 42 | case ts.SyntaxKind.TypeParameter: // Allow f 43 | return true; 44 | default: 45 | return false; 46 | } 47 | } 48 | 49 | function isReturnType(node: ts.Node): boolean { 50 | let parent = node.parent!; 51 | if (parent.kind === ts.SyntaxKind.UnionType) { 52 | [node, parent] = [parent, parent.parent!]; 53 | } 54 | return isSignatureDeclaration(parent) && parent.type === node; 55 | } 56 | 57 | function isSignatureDeclaration(node: ts.Node): node is ts.SignatureDeclaration { 58 | switch (node.kind) { 59 | case ts.SyntaxKind.ArrowFunction: 60 | case ts.SyntaxKind.CallSignature: 61 | case ts.SyntaxKind.FunctionDeclaration: 62 | case ts.SyntaxKind.FunctionExpression: 63 | case ts.SyntaxKind.FunctionType: 64 | case ts.SyntaxKind.MethodDeclaration: 65 | case ts.SyntaxKind.MethodSignature: 66 | return true; 67 | default: 68 | return false; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/suggestions.ts: -------------------------------------------------------------------------------- 1 | import fs = require("fs"); 2 | import os = require("os"); 3 | import path = require("path"); 4 | import { WalkContext } from "tslint"; 5 | 6 | const suggestionsDir = path.join(os.homedir(), ".dts", "suggestions"); 7 | 8 | export interface Suggestion { 9 | fileName: string; 10 | ruleName: string; 11 | message: string; 12 | start?: number; 13 | width?: number; 14 | } 15 | 16 | // Packages for which suggestions were already added in this run of dtslint. 17 | const existingPackages = new Set(); 18 | 19 | /** 20 | * A rule should call this function to provide a suggestion instead of a lint failure. 21 | */ 22 | export function addSuggestion(ctx: WalkContext, message: string, start?: number, width?: number) { 23 | const suggestion: Suggestion = { 24 | fileName: ctx.sourceFile.fileName, 25 | ruleName: ctx.ruleName, 26 | message, 27 | start, 28 | width, 29 | }; 30 | 31 | const packageName = dtPackageName(ctx.sourceFile.fileName); 32 | if (!packageName) { 33 | return; 34 | } 35 | let flag = "a"; 36 | if (!existingPackages.has(packageName)) { 37 | flag = "w"; 38 | existingPackages.add(packageName); 39 | } 40 | try { 41 | if (!fs.existsSync(suggestionsDir)) { 42 | fs.mkdirSync(suggestionsDir, { recursive: true }); 43 | } 44 | fs.writeFileSync( 45 | path.join(suggestionsDir, packageName + ".txt"), 46 | flag === "a" ? "\n" + formatSuggestion(suggestion) : formatSuggestion(suggestion), 47 | { flag, encoding: "utf8" }); 48 | } catch (e) { 49 | console.log(`Could not write suggestions for package ${packageName}. ${e.message || ""}`); 50 | } 51 | } 52 | 53 | const dtPath = path.join("DefinitelyTyped", "types"); 54 | 55 | function dtPackageName(filePath: string): string | undefined { 56 | const dtIndex = filePath.indexOf(dtPath); 57 | if (dtIndex === -1) { 58 | return undefined; 59 | } 60 | const basePath = filePath.substr(dtIndex + dtPath.length); 61 | const dirs = basePath.split(path.sep).filter(dir => dir !== ""); 62 | if (dirs.length === 0) { 63 | return undefined; 64 | } 65 | const packageName = dirs[0]; 66 | // Check if this is an old version of a package. 67 | if (dirs.length > 1 && /^v\d+(\.\d+)?$/.test(dirs[1])) { 68 | return packageName + dirs[1]; 69 | } 70 | return packageName; 71 | } 72 | 73 | function formatSuggestion(suggestion: Suggestion): string { 74 | return JSON.stringify(suggestion, /*replacer*/ undefined, 0); 75 | } 76 | -------------------------------------------------------------------------------- /src/updateConfig.ts: -------------------------------------------------------------------------------- 1 | // This is a stand-alone script that updates TSLint configurations for DefinitelyTyped packages. 2 | // It runs all rules specified in `dt.json`, and updates the existing configuration for a package 3 | // by adding rule exemptions only for the rules that caused a lint failure. 4 | // For example, if a configuration specifies `"no-trailing-whitespace": false` and this rule 5 | // no longer produces an error, then it will not be disabled in the new configuration. 6 | // If you update or create a rule and now it causes new failures in DT, you can update the `dt.json` 7 | // configuration with your rule, then register a disabler function for your rule 8 | // (check `disableRules` function below), then run this script with your rule as argument. 9 | 10 | import cp = require("child_process"); 11 | import fs = require("fs"); 12 | import stringify = require("json-stable-stringify"); 13 | import path = require("path"); 14 | import { Configuration as Config, ILinterOptions, IRuleFailureJson, Linter, LintResult, RuleFailure } from "tslint"; 15 | import * as ts from "typescript"; 16 | import yargs = require("yargs"); 17 | import { isExternalDependency } from "./lint"; 18 | import { disabler as npmNamingDisabler } from "./rules/npmNamingRule"; 19 | 20 | // Rule "expect" needs TypeScript version information, which this script doesn't collect. 21 | const ignoredRules: string[] = ["expect"]; 22 | 23 | function main() { 24 | const args = yargs 25 | .usage(`\`$0 --dt=path-to-dt\` or \`$0 --package=path-to-dt-package\` 26 | 'dt.json' is used as the base tslint config for running the linter.`) 27 | .option("package", { 28 | describe: "Path of DT package.", 29 | type: "string", 30 | conflicts: "dt", 31 | }) 32 | .option("dt", { 33 | describe: "Path of local DefinitelyTyped repository.", 34 | type: "string", 35 | conflicts: "package", 36 | }) 37 | .option("rules", { 38 | describe: "Names of the rules to be updated. Leave this empty to update all rules.", 39 | type: "array", 40 | string: true, 41 | default: [] as string[], 42 | }) 43 | .check(arg => { 44 | if (!arg.package && !arg.dt) { 45 | throw new Error("You must provide either argument 'package' or 'dt'."); 46 | } 47 | const unsupportedRules = arg.rules.filter(rule => ignoredRules.includes(rule)); 48 | if (unsupportedRules.length > 0) { 49 | throw new Error(`Rules ${unsupportedRules.join(", ")} are not supported at the moment.`); 50 | } 51 | return true; 52 | }).argv; 53 | 54 | if (args.package) { 55 | updatePackage(args.package, dtConfig(args.rules)); 56 | } else if (args.dt) { 57 | updateAll(args.dt, dtConfig(args.rules)); 58 | } 59 | } 60 | 61 | const dtConfigPath = "dt.json"; 62 | 63 | function dtConfig(updatedRules: string[]): Config.IConfigurationFile { 64 | const config = Config.findConfiguration(dtConfigPath).results; 65 | if (!config) { 66 | throw new Error(`Could not load config at ${dtConfigPath}.`); 67 | } 68 | // Disable ignored or non-updated rules. 69 | for (const entry of config.rules.entries()) { 70 | const [rule, ruleOpts] = entry; 71 | if (ignoredRules.includes(rule) || (updatedRules.length > 0 && !updatedRules.includes(rule))) { 72 | ruleOpts.ruleSeverity = "off"; 73 | } 74 | } 75 | return config; 76 | } 77 | 78 | function updateAll(dtPath: string, config: Config.IConfigurationFile): void { 79 | const packages = fs.readdirSync(path.join(dtPath, "types")); 80 | for (const pkg of packages) { 81 | updatePackage(path.join(dtPath, "types", pkg), config); 82 | } 83 | } 84 | 85 | function updatePackage(pkgPath: string, baseConfig: Config.IConfigurationFile): void { 86 | installDependencies(pkgPath); 87 | const packages = walkPackageDir(pkgPath); 88 | 89 | const linterOpts: ILinterOptions = { 90 | fix: false, 91 | }; 92 | 93 | for (const pkg of packages) { 94 | const results = pkg.lint(linterOpts, baseConfig); 95 | if (results.failures.length > 0) { 96 | const disabledRules = disableRules(results.failures); 97 | const newConfig = mergeConfigRules(pkg.config(), disabledRules, baseConfig); 98 | pkg.updateConfig(newConfig); 99 | } 100 | } 101 | } 102 | 103 | function installDependencies(pkgPath: string): void { 104 | if (fs.existsSync(path.join(pkgPath, "package.json"))) { 105 | cp.execSync( 106 | "npm install --ignore-scripts --no-shrinkwrap --no-package-lock --no-bin-links", 107 | { 108 | encoding: "utf8", 109 | cwd: pkgPath, 110 | }); 111 | } 112 | } 113 | 114 | function mergeConfigRules( 115 | config: Config.RawConfigFile, 116 | newRules: Config.RawRulesConfig, 117 | baseConfig: Config.IConfigurationFile): Config.RawConfigFile { 118 | const activeRules: string[] = []; 119 | baseConfig.rules.forEach((ruleOpts, ruleName) => { 120 | if (ruleOpts.ruleSeverity !== "off") { 121 | activeRules.push(ruleName); 122 | } 123 | }); 124 | const oldRules: Config.RawRulesConfig = config.rules || {}; 125 | let newRulesConfig: Config.RawRulesConfig = {}; 126 | for (const rule of Object.keys(oldRules)) { 127 | if (activeRules.includes(rule)) { 128 | continue; 129 | } 130 | newRulesConfig[rule] = oldRules[rule]; 131 | } 132 | newRulesConfig = { ...newRulesConfig, ...newRules }; 133 | return { ...config, rules: newRulesConfig }; 134 | } 135 | 136 | /** 137 | * Represents a package from the linter's perspective. 138 | * For example, `DefinitelyTyped/types/react` and `DefinitelyTyped/types/react/v15` are different 139 | * packages. 140 | */ 141 | class LintPackage { 142 | private files: ts.SourceFile[] = []; 143 | private program: ts.Program; 144 | 145 | constructor(private rootDir: string) { 146 | this.program = Linter.createProgram(path.join(this.rootDir, "tsconfig.json")); 147 | } 148 | 149 | config(): Config.RawConfigFile { 150 | return Config.readConfigurationFile(path.join(this.rootDir, "tslint.json")); 151 | } 152 | 153 | addFile(filePath: string): void { 154 | const file = this.program.getSourceFile(filePath); 155 | if (file) { 156 | this.files.push(file); 157 | } 158 | } 159 | 160 | lint(opts: ILinterOptions, config: Config.IConfigurationFile): LintResult { 161 | const linter = new Linter(opts, this.program); 162 | for (const file of this.files) { 163 | if (ignoreFile(file, this.rootDir, this.program)) { 164 | continue; 165 | } 166 | linter.lint(file.fileName, file.text, config); 167 | } 168 | return linter.getResult(); 169 | } 170 | 171 | updateConfig(config: Config.RawConfigFile): void { 172 | fs.writeFileSync( 173 | path.join(this.rootDir, "tslint.json"), 174 | stringify(config, { space: 4 }), 175 | { encoding: "utf8", flag: "w" }); 176 | } 177 | } 178 | 179 | function ignoreFile(file: ts.SourceFile, dirPath: string, program: ts.Program): boolean { 180 | return program.isSourceFileDefaultLibrary(file) || isExternalDependency(file, path.resolve(dirPath), program); 181 | } 182 | 183 | function walkPackageDir(rootDir: string): LintPackage[] { 184 | const packages: LintPackage[] = []; 185 | 186 | function walk(curPackage: LintPackage, dir: string): void { 187 | for (const ent of fs.readdirSync(dir, { encoding: "utf8", withFileTypes: true })) { 188 | const entPath = path.join(dir, ent.name); 189 | if (ent.isFile()) { 190 | curPackage.addFile(entPath); 191 | } else if (ent.isDirectory() && ent.name !== "node_modules") { 192 | if (isVersionDir(ent.name)) { 193 | const newPackage = new LintPackage(entPath); 194 | packages.push(newPackage); 195 | walk(newPackage, entPath); 196 | } else { 197 | walk(curPackage, entPath); 198 | } 199 | } 200 | } 201 | } 202 | 203 | const lintPackage = new LintPackage(rootDir); 204 | packages.push(lintPackage); 205 | walk(lintPackage, rootDir); 206 | return packages; 207 | } 208 | 209 | /** 210 | * Returns true if directory name matches a TypeScript or package version directory. 211 | * Examples: `ts3.5`, `v11`, `v0.6` are all version names. 212 | */ 213 | function isVersionDir(dirName: string): boolean { 214 | return /^ts\d+\.\d$/.test(dirName) || /^v\d+(\.\d+)?$/.test(dirName); 215 | } 216 | 217 | type RuleOptions = boolean | unknown[]; 218 | type RuleDisabler = (failures: IRuleFailureJson[]) => RuleOptions; 219 | const defaultDisabler: RuleDisabler = () => { 220 | return false; 221 | }; 222 | 223 | function disableRules(allFailures: RuleFailure[]): Config.RawRulesConfig { 224 | const ruleToFailures: Map = new Map(); 225 | for (const failure of allFailures) { 226 | const failureJson = failure.toJson(); 227 | if (ruleToFailures.has(failureJson.ruleName)) { 228 | ruleToFailures.get(failureJson.ruleName)!.push(failureJson); 229 | } else { 230 | ruleToFailures.set(failureJson.ruleName, [failureJson]); 231 | } 232 | } 233 | 234 | const newRulesConfig: Config.RawRulesConfig = {}; 235 | ruleToFailures.forEach((failures, rule) => { 236 | if (ignoredRules.includes(rule)) { 237 | return; 238 | } 239 | const disabler = rule === "npm-naming" ? npmNamingDisabler : defaultDisabler; 240 | const opts: RuleOptions = disabler(failures); 241 | newRulesConfig[rule] = opts; 242 | }); 243 | 244 | return newRulesConfig; 245 | } 246 | 247 | if (!module.parent) { 248 | main(); 249 | } 250 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import assert = require("assert"); 2 | import { pathExists, readFile } from "fs-extra"; 3 | import { basename, dirname, join } from "path"; 4 | import stripJsonComments = require("strip-json-comments"); 5 | import * as ts from "typescript"; 6 | 7 | export async function readJson(path: string) { 8 | const text = await readFile(path, "utf-8"); 9 | return JSON.parse(stripJsonComments(text)); 10 | } 11 | 12 | export function failure(ruleName: string, s: string): string { 13 | return `${s} See: https://github.com/Microsoft/dtslint/blob/master/docs/${ruleName}.md`; 14 | } 15 | 16 | export function getCommonDirectoryName(files: ReadonlyArray): string { 17 | let minLen = 999; 18 | let minDir = ""; 19 | for (const file of files) { 20 | const dir = dirname(file); 21 | if (dir.length < minLen) { 22 | minDir = dir; 23 | minLen = dir.length; 24 | } 25 | } 26 | return basename(minDir); 27 | } 28 | 29 | export function eachModuleStatement(sourceFile: ts.SourceFile, action: (statement: ts.Statement) => void): void { 30 | if (!sourceFile.isDeclarationFile) { 31 | return; 32 | } 33 | 34 | for (const node of sourceFile.statements) { 35 | if (ts.isModuleDeclaration(node)) { 36 | const statements = getModuleDeclarationStatements(node); 37 | if (statements) { 38 | for (const statement of statements) { 39 | action(statement); 40 | } 41 | } 42 | } else { 43 | action(node); 44 | } 45 | } 46 | } 47 | 48 | export function getModuleDeclarationStatements(node: ts.ModuleDeclaration): ReadonlyArray | undefined { 49 | let { body } = node; 50 | while (body && body.kind === ts.SyntaxKind.ModuleDeclaration) { 51 | body = body.body; 52 | } 53 | return body && ts.isModuleBlock(body) ? body.statements : undefined; 54 | } 55 | 56 | export async function getCompilerOptions(dirPath: string): Promise { 57 | const tsconfigPath = join(dirPath, "tsconfig.json"); 58 | if (!await pathExists(tsconfigPath)) { 59 | throw new Error(`Need a 'tsconfig.json' file in ${dirPath}`); 60 | } 61 | return (await readJson(tsconfigPath)).compilerOptions; 62 | } 63 | 64 | export function withoutPrefix(s: string, prefix: string): string | undefined { 65 | return s.startsWith(prefix) ? s.slice(prefix.length) : undefined; 66 | } 67 | 68 | export function last(a: ReadonlyArray): T { 69 | assert(a.length !== 0); 70 | return a[a.length - 1]; 71 | } 72 | 73 | export function assertDefined(a: T | undefined): T { 74 | if (a === undefined) { throw new Error(); } 75 | return a; 76 | } 77 | 78 | export async function mapDefinedAsync( 79 | arr: Iterable, mapper: (t: T) => Promise): Promise { 80 | const out = []; 81 | for (const a of arr) { 82 | const res = await mapper(a); 83 | if (res !== undefined) { 84 | out.push(res); 85 | } 86 | } 87 | return out; 88 | } 89 | 90 | export function isMainFile(fileName: string, allowNested: boolean) { 91 | // Linter may be run with cwd of the package. We want `index.d.ts` but not `submodule/index.d.ts` to match. 92 | if (fileName === "index.d.ts") { 93 | return true; 94 | } 95 | 96 | if (basename(fileName) !== "index.d.ts") { 97 | return false; 98 | } 99 | 100 | let parent = dirname(fileName); 101 | // May be a directory for an older version, e.g. `v0`. 102 | // Note a types redirect `foo/ts3.1` should not have its own header. 103 | if (allowNested && /^v(0\.)?\d+$/.test(basename(parent))) { 104 | parent = dirname(parent); 105 | } 106 | 107 | // Allow "types/foo/index.d.ts", not "types/foo/utils/index.d.ts" 108 | return basename(dirname(parent)) === "types"; 109 | } 110 | -------------------------------------------------------------------------------- /test/dt-header/correct/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../../bin/rules"], 3 | "rules": { 4 | "dt-header": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/dt-header/correct/types/foo/bar/index.d.ts.lint: -------------------------------------------------------------------------------- 1 | // This isn't the main index 2 | -------------------------------------------------------------------------------- /test/dt-header/correct/types/foo/index.d.ts.lint: -------------------------------------------------------------------------------- 1 | // Type definitions for dt-header 2.0 2 | // Project: https://github.com/bobby-headers/dt-header 3 | // Definitions by: Jane Doe 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 | // Minimum TypeScript Version: 3.1 6 | -------------------------------------------------------------------------------- /test/dt-header/correct/types/foo/notIndex.d.ts.lint: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dtslint/1ccb5736f1e99a81db56df8b59625fd64d8bc061/test/dt-header/correct/types/foo/notIndex.d.ts.lint -------------------------------------------------------------------------------- /test/dt-header/correct/types/foo/v0.75/index.d.ts.lint: -------------------------------------------------------------------------------- 1 | // Type definitions for dt-header 0.75 2 | // Project: https://github.com/bobby-headers/dt-header 3 | // Definitions by: Jane Doe 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 | // Minimum TypeScript Version: 3.1 6 | -------------------------------------------------------------------------------- /test/dt-header/correct/types/foo/v1/index.d.ts.lint: -------------------------------------------------------------------------------- 1 | // Type definitions for dt-header 1.0 2 | // Project: https://github.com/bobby-headers/dt-header 3 | // Definitions by: Jane Doe 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 | // Minimum TypeScript Version: 3.1 6 | -------------------------------------------------------------------------------- /test/dt-header/wrong/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../../bin/rules"], 3 | "rules": { 4 | "dt-header": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/dt-header/wrong/types/bad-url-username/index.d.ts.lint: -------------------------------------------------------------------------------- 1 | // Type definitions for dt-header 1.0 2 | // Project: https://github.com/bobby-headers/dt-header 3 | // Definitions by: Jane Doe 4 | ~ [Error parsing header. Expected: /\/. See: https://github.com/Microsoft/dtslint/blob/master/docs/dt-header.md] 5 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 6 | -------------------------------------------------------------------------------- /test/dt-header/wrong/types/bad-url/index.d.ts.lint: -------------------------------------------------------------------------------- 1 | // Type definitions for dt-header 1.0 2 | // Project: https://github.com/bobby-headers/dt-header 3 | // Definitions by: Jane Doe 4 | ~ [Error parsing header. Expected: /\/. See: https://github.com/Microsoft/dtslint/blob/master/docs/dt-header.md] 5 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 6 | -------------------------------------------------------------------------------- /test/dt-header/wrong/types/default-author-name/index.d.ts.lint: -------------------------------------------------------------------------------- 1 | // Type definitions for dt-header 1.0 2 | // Project: https://github.com/not/important 3 | // Definitions by: My Self 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ [Author name should be your name, not the default. See: https://github.com/Microsoft/dtslint/blob/master/docs/dt-header.md] 5 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 6 | -------------------------------------------------------------------------------- /test/dt-header/wrong/types/foo/index.d.ts.lint: -------------------------------------------------------------------------------- 1 | // Type definitions for dt-header v1.0.3 2 | ~ [Error parsing header. Expected: foo MAJOR.MINOR (patch version not allowed). See: https://github.com/Microsoft/dtslint/blob/master/docs/dt-header.md] 3 | // Project: https://github.com/bobby-headers/dt-header 4 | // Definitions by: Jane Doe 5 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 6 | -------------------------------------------------------------------------------- /test/dt-header/wrong/types/foo/notIndex.d.ts.lint: -------------------------------------------------------------------------------- 1 | // Type definitions for 2 | ~~~~~~~~~~~~~~~~~~~~~~~ [Header should only be in `index.d.ts` of the root. See: https://github.com/Microsoft/dtslint/blob/master/docs/dt-header.md] 3 | -------------------------------------------------------------------------------- /test/expect/expectError.ts.lint: -------------------------------------------------------------------------------- 1 | const x: string = 0; // $ExpectError 2 | 3 | // Handles 2 errors on 1 line 4 | const y: string = 0 / "1"; // $ExpectError 5 | 6 | // $ExpectError 7 | const z: string = ""; 8 | ~~~~~~~~~~~~~~~~~~~~~ [TypeScript@next: Expected an error on this line, but found none.] 9 | 10 | // $ExpectError 11 | "0" / 1; // $ExpectError 12 | ~~~~~~~~~~~~~~~~~~~~~~~~ [TypeScript@next: This line has 2 $ExpectType assertions.] 13 | -------------------------------------------------------------------------------- /test/expect/expectType.ts.lint: -------------------------------------------------------------------------------- 1 | 2 | // $ExpectType xxx 3 | 4 | ~nil [TypeScript@next: Can not match a node to this assertion.] 5 | 6 | // $ExpectType number[] 7 | [1, 2]; 8 | 9 | // $ExpectType 1 10 | 1; // $ExpectType 2 11 | ~~~~~~~~~~~~~~~~~~~ [TypeScript@next: This line has 2 $ExpectType assertions.] 12 | 13 | // Do nothing for commented-out comments 14 | // f; // $ExpectType foo 15 | 16 | declare function f( 17 | one: number, 18 | two: [number, number], 19 | three: [number, number, number], 20 | four: [number, number, number, number]): number; 21 | 22 | // Test that we never truncate types. 23 | f; // $ExpectType (one: number, two: [number, number], three: [number, number, number], four: [number, number, number, number]) => number 24 | 25 | // Test that we get the type of the initializer on a variable declaration. 26 | // $ExpectType 1 27 | const x = 1; 28 | 29 | declare const ar: readonly number[]; 30 | ar; // $ExpectType ReadonlyArray -------------------------------------------------------------------------------- /test/expect/tsconfig.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/expect/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../bin/rules"], 3 | "rules": { 4 | "expect": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/export-just-namespace/ok.d.ts.lint: -------------------------------------------------------------------------------- 1 | namespace N { 2 | export const x: number; 3 | } 4 | function N(): void; 5 | export = N; 6 | -------------------------------------------------------------------------------- /test/export-just-namespace/test.d.ts.lint: -------------------------------------------------------------------------------- 1 | namespace N { 2 | export const x: number; 3 | } 4 | export = N; 5 | ~~~~~~~~~~~ [Instead of `export =`-ing a namespace, use the body of the namespace as the module body. See: https://github.com/Microsoft/dtslint/blob/master/docs/export-just-namespace.md] 6 | -------------------------------------------------------------------------------- /test/export-just-namespace/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../bin/rules"], 3 | "rules": { 4 | "export-just-namespace": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/no-any-union/test.d.ts.lint: -------------------------------------------------------------------------------- 1 | export const x: any; 2 | 3 | export const y: string | any; 4 | ~~~ [Including `any` in a union will override all other members of the union. See: https://github.com/Microsoft/dtslint/blob/master/docs/no-any-union.md] 5 | -------------------------------------------------------------------------------- /test/no-any-union/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../bin/rules"], 3 | "rules": { 4 | "no-any-union": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/no-bad-reference/decl.d.ts.lint: -------------------------------------------------------------------------------- 1 | /// 2 | ~~~~~~ [Don't use to reference another package. Use an import or instead. See: https://github.com/Microsoft/dtslint/blob/master/docs/no-bad-reference.md] 3 | 4 | /// 5 | -------------------------------------------------------------------------------- /test/no-bad-reference/test.ts.lint: -------------------------------------------------------------------------------- 1 | /// 2 | ~~~~~~ [0] 3 | 4 | /// 5 | ~~~ [0] 6 | 7 | [0]: Don't use in test files. Use or include the file in 'tsconfig.json'. See: https://github.com/Microsoft/dtslint/blob/master/docs/no-bad-reference.md 8 | -------------------------------------------------------------------------------- /test/no-bad-reference/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../bin/rules"], 3 | "rules": { 4 | "no-bad-reference": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/no-const-enum/test.ts.lint: -------------------------------------------------------------------------------- 1 | const enum E { 2 | ~ [0] 3 | } 4 | 5 | enum F {} 6 | 7 | [0]: Use of `const enum`s is forbidden. See: https://github.com/Microsoft/dtslint/blob/master/docs/no-const-enum.md 8 | -------------------------------------------------------------------------------- /test/no-const-enum/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../bin/rules"], 3 | "rules": { 4 | "no-const-enum": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/no-dead-reference/test.ts.lint: -------------------------------------------------------------------------------- 1 | /// 2 | import * as bar from "bar"; 3 | /// 4 | ~~~~~~~~~~~~~~ [0] 5 | /// 6 | ~~~~~~~~~~~~~~ [0] 7 | 8 | [0]: `/// ` directive must be at top of file to take effect. See: https://github.com/Microsoft/dtslint/blob/master/docs/no-dead-reference.md 9 | -------------------------------------------------------------------------------- /test/no-dead-reference/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../bin/rules"], 3 | "rules": { 4 | "no-dead-reference": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/no-import-default-of-export-equals/bad-ambient-modules/test.d.ts.lint: -------------------------------------------------------------------------------- 1 | declare module "a" { 2 | interface I {} 3 | export = I; 4 | } 5 | 6 | declare module "b" { 7 | import a from "a"; 8 | ~ [0] 9 | } 10 | 11 | [0]: The module "a" uses `export = `. Import with `import a = require("a")`. See: https://github.com/Microsoft/dtslint/blob/master/docs/no-import-default-of-export-equals.md 12 | -------------------------------------------------------------------------------- /test/no-import-default-of-export-equals/bad-ambient-modules/tsconfig.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/no-import-default-of-export-equals/bad-ambient-modules/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../../bin/rules"], 3 | "rules": { 4 | "no-import-default-of-export-equals": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/no-import-default-of-export-equals/bad-external-modules/a.d.ts: -------------------------------------------------------------------------------- 1 | declare function foo(): void; 2 | export = foo; -------------------------------------------------------------------------------- /test/no-import-default-of-export-equals/bad-external-modules/b.d.ts.lint: -------------------------------------------------------------------------------- 1 | import D from "./a"; 2 | ~ [0] 3 | 4 | [0]: The module "./a" uses `export = `. Import with `import D = require("./a")`. See: https://github.com/Microsoft/dtslint/blob/master/docs/no-import-default-of-export-equals.md -------------------------------------------------------------------------------- /test/no-import-default-of-export-equals/bad-external-modules/tsconfig.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/no-import-default-of-export-equals/bad-external-modules/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../../bin/rules"], 3 | "rules": { 4 | "no-import-default-of-export-equals": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/no-import-default-of-export-equals/good-ambient-modules/test.d.ts.lint: -------------------------------------------------------------------------------- 1 | declare module "a" { 2 | interface I {} 3 | export default I; 4 | } 5 | 6 | declare module "b" { 7 | import a from "a"; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /test/no-import-default-of-export-equals/good-ambient-modules/tsconfig.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/no-import-default-of-export-equals/good-ambient-modules/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../../bin/rules"], 3 | "rules": { 4 | "no-import-default-of-export-equals": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/no-padding/test.ts.lint: -------------------------------------------------------------------------------- 1 | function f() { 2 | ~ [0 % ("after '{'")] 3 | 4 | return [ 5 | ~ [0 % ("after '['")] 6 | 7 | f( 8 | ~ [0 % ("after '('")] 9 | 10 | 0 11 | 12 | ) 13 | ~ [0 % ("before ')'")] 14 | 15 | ]; 16 | ~ [0 % ("before ']'")] 17 | 18 | } 19 | ~ [0 % ("before '}'")] 20 | 21 | function f() { 22 | return [ 23 | f( 24 | 0 25 | ) 26 | ]; 27 | } 28 | 29 | [0]: Don't leave a blank line %s. See: https://github.com/Microsoft/dtslint/blob/master/docs/no-padding.md -------------------------------------------------------------------------------- /test/no-padding/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../bin/rules"], 3 | "rules": { 4 | "no-padding": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/no-redundant-jsdoc2/test.ts.lint: -------------------------------------------------------------------------------- 1 | /** @deprecated */ 2 | export const x: number; 3 | /** @see x */ 4 | export const y: number; 5 | /** @author look, nobody can remember the format for this thing */ 6 | export const z: number; 7 | /** @private */ 8 | ~~~~~~~ [JSDoc tag '@private' is redundant in TypeScript code.] 9 | export const soHidden: number; 10 | /** @protected */ 11 | ~~~~~~~~~ [JSDoc tag '@protected' is redundant in TypeScript code.] 12 | export const muchSecurity: number; 13 | /** @readonly */ 14 | ~~~~~~~~ [JSDoc tag '@readonly' is redundant in TypeScript code.] 15 | export const wow: number; 16 | /** @implements {Glorfindel} 17 | ~~~~~~~~~~ [JSDoc tag '@implements' is redundant in TypeScript code.] 18 | * narrator: it doesn't. 19 | */ 20 | class Yubatyu { 21 | } 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/no-redundant-jsdoc2/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../bin/rules"], 3 | "rules": { 4 | "no-redundant-jsdoc2": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/no-relative-import-in-test/decl.d.ts.lint: -------------------------------------------------------------------------------- 1 | import { x } from "./declarationFile.d"; 2 | -------------------------------------------------------------------------------- /test/no-relative-import-in-test/declarationFile.d.ts: -------------------------------------------------------------------------------- 1 | export const x: number; 2 | -------------------------------------------------------------------------------- /test/no-relative-import-in-test/test.ts.lint: -------------------------------------------------------------------------------- 1 | import { x } from "./declarationFile.d"; 2 | ~~~~~~~~~~~~~~~~~~~~~ [0] 3 | import { y } from "./testFile"; 4 | 5 | [0]: Test file should not use a relative import. Use a global import as if this were a user of the package. See: https://github.com/Microsoft/dtslint/blob/master/docs/no-relative-import-in-test.md 6 | -------------------------------------------------------------------------------- /test/no-relative-import-in-test/testFile.ts: -------------------------------------------------------------------------------- 1 | export const y = 0; 2 | -------------------------------------------------------------------------------- /test/no-relative-import-in-test/tsconfig.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/no-relative-import-in-test/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../bin/rules"], 3 | "rules": { 4 | "no-relative-import-in-test": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/no-single-declare-module/augmentation.d.ts.lint: -------------------------------------------------------------------------------- 1 | import x from "x"; 2 | 3 | declare module "foo" {} 4 | -------------------------------------------------------------------------------- /test/no-single-declare-module/multiple.d.ts.lint: -------------------------------------------------------------------------------- 1 | declare module "foo" {} 2 | declare module "bar" {} 3 | -------------------------------------------------------------------------------- /test/no-single-declare-module/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../bin/rules"], 3 | "rules": { 4 | "no-single-declare-module": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/no-single-declare-module/wildcard.d.ts.lint: -------------------------------------------------------------------------------- 1 | declare module "*.svg" {} 2 | -------------------------------------------------------------------------------- /test/no-single-declare-module/wrong.d.ts.lint: -------------------------------------------------------------------------------- 1 | declare module "foo" {} 2 | ~~~~~~~~~~~~~~~~~~~~~~~ [File has only 1 ambient module declaration. Move the contents outside the ambient module block, rename the file to match the ambient module name, and remove the block. See: https://github.com/Microsoft/dtslint/blob/master/docs/no-single-declare-module.md] 3 | 4 | // Other global declarations don't affect this. They should go in "declare global". 5 | interface I {} 6 | -------------------------------------------------------------------------------- /test/no-single-element-tuple-type/test.ts.lint: -------------------------------------------------------------------------------- 1 | const x: [string]; 2 | ~~~~~~~~ [0] 3 | const y: [string, number]; 4 | 5 | [0]: Type [T] is a single-element tuple type. You probably meant T[]. See: https://github.com/Microsoft/dtslint/blob/master/docs/no-single-element-tuple-type.md 6 | -------------------------------------------------------------------------------- /test/no-single-element-tuple-type/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../bin/rules"], 3 | "rules": { 4 | "no-single-element-tuple-type": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/no-unnecessary-generics/test.ts.lint: -------------------------------------------------------------------------------- 1 | interface I { 2 | (value: T): void; 3 | ~ [0] 4 | m(x: T): void; 5 | ~ [0] 6 | } 7 | 8 | class C { 9 | constructor(x: T) {} 10 | ~ [0] 11 | } 12 | 13 | type Fn = () => T; 14 | ~ [0] 15 | type Ctr = new() => T; 16 | ~ [0] 17 | 18 | function f(): T { } 19 | ~ [0] 20 | 21 | const f = function(): T {}; 22 | ~ [0] 23 | const f2 = (): T => {}; 24 | ~ [0] 25 | 26 | function f(x: { T: number }): void; 27 | ~ [Type parameter T is never used. See: https://github.com/Microsoft/dtslint/blob/master/docs/no-unnecessary-generics.md] 28 | 29 | function f(u: U): U; 30 | ~ [0] 31 | 32 | // OK: 33 | // Uses type parameter twice 34 | function foo(m: Map): void {} 35 | // `T` appears in a constraint, so it appears twice. 36 | function f(t: T, u: U): U; 37 | 38 | [0]: Type parameter T is used only once. See: https://github.com/Microsoft/dtslint/blob/master/docs/no-unnecessary-generics.md 39 | -------------------------------------------------------------------------------- /test/no-unnecessary-generics/tsconfig.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/no-unnecessary-generics/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../bin/rules"], 3 | "rules": { 4 | "no-unnecessary-generics": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/no-useless-files/ok.ts.tlint: -------------------------------------------------------------------------------- 1 | export default "I am useful"; 2 | -------------------------------------------------------------------------------- /test/no-useless-files/test.ts.lint: -------------------------------------------------------------------------------- 1 | // I am useless 2 | ~ [File has no content. See: https://github.com/Microsoft/dtslint/blob/master/docs/no-useless-files.md] 3 | -------------------------------------------------------------------------------- /test/no-useless-files/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../bin/rules"], 3 | "rules": { 4 | "no-useless-files": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/npm-naming/code/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../../bin/rules"], 3 | "rules": { 4 | "npm-naming": [true, {"mode": "code", "errors": [["NeedsExportEquals", true]], "single-line": true }] 5 | } 6 | } -------------------------------------------------------------------------------- /test/npm-naming/code/types/dts-critic/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for package dts-critic 1.0 2 | // Project: https://https://github.com/DefinitelyTyped/dts-critic 3 | // Definitions by: Jane Doe 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 | 6 | export default dtsCritic(); -------------------------------------------------------------------------------- /test/npm-naming/code/types/dts-critic/index.d.ts.lint: -------------------------------------------------------------------------------- 1 | // Type definitions for package dts-critic 1.0 2 | ~ [0] 3 | // Project: https://https://github.com/DefinitelyTyped/dts-critic 4 | // Definitions by: Jane Doe 5 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 6 | 7 | export default dtsCritic(); 8 | [0]: The declaration doesn't match the JavaScript module 'dts-critic'. Reason: The declaration should use 'export =' syntax because the JavaScript source uses 'module.exports =' syntax and 'module.exports' can be called or constructed. To learn more about 'export =' syntax, see https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require. If you won't fix this error now or you think this error is wrong, you can disable this check by adding the following options to your project's tslint.json file under "rules": "npm-naming": [true,{"mode":"code","errors":[["NeedsExportEquals",false]]}] See: https://github.com/Microsoft/dtslint/blob/master/docs/npm-naming.md -------------------------------------------------------------------------------- /test/npm-naming/name/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../../bin/rules"], 3 | "rules": { 4 | "npm-naming": [true, {"mode": "name-only", "single-line": true}] 5 | } 6 | } -------------------------------------------------------------------------------- /test/npm-naming/name/types/parseltongue/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for package parseltongue 1.0 2 | // Project: https://github.com/bobby-headers/dt-header 3 | // Definitions by: Jane Doe 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -------------------------------------------------------------------------------- /test/npm-naming/name/types/parseltongue/index.d.ts.lint: -------------------------------------------------------------------------------- 1 | // Type definitions for package parseltongue 1.0 2 | ~~~~~~~~~~~~~~~~~~~~~~~ [0] 3 | // Project: https://github.com/bobby-headers/dt-header 4 | // Definitions by: Jane Doe 5 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 6 | [0]: Declaration file must have a matching npm package. To resolve this error, either: 1. Change the name to match an npm package. 2. Add a Definitely Typed header with the first line // Type definitions for non-npm package parseltongue-browser Add -browser to the end of your name to make sure it doesn't conflict with existing npm packages. If you won't fix this error now or you think this error is wrong, you can disable this check by adding the following options to your project's tslint.json file under "rules": "npm-naming": false See: https://github.com/Microsoft/dtslint/blob/master/docs/npm-naming.md -------------------------------------------------------------------------------- /test/prefer-declare-function/test.d.ts.lint: -------------------------------------------------------------------------------- 1 | export const x: () => void; 2 | ~~~~~~~~~~~~~ [0] 3 | 4 | namespace N { 5 | const x: () => void; 6 | ~~~~~~~~~~~~~ [0] 7 | } 8 | 9 | 10 | [0]: Use a function declaration instead of a variable of function type. See: https://github.com/Microsoft/dtslint/blob/master/docs/prefer-declare-function.md 11 | -------------------------------------------------------------------------------- /test/prefer-declare-function/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../bin/rules"], 3 | "rules": { 4 | "prefer-declare-function": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/redundant-undefined/test.ts.lint: -------------------------------------------------------------------------------- 1 | function f(s?: string | undefined): void {} 2 | ~~~~~~~~~ [0] 3 | 4 | interface I { 5 | ok?: string | undefined; 6 | s?: string; 7 | almost?: number | string; 8 | } 9 | 10 | [0]: Parameter is optional, so no need to include `undefined` in the type. See: https://github.com/Microsoft/dtslint/blob/master/docs/redundant-undefined.md 11 | -------------------------------------------------------------------------------- /test/redundant-undefined/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../bin/rules"], 3 | "rules": { 4 | "redundant-undefined": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/strict-export-declare-modifiers/testAmbient.d.ts.lint: -------------------------------------------------------------------------------- 1 | declare function f(): void; 2 | 3 | declare type T = number; 4 | ~~~~~~~ [declare-redundant] 5 | 6 | declare interface I {} 7 | ~~~~~~~ [declare-redundant] 8 | 9 | declare module "m" { 10 | export function f(): void; 11 | ~~~~~~ [export-redundant] 12 | function g(): void; 13 | export default function h(); 14 | } 15 | 16 | declare module "m2" { 17 | export {}; 18 | export function f(): void; 19 | function g(): void; 20 | } 21 | 22 | 23 | [declare-redundant]: 'declare' keyword is redundant here. See: https://github.com/Microsoft/dtslint/blob/master/docs/strict-export-declare-modifiers.md 24 | [export-redundant]: 'export' keyword is redundant here because all declarations in this module are exported automatically. If you have a good reason to export some declarations and not others, add 'export {}' to the module to shut off automatic exporting. See: https://github.com/Microsoft/dtslint/blob/master/docs/strict-export-declare-modifiers.md 25 | -------------------------------------------------------------------------------- /test/strict-export-declare-modifiers/testModule.d.ts.lint: -------------------------------------------------------------------------------- 1 | import * as foo from "foo"; 2 | import foo = require("foo"); 3 | export { foo }; 4 | export { foo } from "foo"; 5 | export as namespace Foo; 6 | 7 | interface I {} 8 | 9 | export declare function f(): void; 10 | ~~~~~~~ [declare-redundant] 11 | 12 | declare function g(): void; 13 | 14 | declare namespace N {} 15 | 16 | export namespace M { 17 | export function f(): void; 18 | ~~~~~~ [export-redundant] 19 | // TS compiler warns for 'declare' here. 20 | } 21 | 22 | [declare-redundant]: 'declare' keyword is redundant here. See: https://github.com/Microsoft/dtslint/blob/master/docs/strict-export-declare-modifiers.md 23 | [export-redundant]: 'export' keyword is redundant here because all declarations in this module are exported automatically. If you have a good reason to export some declarations and not others, add 'export {}' to the module to shut off automatic exporting. See: https://github.com/Microsoft/dtslint/blob/master/docs/strict-export-declare-modifiers.md -------------------------------------------------------------------------------- /test/strict-export-declare-modifiers/testModuleAutoExport.d.ts.lint: -------------------------------------------------------------------------------- 1 | import * as foo from "foo"; 2 | import foo = require("foo"); 3 | export as namespace Foo; 4 | 5 | interface I {} 6 | ~ [export-preferred] 7 | 8 | export declare function f(): void; 9 | ~~~~~~~ [declare] 10 | 11 | declare function g(): void; 12 | ~~~~~~~ [declare] 13 | ~ [export-preferred] 14 | 15 | declare namespace N {} 16 | ~~~~~~~ [declare] 17 | ~ [export-preferred] 18 | 19 | export namespace M { 20 | export function f(): void; 21 | ~~~~~~ [export-redundant] 22 | // TS compiler warns for 'declare' here. 23 | } 24 | 25 | [declare]: 'declare' keyword is redundant here. See: https://github.com/Microsoft/dtslint/blob/master/docs/strict-export-declare-modifiers.md 26 | [export-preferred]: All declarations in this module are exported automatically. Prefer to explicitly write 'export' for clarity. If you have a good reason not to export this declaration, add 'export {}' to the module to shut off automatic exporting. See: https://github.com/Microsoft/dtslint/blob/master/docs/strict-export-declare-modifiers.md 27 | [export-redundant]: 'export' keyword is redundant here because all declarations in this module are exported automatically. If you have a good reason to export some declarations and not others, add 'export {}' to the module to shut off automatic exporting. See: https://github.com/Microsoft/dtslint/blob/master/docs/strict-export-declare-modifiers.md -------------------------------------------------------------------------------- /test/strict-export-declare-modifiers/testModuleTs.ts.lint: -------------------------------------------------------------------------------- 1 | export declare class C {} 2 | 3 | export function f() {} 4 | 5 | declare interface I {} 6 | ~~~~~~~ [declare-redundant] 7 | 8 | interface J {} 9 | 10 | namespace N { 11 | export const x: number; 12 | } 13 | 14 | declare namespace M { 15 | export const x: number; 16 | ~~~~~~ [export-redundant] 17 | } 18 | 19 | [declare-redundant]: 'declare' keyword is redundant here. See: https://github.com/Microsoft/dtslint/blob/master/docs/strict-export-declare-modifiers.md 20 | [export-redundant]: 'export' keyword is redundant here because all declarations in this module are exported automatically. If you have a good reason to export some declarations and not others, add 'export {}' to the module to shut off automatic exporting. See: https://github.com/Microsoft/dtslint/blob/master/docs/strict-export-declare-modifiers.md 21 | -------------------------------------------------------------------------------- /test/strict-export-declare-modifiers/testValues.d.ts.lint: -------------------------------------------------------------------------------- 1 | declare class Foo {} 2 | export { Foo as Bar } -------------------------------------------------------------------------------- /test/strict-export-declare-modifiers/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../bin/rules"], 3 | "rules": { 4 | "strict-export-declare-modifiers": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | const { join: joinPaths } = require("path"); 3 | const { consoleTestResultHandler, runTest } = require("tslint/lib/test"); 4 | const { existsSync, readdirSync } = require("fs"); 5 | 6 | const testDir = __dirname; 7 | 8 | const tests = readdirSync(testDir).filter(x => x !== "test.js"); 9 | 10 | for (const testName of tests) { 11 | const testDirectory = joinPaths(testDir, testName); 12 | if (existsSync(joinPaths(testDirectory, "tslint.json"))) { 13 | testSingle(testDirectory); 14 | } else { 15 | for (const subTestName of readdirSync(testDirectory)) { 16 | testSingle(joinPaths(testDirectory, subTestName)); 17 | } 18 | } 19 | } 20 | 21 | function testSingle(testDirectory) { 22 | const result = runTest(testDirectory); 23 | if (!consoleTestResultHandler(result, /*logger*/ console)) { 24 | process.exit(1); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/trim-file/test.ts.lint: -------------------------------------------------------------------------------- 1 | 2 | ~nil [File should not begin with a blank line. See: https://github.com/Microsoft/dtslint/blob/master/docs/trim-file.md] 3 | 0; 4 | 5 | ~nil [File should not end with a blank line. (Ending in one newline OK, ending in two newlines not OK.) See: https://github.com/Microsoft/dtslint/blob/master/docs/trim-file.md] 6 | -------------------------------------------------------------------------------- /test/trim-file/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../bin/rules"], 3 | "rules": { 4 | "trim-file": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/void-return/test.ts.lint: -------------------------------------------------------------------------------- 1 | function f(): void; 2 | function f(): number | void; 3 | function f(): Promise; 4 | function f(): T; 5 | function f(action: () => void): void; 6 | 7 | function f(x: void): number; 8 | ~~~~ [0] 9 | function f(x: number | void): number; 10 | ~~~~ [0] 11 | 12 | f(); 13 | 14 | [0]: Use the `void` type for return types only. Otherwise, use `undefined`. See: https://github.com/Microsoft/dtslint/blob/master/docs/void-return.md 15 | -------------------------------------------------------------------------------- /test/void-return/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["../../bin/rules"], 3 | "rules": { 4 | "void-return": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "lib": ["es2017"], 6 | "outDir": "bin", 7 | "sourceMap": true, 8 | "newLine": "lf", 9 | 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitAny": true, 12 | "noImplicitReturns": true, 13 | "noImplicitThis": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "strictNullChecks": true, 17 | "esModuleInterop": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:latest", 3 | "rules": { 4 | "arrow-parens": [true, "ban-single-arg-parens"], 5 | "indent": [true, "spaces"], 6 | "interface-name": [true, "never-prefix"], 7 | "max-line-length": [true, 130], 8 | "member-access": [true, "no-public"], 9 | "variable-name": [true, "check-format", "allow-leading-underscore"], 10 | "curly": false, 11 | 12 | "no-console": false, 13 | "no-namespace": false, 14 | "object-literal-sort-keys": false, 15 | "switch-default": false 16 | } 17 | } 18 | --------------------------------------------------------------------------------