Add dtslint/dts-critic to the monorepo (#353)

* 0.6.2 - update dependencies

* 0.7.0 - log performance data to disk

* Fix lint x(

* 0.7.1 - Update dependencies

* 0.7.2 - Update dts-critic dependency

* 0.7.3 - update dts-critic dependency

* Resolve localTs path (#229)

* 0.7.4 - call path.resolve on tsLocal path

* 0.7.5 - support TS 3.6

* 0.7.6 roll back to definitelytyped-header-parser 1.1

* 0.7.7 - support TS 3.6 again

* Actually change to 0.7.7

* Fix banned words check and update dts-critic (#236)

* Fix banned words check and update dts-critic

1. Update to dts-critic 1.2, which no longer requires project urls to
include the homepage url specified on npm.
2. Check for banned words for all packages, not just those complex
enough to have a package.json. The location of the check was wrong.

* Remove handling for project url mismatch from npm-naming

* 0.7.7: Fix banned-words check and update dts-critic

* forbid only whole-word 'download'

previously it was any substring.

* 0.7.9 - fix forbidden-word regex for 'download'

* Show TS version in ExpectType error (#237)

I didn't add a test because tslint doesn't support baselines with
newlines in them. It works locally, though.

* 0.8.0: Add TS version to ExpectType errors

* Add CODEOWNERS so PRs ping me

* 0.9 - npm-naming:require d.ts to match npm version

* Update checks.ts (#240)

Not all type definitions use `React.createElement` as the jsx pragma. I bumped into this issue when updating `storybook__preact` types as per https://github.com/DefinitelyTyped/DefinitelyTyped/pull/37275

By adding the compiler option `jsxFactory` other pragmas can be used in type definitions

* 0.9.1 - Update @types/node dependency; allow jsxFactory

* Add documentation about localTs and expectOnly option (#244)

* Add documentation about localTs and expectOnly option

* Update `--expectOnly` description

According to https://github.com/microsoft/dtslint/pull/244#discussion_r312997359

Co-Authored-By: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com>

* Remove long series of empty lines in tool output (#247)

* 0.9.2 - remove bogus newlines+better documentation

Thanks to @IvonGoncharov and @lukyth, respectively.

* Use prepublishOnly instead of made-up do-publish

* ExpectRule creates cache dir (#248)

This is important for users that always use --localTs, which means that
they never run the code that creates the cache directory as part of
installing old versions of typescript.

* 0.9.4 - stop crashing after an initial --localTs run

* 0.9.4 - Add TS3.7 support

* dt-header-parser 3.7.1

* 0.9.5 - definitelytyped-header-parser 3.7.1

* 0.9.6 - dt-header-parser 3.7.2

The 3.7 parser that actually contains 3.7!

* Point ExpectRule users to tsd in README

* Merge pull request #250 from microsoft/multipleExpectTypes

Support $ExpectType with || separating multiple choices

* 0.9.7 - ExpectType supports multiples, sep by ||

* 0.9.8 - update dts-critic dependency

* Fix typo in readme (#251) (#252)

* 0.9.9 - Update dts-critic dependency

* Add support for 3.8 via definitelytyped-header-parser

* Revert to 3.7 definitelytyped-header-parser

* really revert to 3.7

* Add npm-naming documentation

* even more documentation

* unused-dependencies (#261)

* Support TS 3.8

* Drop support for 2.0 2.7 (#264)

* Switch to supported versions of Typescript

I still need to update package-lock.json when I install the shipping
version of definitelytyped-header-parser.

* Upgrade definitelytyped-header-parser

Also fix some lint

* 2.0: stop testing on Typescript 2.0 - 2.7

* Update dts-critic to 2.2.2

* Update dts-critic to 2.2.3

* 2.0.3 - update dtslint and definitelytyped-header-parser

* Support “Minimum” in TS version line (#265)

* 2.0.4 - support "Minimum" in TS version line

* Fix regex typo (#266)

* 2.0.5 - fix Typescript version parsing regex

* Remove unused 'request' dependency (#268)

Maybe I'm missing something but I don't see why it was added in
3c57139d7e

* 2.0.6 - remove request dependency

* Update npm naming (#269)

* Update npm-naming for new dts-critic

* Remove logs

* Fix tslint errors

* Update node version

* Update docs

* fixes

* Update npm-naming for new dts-critic

* Remove logs

* Fix tslint errors

* Update node version

* Update docs

* fixes

* 3.0 - new version of dts-critic

* Support Node 10 (#272)

* Tslint updater (#274)

* Create tslint updater

* Small fixes

* Adjustments

* Fixes

* Fix jsons

* Explicitly install dependencies

* Add script explanation

* Refactor

* 3.1.0 - turn on dts-critic rules

* 3.2.0 - update definitelytyped-header-parser to TS3.9

* Add support for lint suggestions (#276)

* Support suggestions

* Refactor

* Fix lint errors

* Remove export and add comment

* Refactor

* feat: Disallow `"strict": false` (#277)

* Use TypeScriptVersion.latest instead of "next" (#279)

* Convert latest TS version to "next" on DT

dtslint installs the latest version of typescript as "next" because
that's the npm name.

Probably a better fix is to install typescript@next into a directory
with the same name as the rest, stripping off the .0-dev.20200323 part.

* Just install typescript@next into 3.9 folder

This removes the need to change the name when testing dependencies. I'm
not sure yet about when testing the main package.

* Remove "next" from TsVersion type

Instead, just use TYpeScriptVersion.latest everywhere except when
installing from npm.

* Update TS and fix new compile error

* 3.4 - only use "next" to install from npm

* fix lint

* Merge pull request #280 from microsoft/fixDosPathComparison

Fix issue with path comparison for typesVersions on Windows file systems

* Support parallel minor versions (#282)

* [package] Use latest typescript@next

* Support parallel minor versions.

* 3.4.1 - allow v0.xx packages in one more place

* 3.4.2 - update dts-critic dependency

* Update to 4.0, deprecate 2.8 (#287)

* Update to 4.0, deprecate 2.8

Also switch from definitelytyped-header-parser to
@definitelytyped/header-parser,
@definitelytyped/typescript-versions,
@definitelytyped/utils.

This brings a couple of updates, principally a new way of tracking which
TS versions have shipped under the "latest" tag vs shipped under the
"next" tag. dtslint tests *only* versions that have shipped under
"latest", plus whatever is *currently* shipping under "next". It doesn't
test any betas or RCs.

Note that the new dependencies are currently closed source. We're
waiting on the MS open source legal office to OK the new repos.

* make range handle latest correctly

* reindent tsconfig

* fix lint

* Merge pull request #289 from microsoft/bug/wait-for-installation

Ensure TypeScript is installed before starting

* Lint

* 3.5.1

* 3.5.2

* 3.6 - update dependencies to support shipped TS3.9

* 3.6.1 - update to fixed header-parser

* 3.6.2 - update dts-critic (with fixed header-parser)

* 3.6.3 - update dts-critic

Bugfixes for dts-critic.

* 3.6.4 - update dts-critic dependency

should make dtslint-runner not run out of space

* Use latest @definitelytyped packages to ensure same version as DefinitelyTyped

* 3.6.5

* Update tuple rule for new AST

* 3.6.6

* Merge pull request #292 from microsoft/disable-failing-custom-lint-rules

Disable failing custom lint rules

* 3.6.7

* Disable no-redundant-jsdoc-2 (#294)

It is also broken on typescript@next

* 3.6.8 - disable another failing lint rule

* Update and re-enable tslint rules (#296)

1. Update to dts-critic and @definitelytyped/definitions-parser that use
peerDeps for typescript.
2. Specify dts-critic@latest instead of a specific version.
3. Re-enable the disabled lint rules.

* 3.6.9 - update dependencies, re-enable lint rules

* chore(deps): Move `typescript` to `peerDependencies` (#295)

* 3.6.10 - move TS to peer dep

* 3.6.11 - update dts-critic dependency

* 3.6.12 - update dts-critic dependency

* Merge pull request #301 from microsoft/jsdoc-deprecated-support

Adds a rough approximation of deprecated support

* Merge pull request #302 from microsoft/auto_deploy

Add auto-deploy deploy step

* Check should deploy

* Use main

* Use the right name

* Fix lint, add CI

* Rename CI

* Merge pull request #303 from microsoft/deprecated_allow

Allow JSDoc deprecated anywhere

* 3.7.0 - update for TS 4.1 support

* update package-lock

* Flip typesVersions (#306)

* Flip typesVersions

1. Update @definitelytyped dependencies
2. Simplify+invert runTests

* account for Minimum TS version in ts* directories

* ❤️ lint

* maxVersion: narrow type to removed undefined from first param

* go back to latest of @definitelytyped packages

* 4.0.0 - flip order of typesVersions

Order typesVersions old-to-new, not new-to-old. Also put newest version
in the root, with older versions in ts* subdirectories.

* 4.0.1 - deprecate Typescript 3.1

* 4.0.2 - fixes from @definitelytyped/*

* Add @see tag (#310)

* Add support for @see tag

Also fix no-redundant-jsdoc2 test. It was not testing anything before.

* Move test to correctly named folder

* 4.0.3 - update @definitelytyped dependencies

* 4.0.4 - track import dependencies as well

* 4.0.5 - update @definitelytyped/* and dts-critic

Adds support for 4.2 now that 4.1 is in RC.

* 4.0.6 - Update @definitelytyped/* and dtslint

1. Ship TS 4.1
2. Deprecate TS 3.2

* Fix package when installed using pnpm (#222)

Co-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com>

* Merge pull request #319 from microsoft/tell_npm_instructions

Adds a message telling you to npm install when there's missing deps

* Merge pull request #320 from microsoft/vbump

Version bump and remove travis

* Update jsdoc tags + housekeeping + ts-ignore fix (#324)

1. Update package-lock, including dts-critic' npm@7 fixes
2. Don't forbid ts-ignore or tslint:disable inside node_modules
3. Allow or disallow a bunch of new JSDoc tags.

* 4.0.8 - upgrade no-redundant-jsdoc-2 + fix ts-ignore

* use eslint on this repo (#318)

Co-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com>

* Merge pull request #321 from OliverJAsh/patch-1

Allow `noUncheckedIndexedAccess`

* Merge pull request #317 from jablko/patch-7

Copy advice from the DT common mistakes

* Merge pull request #327 from microsoft/update_node_Types

Update node types

* update to latest @definitelytyped+dts-critic

* Update @definitelytyped+dts-critic

* Version bump to 4.1.0 to workaround npm-should-deploy-action bug

* Disable no-redundant-undefined rule (#336)

It is going to turn into redundant-undefined soon, but in the meantime I
need to turn off the rule so that I can switch DT from complying with
no-redundant-undefined to complying with redundant-undefined.

* 4.1.1 - disable no-redundant-undefined

* no-redundant-undefined: enforce `undefined` for optional parameters (#335)

* make optional properties require undefined

* rename to redundant-undefined

* add missed no-redundant-undefined references

* add fix for missing undefined

* improve fix to handle function types

* Remove checks on properties

Including undefined, or not, is now semantically meaningful when
exactOptionalPropertyTypes is true.

* 4.1.2 - turn on redundant-undefined

Now only optional parameters are required to omit undefined. Optional
properties may include or not include it.

* Allow pre-1.0 /v* subdirectories. (#338)

Does not allow arbitrary minor versions, only 0.*
Matches microsoft/DefinitelyTyped-tools#723

Fixes #333

* 4.1.3 - allow pre-1.0 /v* subdirectories

* fix(no-single-declare-module): allow single wildcard module declaration (#339)

* Remove 'external module' wording from rules (#340)

The term now is just 'module'. I improved the wording while I was here.

* 4.1.3 - Improve no-single-module rule

- Allow single *.ext modules
- Remove 'external module' wording

* 4.1.5 - update @definitelytyped + dts-critic depenendencies

Add support for TS 4.5
Remove support for TS 3.6

* 4.1.6 - update @definitelytyped + dts-critic depenendencies

* Sync README tsconfig.json with dts-gen template (#345)

* Add concurrency explanation -> FAQ (#343)

* Add concurrency explanation -> FAQ

* Update typescript-installs FAQ

* Merge pull request #346 from microsoft/weswigham-patch-1

Allow package.json files to specify imports/exports/type

* 4.2.0

* Fix typo in tsconfig.json example (#347)

* 4.2.1 - update @definitelytyped and dts-critic dependencies

* Initial commit

* Create README.md

* Initial version

* Add jest (and yargs)

* Working on npm retrieval now

* Basic checking is done

* Improve error handling on the command-line

* Update README and add DT runner

* Add check to dtsCritic. Oops!

* Some minor improvements to index.js

* Changes to DT tester

Plus more normalisation in the core critic

* Add fixer to one Definitely Typed error

It's not a very good fixer.

* Unmangle scoped package names for npm lookup

* Work with non-npm packages

* Correct error code and allow current squatters

They get an explicit check.
Also update readme to explain the new check.

* Bring tests back up to date

* Add author field to package.json

* Correct JSON format in package.json

* Add index.d.ts

* Improve error wording

* Bump package.json to 1.0.2

* Update definitelytyped-header-parser dependency

* Update/publish new npm version

* Update definitelytyped-header-parser

* Update missed tests

* Read source text

Also switch to download-file-sync, and just download files
synchronously. This simplifies the code considerably.

* Error on unmatched 'export default'

* Cleanup

* Fixes to checkNames feature

1. Update dt.js to fix errors from checkNames
2. Handle scoped names in checkNames.
3. Better predicate in checkNames.

* Create ignore list for export default

* Add geojson to list of squatters

See https://github.com/DefinitelyTyped/DefinitelyTyped/pull/33850 for
discussion on how the owners decided to allow this change.

* Print source when `export default` rule fails.

I'm pretty sure unpkg fails some of the time and I don't know what is in
stdout when it does.

* Update package.json for 1.0.5 (and lock)

* Merge pull request #4 from sandersn/do-not-fail-on-500

Do not error for 500 or 524 errors

* 1.0.6 - no error for 5xx errors

* Merge pull request #5 from wooorm/fix-trailing-comma

Remove unneeded trailing comma

* 1.0.7 - remove trailing comma

* Use header version for querying unpkg.

If there is a dt header, use the version from the header to query unpkg
instead of just using the latest version on unpkg.

* 1.0.8 - fix:query unpkg with package version from header

* Couple of fixes

1. No error if types have `export default` AND `export =`. One package
had an incorrect export default in the comments, but correct code.
2. No error if the version specified in the header doesn't correspond to
a version on NPM. This is pretty common! I thought we checked for it.

* 1.0.9 - minor fixes

* 1.1 - add support for TS 3.6

* Update package.json to 1.1.0

* Merge pull request #6 from sandersn/stop-checking-project-url

Stop checking project URL.

* 1.2 : stop requiring project url and npm homepage url to be the same

* Merge pull request #8 from sandersn/require-matching-npm-version

Require matching npm version

* 2.0 - require d.ts to match npm version

* Merge pull request #9 from orta/curl_check

Adds a curl check on launch

* 2.1 - nice error if curl doesn't exist

* Skip Rate exceeded from unpkg too

* 2.2 - skip Rate exceeded from unpkg

* 2.2.1: Update deps + add to error message

* Increase curl buffer size

Some package.jsons were larger than 1 MB. Also remove dependency on
download-file-sync, replacing with a call to execFileSync that specifies
a 50MB buffer.

* Further increase curl buffer size

Turns out 50 MB is not enough. Use 100 MB instead.

* 2.2.3 - increase curl buffer size

* 2.2.4 - update definitelytyped-header-parser

* JS to TS (#14)

* Convert JS to TS

* Configure build and test gulp tasks

* Add eslint

* Fix eslint issues

* Minor fixes

* Update docs instructions

* Add typescript as dependency

* Fix tsconfig noEmit

* Fix gulp build

* Create nodejs.yml

* Merge pull request #16 from DefinitelyTyped/add-build-task

add build task

* Add new checks (#15)

* Create exports checking script

* Minor refactor

* Add new checks to index.ts.

* Rename file

* Minor fixes

* Fix test

* Add option to format result as json

* Update readme

* Nits

* Turn off js exports property check by default

* Fixes

* More nits

* Use enum

* Fixes

* Improve error messages

* More fixes

* More fixes

* Change dtsCritic options

* Change function and error names

* Add mode option to CLI and move some things

* Update readme

* Fixes (#17)

* Fixes

* Rename toExportErrorKind

* Make ErrorKind a string enum

* Path-related fixes

* Update index.ts and remove gulp

* 3.0 - use compiler to check d.ts + better source downloads

* Silence tar warnings

* 3.0.1 - silence tar warning output

* Add engines to package.json (#18)

* Add engines to package.json

* Use node 10.17

* Delete npm sources after usage

This prevents dtslint-runner's overnight run from running out of space
on Azure pipelines

* 3.0.2 - delete npm sources after running

* switch to @definitelytyped/header-parser

* 3.1.0 - update to new header-parser from @definitelytyped

* 3.2.0 - update to 3.9 version of header-parser

* 3.2.1 - update to fixed header-parser

* update typescript to 3.9

* Merge pull request #28 from DefinitelyTyped/use-tmp-for-npm-downloads

Use tmp for npm downloads

* Merge pull request #29 from DefinitelyTyped/ignore-webpack-single-letter-properties

ignore webpack's single-letter properties

* 3.2.2 - use tmp, skip webpack properties

1. Use tmp instead of rimraf to make sure npm package downloads are deleted.
2. Do not report missing-property errors on webpack-emitted single-letter
capital variables.

* resume deleting source after a single run

* 3.2.3 - delete package source each time

It went back to running out of space -- I guess tmp dirs aren't deleted
soon enough.

* 3.2.4 - make typescript a peer dependency

* Merge pull request #31 from DefinitelyTyped/update-yargs-dep

Update yargs dependency

* 3.2.5 - update yargs dependency

* Merge pull request #32 from DefinitelyTyped/dont-squelch-tar-warnings-on-macos

Do not squelch tar warnings on macOS

* 3.2.6 - no tar warnings on macOS

* Merge pull request #34 from DefinitelyTyped/add-prepublish-script

Add prepublishOnly script

* 3.2.7 - actually update .js + add prepublish script

* 3.3.0 update to header-parser with 4.1 support

* 3.3.1: update to header-parser with 3.1 deprecated

* 3.3.2 - update @definitelytyped/header-parser dependency

* 3.3.3 - update @definitelytyped/header-parser for 4.2

* 3.3.4 - update @definitelytyped/header-parser for shipping 4.1

* Merge pull request #42 from DefinitelyTyped/check-stderr-too

Check stderr when stdout is empty

* 3.3.5 - work correctly with npm@7

* 3.3.6 - fix for npm pack on npm@7

* Update to latest header-parser

* 3.3.8 - update to latest header-parser

* 3.3.9 - update @definitelytyped/header-parser dependency

* true 3.3.9 - update @definitelytyped/header-parser

To the version that supports TS 4.5.

* 3.3.10 - update @definitelytyped/header-parser

* 3.3.11 -- update @definitelytyped/header-parser dependency

* Add dtslint and dts-critic to README

* Alter package.jsons and run 'yarn install'

* Post-package-import updates

1. Update some TS usage.
2. Depend on TS internals instead of tsutils for JSDoc.
3. Update name of dtsutil/dts-critic imports.

* Fix lint

Also improve regex for "No interfaces starting with I" so that it no
longer forbids I followed by a lowercase letter.

* Address PR comments

* remove trailing comma from dtslint/package.json

* update versions in new package.json

* fix internal dependency versions

Co-authored-by: Nicholas Jamieson <nicholas@cartant.com>
Co-authored-by: Juan J. Jimenez-Anca <cortopy@users.noreply.github.com>
Co-authored-by: Kanitkorn Sujautra <lukyth@users.noreply.github.com>
Co-authored-by: Ivan Goncharov <ivan.goncharov.ua@gmail.com>
Co-authored-by: Anders Hejlsberg <andersh@microsoft.com>
Co-authored-by: Travis Valenti <ennuuos@gmail.com>
Co-authored-by: PopGoesTheWza <32041843+PopGoesTheWza@users.noreply.github.com>
Co-authored-by: Andrew Branch <andrewbranch@users.noreply.github.com>
Co-authored-by: Gabriela Araujo Britto <gab@cin.ufpe.br>
Co-authored-by: Dominik Moritz <domoritz@gmail.com>
Co-authored-by: ExE Boss <3889017+ExE-Boss@users.noreply.github.com>
Co-authored-by: Ron Buckton <ron.buckton@microsoft.com>
Co-authored-by: Eloy Durán <eloy.de.enige@gmail.com>
Co-authored-by: Andrew Branch <andrew@wheream.io>
Co-authored-by: Orta Therox <ortam@microsoft.com>
Co-authored-by: Orta <git@orta.io>
Co-authored-by: James Garbutt <43081j@users.noreply.github.com>
Co-authored-by: Gautier Ben Aïm <48261497+GauBen@users.noreply.github.com>
Co-authored-by: Jack Bates <jack@nottheoilrig.com>
Co-authored-by: Wesley Wigham <wewigham@microsoft.com>
Co-authored-by: Andrew Luca <thendrluca@gmail.com>
Co-authored-by: Gabriela Araujo Britto <t-gaar@microsoft.com>
This commit is contained in:
Nathan Shively-Sanders
2021-12-01 07:39:46 -08:00
committed by GitHub
parent 8ed0b5b7ed
commit dae5c76848
185 changed files with 8872 additions and 120 deletions

View File

@@ -23,7 +23,7 @@
{
"selector": "interface",
"format": ["PascalCase"],
"custom": { "regex": "^I", "match": false } },
"custom": { "regex": "^I[A-Z0-9]", "match": false } },
{
"selector": "class",
"format": ["PascalCase"]

View File

@@ -3,7 +3,9 @@
A monorepo for formerly disparate DefinitelyTyped-related tools:
- [definitions-parser](packages/definitions-parser): the part of [microsoft/types-publisher](https://github.com/microsoft/types-publisher) that reads DefinitelyTyped repository data
- [dtslint](packages/dtslint): [microsoft/dtslint](https://github.com/microsoft/dtslint)
- [dtslint-runner](packages/dtslint-runner): [DefinitelyTyped/dtslint-runner](https://github.com/DefinitelyTyped/dtslint-runner)
- [dts-critic](packages/dts-critic): [DefinitelyTyped/dts-critic](https://github.com/DefinitelyTyped/dts-critic)
- [header-parser](packages/header-parser): [microsoft/definitelytyped-header-parser](https://github.com/microsoft/definitelytyped-header-parser)
- [perf](packages/perf): [andrewbranch/definitely-not-slow](https://github.com/andrewbranch/definitely-not-slow)
- [publisher](packages/publisher): the rest of [microsoft/types-publisher](https://github.com/microsoft/types-publisher)

69
packages/dts-critic/.gitignore vendored Normal file
View File

@@ -0,0 +1,69 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
.vscode
# TypeScript compiler output
dist/
# Files downloaded during development
sources/

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Nathan Shively-Sanders
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,88 @@
# dts-critic
Checks a new dts against the Javascript sources and tells you what
problems it has.
# Usage
Build the program:
```sh
$ npm run build
```
Run the program using node:
```sh
$ node dist/index.js --dts=path-to-d.ts [--js=path-to-source] [--mode=mode] [--debug]
```
If the d.ts path is to a file named `index.d.ts`, the name of the directory
will be used as the package name instead. For example
`~/dt/types/jquery/index.d.ts` will use `jquery` as the name.
`path-to-source` is optional; if you leave it off, the code will
check npm for a package with the same name as the d.ts.
## Mode
You can run dts-critic in different modes that affect which checks will be performed:
1. `name-only`: dts-critic will check your package name and [DefinitelyTyped header]
(https://github.com/Microsoft/definitelytyped-header-parser) (if present) against npm packages.
For example, if your declaration is for an npm package called 'cool-js-package', it will check if a
package named 'cool-js-package' actually exists in npm.
2. `code`: in addition to the checks performed in `name-only` mode, dts-critic will check if your
declaration exports match the source JavaScript module exports.
For example, if your declaration has a default export, it will check if the JavaScript module also
has a default export.
# Current checks
## Npm declaration
If your declaration is for an npm package:
1. An npm package with the same name of your declaration's package must exist.
2. If your declaration has a [Definitely Typed header](https://github.com/Microsoft/definitelytyped-header-parser)
and the header specifies a target version, the npm package must have
a matching version.
3. If you are running under `code` mode, your declaration must also match the source JavaScript module.
## Non-npm declaration
<!-- 2. If no local path to source is provided, an npm package with the
same name as the d.ts must exist. -->
If your declaration is for a non-npm package (in other words, if your declaration has a
[Definitely Typed header](https://github.com/Microsoft/definitelytyped-header-parser) *and*
the header specifies that the declaration file is for a non-npm package):
1. An npm package with the same name of your declaration's package **cannot** exist.
3. If you are running under `code` mode *and* a path to the JavaScript source file was provided, your
declaration must also match the source JavaScript module.
# Planned work
1. Make sure your module structure fits the source.
2. Make sure your exported symbols match the source.
3. Make sure your types match the source types???
6. Download source based on npm homepage (if it is github).
Note that for real use on Definitely Typed, a lot of these checks need to be pretty loose.
# Also
```sh
$ node dist/dt.js
```
Will run dts-critic on every directory inside `../DefinitelyTyped` and
print errors.
# Contributing
## Testing
The tests use the [Jest](https://jestjs.io/) framework. To build and execute the tests, run:
```sh
$ npm run test
```
This will build the program and run jest.

View File

@@ -0,0 +1,412 @@
import fs = require("fs");
import yargs = require("yargs");
import headerParser = require("@definitelytyped/header-parser");
import path = require("path");
import cp = require("child_process");
import {
dtsCritic,
dtToNpmName,
getNpmInfo,
parseExportErrorKind,
CriticError,
ExportErrorKind,
Mode,
checkSource,
findDtsName,
CheckOptions,
parseMode} from "./index";
const sourcesDir = "sources";
const downloadsPath = path.join(sourcesDir, "dts-critic-internal/downloads.json");
const isNpmPath = path.join(sourcesDir, "dts-critic-internal/npm.json");
function getPackageDownloads(dtName: string): number {
const npmName = dtToNpmName(dtName);
const url = `https://api.npmjs.org/downloads/point/last-month/${npmName}`;
const result = JSON.parse(
cp.execFileSync(
"curl",
["--silent", "-L", url],
{ encoding: "utf8" })) as { downloads?: number };
return result.downloads || 0;
}
interface DownloadsJson { [key: string]: number | undefined }
function getAllPackageDownloads(dtPath: string): DownloadsJson {
if (fs.existsSync(downloadsPath)) {
return JSON.parse(fs.readFileSync(downloadsPath, { encoding: "utf8" })) as DownloadsJson;
}
initDir(path.dirname(downloadsPath));
const downloads: DownloadsJson = {};
const dtTypesPath = getDtTypesPath(dtPath);
for (const item of fs.readdirSync(dtTypesPath)) {
const d = getPackageDownloads(item);
downloads[item] = d;
}
fs.writeFileSync(downloadsPath, JSON.stringify(downloads), { encoding: "utf8" });
return downloads;
}
function initDir(path: string): void {
if (!fs.existsSync(path)) {
fs.mkdirSync(path);
}
}
function getDtTypesPath(dtBasePath: string): string {
return path.join(dtBasePath, "types");
}
function compareDownloads(downloads: DownloadsJson, package1: string, package2: string): number {
const count1 = downloads[package1] || 0;
const count2 = downloads[package2] || 0;
return count1 - count2;
}
interface IsNpmJson { [key: string]: boolean | undefined }
function getAllIsNpm(dtPath: string): IsNpmJson {
if (fs.existsSync(isNpmPath)) {
return JSON.parse(fs.readFileSync(isNpmPath, { encoding: "utf8" })) as IsNpmJson;
}
initDir(path.dirname(isNpmPath));
const isNpm: IsNpmJson = {};
const dtTypesPath = getDtTypesPath(dtPath);
for (const item of fs.readdirSync(dtTypesPath)) {
isNpm[item] = getNpmInfo(item).isNpm;
}
fs.writeFileSync(isNpmPath, JSON.stringify(isNpm), { encoding: "utf8" });
return isNpm;
}
function getPopularNpmPackages(count: number, dtPath: string): string[] {
const dtPackages = getDtNpmPackages(dtPath);
const downloads = getAllPackageDownloads(dtPath);
dtPackages.sort((a, b) => compareDownloads(downloads, a, b));
return dtPackages.slice(dtPackages.length - count);
}
function getUnpopularNpmPackages(count: number, dtPath: string): string[] {
const dtPackages = getDtNpmPackages(dtPath);
const downloads = getAllPackageDownloads(dtPath);
dtPackages.sort((a, b) => compareDownloads(downloads, a, b));
return dtPackages.slice(0, count);
}
function getDtNpmPackages(dtPath: string): string[] {
const dtPackages = fs.readdirSync(getDtTypesPath(dtPath));
const isNpmJson = getAllIsNpm(dtPath);
return dtPackages.filter(pkg => isNpmPackage(pkg, /* header */ undefined, isNpmJson));
}
function getNonNpm(args: { dtPath: string }): void {
const nonNpm: string[] = [];
const dtTypesPath = getDtTypesPath(args.dtPath);
const isNpmJson = getAllIsNpm(args.dtPath);
for (const item of fs.readdirSync(dtTypesPath)) {
const entry = path.join(dtTypesPath, item);
const dts = fs.readFileSync(entry + "/index.d.ts", "utf8");
let header;
try {
header = headerParser.parseHeaderOrFail(dts);
}
catch (e) {
header = undefined;
}
if (!isNpmPackage(item, header, isNpmJson)) {
nonNpm.push(item);
}
}
console.log(`List of non-npm packages on DT:\n${nonNpm.map(name => `DT name: ${name}\n`).join("")}`);
}
interface CommonArgs {
dtPath: string,
mode: string,
enableError: string[] | undefined,
debug: boolean,
json: boolean,
}
function checkAll(args: CommonArgs): void {
const dtPackages = fs.readdirSync(getDtTypesPath(args.dtPath));
checkPackages({ packages: dtPackages, ...args });
}
function checkPopular(args: { count: number } & CommonArgs): void {
checkPackages({ packages: getPopularNpmPackages(args.count, args.dtPath), ...args });
}
function checkUnpopular(args: { count: number } & CommonArgs): void {
checkPackages({ packages: getUnpopularNpmPackages(args.count, args.dtPath), ...args });
}
function checkPackages(args: { packages: string[] } & CommonArgs): void {
const results = args.packages.map(pkg => doCheck({ package: pkg, ...args }));
printResults(results, args.json);
}
function checkPackage(args: { package: string } & CommonArgs): void {
printResults([doCheck(args)], args.json);
}
function doCheck(args: { package: string, dtPath: string, mode: string, enableError: string[] | undefined, debug: boolean }): Result {
const dtPackage = args.package;
const opts = getOptions(args.mode, args.enableError || []);
try {
const dtsPath = path.join(getDtTypesPath(args.dtPath), dtPackage, "index.d.ts");
const errors = dtsCritic(dtsPath, /* sourcePath */ undefined, opts, args.debug);
return { package: args.package, output: errors };
}
catch (e) {
return { package: args.package, output: e.toString() };
}
}
function getOptions(modeArg: string, enabledErrors: string[]): CheckOptions {
const mode = parseMode(modeArg);
if (!mode) {
throw new Error(`Could not find mode named '${modeArg}'.`);
}
switch (mode) {
case Mode.NameOnly:
return { mode };
case Mode.Code:
const errors = getEnabledErrors(enabledErrors);
return { mode, errors };
}
}
function getEnabledErrors(errorNames: string[]): Map<ExportErrorKind, boolean> {
const errors: ExportErrorKind[] = [];
for (const name of errorNames) {
const error = parseExportErrorKind(name);
if (error === undefined) {
throw new Error(`Could not find error named '${name}'.`);
}
errors.push(error);
}
return new Map(errors.map(err => [err, true]));
}
function checkFile(args: { jsFile: string, dtsFile: string, debug: boolean }): void {
console.log(`\tChecking JS file ${args.jsFile} and declaration file ${args.dtsFile}`);
try {
const errors = checkSource(findDtsName(args.dtsFile), args.dtsFile, args.jsFile, new Map(), args.debug);
console.log(formatErrors(errors));
}
catch (e) {
console.log(e);
}
}
interface Result {
package: string,
output: CriticError[] | string,
}
function printResults(results: Result[], json: boolean): void {
if (json) {
console.log(JSON.stringify(results));
return;
}
for (const result of results) {
console.log(`\tChecking package ${result.package} ...`);
if (typeof result.output === "string") {
console.log(`Exception:\n${result.output}`);
}
else {
console.log(formatErrors(result.output));
}
}
}
function formatErrors(errors: CriticError[]): string {
const lines: string[] = [];
for (const error of errors) {
lines.push("Error: " + error.message);
}
if (errors.length === 0) {
lines.push("No errors found! :)");
}
return lines.join("\n");
}
function isNpmPackage(name: string, header?: headerParser.Header, isNpmJson: IsNpmJson = {}): boolean {
if (header && header.nonNpm) return false;
const isNpm = isNpmJson[name];
if (isNpm !== undefined) {
return isNpm;
}
return getNpmInfo(name).isNpm;
}
function main() {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
yargs
.usage("$0 <command>")
.command("check-all", "Check source and declaration of all DT packages that are on NPM.", {
dtPath: {
type: "string",
default: "../DefinitelyTyped",
describe: "Path of DT repository cloned locally.",
},
mode: {
type: "string",
required: true,
choices: [Mode.NameOnly, Mode.Code],
describe: "Mode that defines which group of checks will be made.",
},
enableError: {
type: "array",
string: true,
describe: "Enable checking for a specific export error."
},
debug: {
type: "boolean",
default: false,
describe: "Turn debug logging on.",
},
json: {
type: "boolean",
default: false,
describe: "Format output result as json."
},
}, checkAll)
.command("check-popular", "Check source and declaration of most popular DT packages that are on NPM.", {
count: {
alias: "c",
type: "number",
required: true,
describe: "Number of packages to be checked.",
},
dtPath: {
type: "string",
default: "../DefinitelyTyped",
describe: "Path of DT repository cloned locally.",
},
mode: {
type: "string",
required: true,
choices: [Mode.NameOnly, Mode.Code],
describe: "Mode that defines which group of checks will be made.",
},
enableError: {
type: "array",
string: true,
describe: "Enable checking for a specific export error."
},
debug: {
type: "boolean",
default: false,
describe: "Turn debug logging on.",
},
json: {
type: "boolean",
default: false,
describe: "Format output result as json."
},
}, checkPopular)
.command("check-unpopular", "Check source and declaration of least popular DT packages that are on NPM.", {
count: {
alias: "c",
type: "number",
required: true,
describe: "Number of packages to be checked.",
},
dtPath: {
type: "string",
default: "../DefinitelyTyped",
describe: "Path of DT repository cloned locally.",
},
mode: {
type: "string",
required: true,
choices: [Mode.NameOnly, Mode.Code],
describe: "Mode that defines which group of checks will be made.",
},
enableError: {
type: "array",
string: true,
describe: "Enable checking for a specific export error."
},
debug: {
type: "boolean",
default: false,
describe: "Turn debug logging on.",
},
json: {
type: "boolean",
default: false,
describe: "Format output result as json."
},
}, checkUnpopular)
.command("check-package", "Check source and declaration of a DT package.", {
package: {
alias: "p",
type: "string",
required: true,
describe: "DT name of a package."
},
dtPath: {
type: "string",
default: "../DefinitelyTyped",
describe: "Path of DT repository cloned locally.",
},
mode: {
type: "string",
required: true,
choices: [Mode.NameOnly, Mode.Code],
describe: "Mode that defines which group of checks will be made.",
},
enableError: {
type: "array",
string: true,
describe: "Enable checking for a specific export error."
},
debug: {
type: "boolean",
default: false,
describe: "Turn debug logging on.",
},
json: {
type: "boolean",
default: false,
describe: "Format output result as json."
},
}, checkPackage)
.command("check-file", "Check a JavaScript file and its matching declaration file.", {
jsFile: {
alias: "j",
type: "string",
required: true,
describe: "Path of JavaScript file.",
},
dtsFile: {
alias: "d",
type: "string",
required: true,
describe: "Path of declaration file.",
},
debug: {
type: "boolean",
default: false,
describe: "Turn debug logging on.",
},
}, checkFile)
.command("get-non-npm", "Get list of DT packages whose source package is not on NPM", {
dtPath: {
type: "string",
default: "../DefinitelyTyped",
describe: "Path of DT repository cloned locally.",
},
}, getNonNpm)
.demandCommand(1)
.help()
.argv;
}
main();

79
packages/dts-critic/dt.ts Normal file
View File

@@ -0,0 +1,79 @@
import { dtsCritic as critic, ErrorKind } from "./index";
import fs = require("fs");
import stripJsonComments = require("strip-json-comments");
function hasNpmNamingLintRule(tslintPath: string): boolean {
if (fs.existsSync(tslintPath)) {
const tslint = JSON.parse(stripJsonComments(fs.readFileSync(tslintPath, "utf-8")));
if(tslint.rules && tslint.rules["npm-naming"] !== undefined) {
return !!tslint.rules["npm-naming"];
}
return true;
}
return false;
}
function addNpmNamingLintRule(tslintPath: string): void {
if (fs.existsSync(tslintPath)) {
const tslint = JSON.parse(stripJsonComments(fs.readFileSync(tslintPath, "utf-8")));
if (tslint.rules) {
tslint.rules["npm-naming"] = false;
}
else {
tslint.rules = { "npm-naming": false };
}
fs.writeFileSync(tslintPath, JSON.stringify(tslint, undefined, 4), "utf-8");
}
}
function main() {
for (const item of fs.readdirSync("../DefinitelyTyped/types")) {
const entry = "../DefinitelyTyped/types/" + item;
try {
if (hasNpmNamingLintRule(entry + "/tslint.json")) {
const errors = critic(entry + "/index.d.ts");
for (const error of errors) {
switch (error.kind) {
case ErrorKind.NoMatchingNpmPackage:
console.log(`No matching npm package found for ` + item);
// const re = /\/\/ Type definitions for/;
// const s = fs.readFileSync(entry + '/index.d.ts', 'utf-8')
// fs.writeFileSync(entry + '/index.d.ts', s.replace(re, '// Type definitions for non-npm package'), 'utf-8')
break;
case ErrorKind.NoDefaultExport:
console.log("converting", item, "to export = ...");
const named = /export default function\s+(\w+\s*)\(/;
const anon = /export default function\s*\(/;
const id = /export default(\s+\w+);/;
let s = fs.readFileSync(entry + "/index.d.ts", "utf-8");
s = s.replace(named, "export = $1;\ndeclare function $1(");
s = s.replace(anon, "export = _default;\ndeclare function _default(");
s = s.replace(id, "export =$1;");
fs.writeFileSync(entry + "/index.d.ts", s, "utf-8");
break;
case ErrorKind.NoMatchingNpmVersion:
const m = error.message.match(/in the header, ([0-9.]+),[\s\S]to match one on npm: ([0-9., ]+)\./);
if (m) {
const headerver = parseFloat(m[1]);
const npmvers = m[2].split(",").map((s: string) => parseFloat(s.trim()));
const fixto = npmvers.every((v: number) => headerver > v) ? -1.0 : Math.max(...npmvers);
console.log(`npm-version:${item}:${m[1]}:${m[2]}:${fixto}`);
addNpmNamingLintRule(entry + "/tslint.json");
}
else {
console.log("could not parse error message: ", error.message);
}
break;
default:
console.log(error.message);
}
}
}
}
catch (e) {
console.log("*** ERROR for " + item + " ***");
console.log(e);
}
}
}
main();

View File

@@ -0,0 +1,259 @@
import {
findDtsName,
getNpmInfo,
dtToNpmName,
parseExportErrorKind,
dtsCritic,
checkSource,
ErrorKind,
ExportErrorKind } from "./index";
function suite(description: string, tests: { [s: string]: () => void; }) {
describe(description, () => {
for (const k in tests) {
test(k, tests[k], 10 * 1000);
}
});
}
suite("findDtsName", {
absolutePath() {
expect(findDtsName("~/dt/types/jquery/index.d.ts")).toBe("jquery");
},
relativePath() {
expect(findDtsName("jquery/index.d.ts")).toBe("jquery");
},
currentDirectory() {
expect(findDtsName("index.d.ts")).toBe("dts-critic");
},
relativeCurrentDirectory() {
expect(findDtsName("./index.d.ts")).toBe("dts-critic");
},
emptyDirectory() {
expect(findDtsName("")).toBe("dts-critic");
},
});
suite("getNpmInfo", {
nonNpm() {
expect(getNpmInfo("parseltongue")).toEqual({ isNpm: false });
},
npm() {
expect(getNpmInfo("typescript")).toEqual({
isNpm: true,
versions: expect.arrayContaining(["3.7.5"]),
tags: expect.objectContaining({ latest: expect.stringContaining("") }),
});
},
});
suite("dtToNpmName", {
nonScoped() {
expect(dtToNpmName("content-type")).toBe("content-type");
},
scoped() {
expect(dtToNpmName("babel__core")).toBe("@babel/core");
},
});
suite("parseExportErrorKind", {
existent() {
expect(parseExportErrorKind("NoDefaultExport")).toBe(ErrorKind.NoDefaultExport);
},
existentDifferentCase() {
expect(parseExportErrorKind("JspropertyNotinDTS")).toBe(ErrorKind.JsPropertyNotInDts);
},
nonexistent() {
expect(parseExportErrorKind("FakeError")).toBe(undefined);
}
});
const allErrors: Map<ExportErrorKind, true> = new Map([
[ErrorKind.NeedsExportEquals, true],
[ErrorKind.NoDefaultExport, true],
[ErrorKind.JsSignatureNotInDts, true],
[ErrorKind.DtsSignatureNotInJs, true],
[ErrorKind.DtsPropertyNotInJs, true],
[ErrorKind.JsPropertyNotInDts, true],
]);
suite("checkSource", {
noErrors() {
expect(checkSource(
"noErrors",
"testsource/noErrors.d.ts",
"testsource/noErrors.js",
allErrors,
false,
)).toEqual([]);
},
missingJsProperty() {
expect(checkSource(
"missingJsProperty",
"testsource/missingJsProperty.d.ts",
"testsource/missingJsProperty.js",
allErrors,
false,
)).toEqual(expect.arrayContaining([
{
kind: ErrorKind.JsPropertyNotInDts,
message: `The declaration doesn't match the JavaScript module 'missingJsProperty'. Reason:
The JavaScript module exports a property named 'foo', which is missing from the declaration module.`
}
]));
},
noMissingWebpackProperty() {
expect(checkSource(
"missingJsProperty",
"testsource/webpackPropertyNames.d.ts",
"testsource/webpackPropertyNames.js",
allErrors,
false,
)).toHaveLength(0);
},
missingDtsProperty() {
expect(checkSource(
"missingDtsProperty",
"testsource/missingDtsProperty.d.ts",
"testsource/missingDtsProperty.js",
allErrors,
false,
)).toEqual(expect.arrayContaining([
{
kind: ErrorKind.DtsPropertyNotInJs,
message: `The declaration doesn't match the JavaScript module 'missingDtsProperty'. Reason:
The declaration module exports a property named 'foo', which is missing from the JavaScript module.`,
position: {
start: 65,
length: 11,
},
}
]));
},
missingDefaultExport() {
expect(checkSource(
"missingDefault",
"testsource/missingDefault.d.ts",
"testsource/missingDefault.js",
allErrors,
false,
)).toEqual(expect.arrayContaining([
{
kind: ErrorKind.NoDefaultExport,
message: `The declaration doesn't match the JavaScript module 'missingDefault'. Reason:
The declaration specifies 'export default' but the JavaScript source does not mention 'default' anywhere.
The most common way to resolve this error is to use 'export =' syntax instead of 'export default'.
To learn more about 'export =' syntax, see https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require.`,
position: {
start: 0,
length: 32,
},
}
]));
},
missingJsSignatureExportEquals() {
expect(checkSource(
"missingJsSignatureExportEquals",
"testsource/missingJsSignatureExportEquals.d.ts",
"testsource/missingJsSignatureExportEquals.js",
allErrors,
false,
)).toEqual(expect.arrayContaining([
{
kind: ErrorKind.JsSignatureNotInDts,
message: `The declaration doesn't match the JavaScript module 'missingJsSignatureExportEquals'. Reason:
The JavaScript module can be called or constructed, but the declaration module cannot.`,
}
]));
},
missingJsSignatureNoExportEquals() {
expect(checkSource(
"missingJsSignatureNoExportEquals",
"testsource/missingJsSignatureNoExportEquals.d.ts",
"testsource/missingJsSignatureNoExportEquals.js",
allErrors,
false,
)).toEqual(expect.arrayContaining([
{
kind: ErrorKind.JsSignatureNotInDts,
message: `The declaration doesn't match the JavaScript module 'missingJsSignatureNoExportEquals'. Reason:
The JavaScript module can be called or constructed, but the declaration module cannot.
The most common way to resolve this error is to use 'export =' syntax.
To learn more about 'export =' syntax, see https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require.`,
}
]));
},
missingDtsSignature() {
expect(checkSource(
"missingDtsSignature",
"testsource/missingDtsSignature.d.ts",
"testsource/missingDtsSignature.js",
allErrors,
false,
)).toEqual(expect.arrayContaining([
{
kind: ErrorKind.DtsSignatureNotInJs,
message: `The declaration doesn't match the JavaScript module 'missingDtsSignature'. Reason:
The declaration module can be called or constructed, but the JavaScript module cannot.`,
}
]));
},
missingExportEquals() {
expect(checkSource(
"missingExportEquals",
"testsource/missingExportEquals.d.ts",
"testsource/missingExportEquals.js",
allErrors,
false,
)).toEqual(expect.arrayContaining([
{
kind: ErrorKind.NeedsExportEquals,
message: `The declaration doesn't match the JavaScript module 'missingExportEquals'. 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.`,
}
]));
},
});
suite("dtsCritic", {
noErrors() {
expect(dtsCritic("testsource/dts-critic.d.ts", "testsource/dts-critic.js")).toEqual([]);
},
noMatchingNpmPackage() {
expect(dtsCritic("testsource/parseltongue.d.ts")).toEqual([
{
kind: ErrorKind.NoMatchingNpmPackage,
message: `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.`,
},
]);
},
noMatchingNpmVersion() {
expect(dtsCritic("testsource/typescript.d.ts")).toEqual([
{
kind: ErrorKind.NoMatchingNpmVersion,
message: expect.stringContaining(`The types for 'typescript' must match a version that exists on npm.
You should copy the major and minor version from the package on npm.`),
},
]);
},
nonNpmHasMatchingPackage() {
expect(dtsCritic("testsource/tslib.d.ts")).toEqual([
{
kind: ErrorKind.NonNpmHasMatchingPackage,
message: `The non-npm package 'tslib' conflicts with the existing npm package 'tslib'.
Try adding -browser to the end of the name to get
tslib-browser
`,
},
]);
}
});

1031
packages/dts-critic/index.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
module.exports = {
rootDir: "dist",
moduleFileExtensions: [
"js",
"jsx",
"json",
"node"
],
};

View File

@@ -0,0 +1,56 @@
{
"name": "@definitelytyped/dts-critic",
"version": "0.0.94",
"author": "Nathan Shively-Sanders",
"description": "Checks a new .d.ts against the Javascript source and tells you what problems it has",
"dependencies": {
"@definitelytyped/header-parser": "latest",
"command-exists": "^1.2.8",
"rimraf": "^3.0.2",
"semver": "^6.2.0",
"tmp": "^0.2.1",
"yargs": "^15.3.1"
},
"peerDependencies": {
"typescript": "*"
},
"devDependencies": {
"@types/command-exists": "^1.2.0",
"@types/jest": "^24.0.0",
"@types/node": "~10.17.0",
"@types/rimraf": "^3.0.0",
"@types/semver": "^6.0.1",
"@types/strip-json-comments": "0.0.30",
"@types/tmp": "^0.2.0",
"@types/yargs": "^12.0.8",
"strip-json-comments": "^2.0.1",
"typescript": "*"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"test": "npm run build && jest",
"build": "tsc",
"dt": "node dist/dt.js",
"prepublishOnly": "npm run build && npm run test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/microsoft/DefinitelyTyped-tools.git"
},
"keywords": [
"definitely",
"typed",
"refresh",
"npm",
"tag"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/microsoft/DefinitelyTyped-tools/issues"
},
"homepage": "https://github.com/microsoft/DefinitelyTyped-tools#readme",
"engines": {
"node": ">=10.17.0"
}
}

View File

@@ -0,0 +1,8 @@
// Type definitions for package dts-critic 2.0
// Project: https://github.com/microsoft/TypeScript
// Definitions by: TypeScript Bot <https://github.com/typescript-bot>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
declare function _default(): void;
export = _default;

View File

@@ -0,0 +1 @@
module.exports = function() {};

View File

@@ -0,0 +1 @@
export default function(): void;

View File

@@ -0,0 +1 @@
module.exports = function() {};

View File

@@ -0,0 +1,3 @@
export const a: () => void;
export const b: number;
export const foo: string;

View File

@@ -0,0 +1,4 @@
module.exports = {
a: () => {},
b: 0,
};

View File

@@ -0,0 +1,7 @@
interface Exports {
(): void,
foo: () => {},
}
declare const exp: Exports;
export = exp;

View File

@@ -0,0 +1,3 @@
module.exports = {
foo: () => {},
};

View File

@@ -0,0 +1 @@
export function foo(a: number): number;

View File

@@ -0,0 +1,5 @@
function foo(a) {
return a;
}
module.exports = foo;

View File

@@ -0,0 +1,2 @@
export const a: () => void;
export const b: number;

View File

@@ -0,0 +1,5 @@
module.exports = {
a: () => {},
b: 0,
foo: "missing",
};

View File

@@ -0,0 +1,6 @@
interface Foo {
bar: () => void,
}
declare const foo: Foo;
export = foo;

View File

@@ -0,0 +1,3 @@
module.exports = class Foo {
bar() {}
};

View File

@@ -0,0 +1 @@
export default function(): void;

View File

@@ -0,0 +1 @@
module.exports = () => {};

View File

@@ -0,0 +1,2 @@
export const a: number;
export const b: string;

View File

@@ -0,0 +1,2 @@
exports.a = 42;
exports.b = "forty-two";

View File

View File

@@ -0,0 +1,4 @@
// Type definitions for non-npm package tslib
// Project: https://github.com/microsoft/TypeScript
// Definitions by: TypeScript Bot <https://github.com/typescript-bot>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped

View File

@@ -0,0 +1,4 @@
// Type definitions for typescript 1200000.5
// Project: https://github.com/microsoft/TypeScript
// Definitions by: TypeScript Bot <https://github.com/typescript-bot>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped

View File

@@ -0,0 +1 @@
export var normal: string;

View File

@@ -0,0 +1,12 @@
var $export = {};
// type bitmap
$export.F = 1; // forced
$export.G = 2; // global
$export.S = 4; // static
$export.P = 8; // proto
$export.B = 16; // bind
$export.W = 32; // wrap
$export.U = 64; // safe
$export.R = 128; // real proto method for `library`
$export.normal = "hi";
module.exports = $export;

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"target": "es2019",
"module": "commonjs",
"resolveJsonModule": true,
"strict": true,
"sourceMap": true,
"outDir": "dist",
"declaration": true,
"esModuleInterop": true,
"noImplicitReturns": true,
},
"exclude": [
"dist/*",
"sources/*",
"testsource/*",
]
}

View File

@@ -30,9 +30,6 @@
"devDependencies": {
"@types/fs-extra": "^8.1.0",
"@types/stats-lite": "^2.2.0",
"dtslint": "^4.0.6"
},
"peerDependencies": {
"dtslint": "*"
"@definitelytyped/dtslint": "^0.0.94"
}
}

View File

@@ -74,7 +74,7 @@ export async function runDTSLint({
expectOnly: expectOnly || !packageNames.includes(path)
})),
commandLineArgs: dtslintArgs,
workerFile: require.resolve("dtslint"),
workerFile: require.resolve("@definitelytyped/dtslint"),
nProcesses,
cwd: typesPath,
crashRecovery: true,

View File

@@ -0,0 +1,18 @@
name: CI
on:
pull_request
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
registry-url: "https://registry.npmjs.org"
- run: "npm install"
- run: "npm test"
- run: "npm run lint"

View File

@@ -0,0 +1,29 @@
name: Deploy to npm
on:
push:
branches:
- main
- master
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
registry-url: "https://registry.npmjs.org"
# Ensure everything is set up right
- run: "npm install"
- run: "npm test"
- uses: orta/npm-should-deploy-action@main
id: check
- run: "npm publish"
if: ${{ steps.check.outputs.deploy == 'true' }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

3
packages/dtslint/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
bin
node_modules
typescript-installs

View File

@@ -0,0 +1 @@
* @sandersn

21
packages/dtslint/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

177
packages/dtslint/README.md Normal file
View File

@@ -0,0 +1,177 @@
`dtslint` tests a TypeScript declaration file for style and correctness.
It will install `typescript` and `tslint` for you, so this is the only tool you need to test a type definition.
Lint rules new to dtslint are documented in the [docs](docs) directory.
# Just looking for ExpectType and ExpectError?
[Use tsd instead](https://github.com/SamVerschueren/tsd).
# Setup
If you are working on DefinitelyTyped, read the [DefinitelyTyped README](https://github.com/DefinitelyTyped/DefinitelyTyped#readme).
If you are writing the library in TypeScript, don't use `dtslint`.
Use [`--declaration`](http://www.typescriptlang.org/docs/handbook/compiler-options.html) to have type definitions generated for you.
If you are a library author, read below.
## Add types for a library (not on DefinitelyTyped)
[`dts-gen`](https://github.com/Microsoft/dts-gen#readme) may help, but is not required.
Create a `types` directory. (Name is arbitrary.)
Add `"types": "types"` to your `package.json`.
Read more on bundling types [here](http://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html).
#### `types/index.d.ts`
Only `index.d.ts` needs to be published to NPM. Other files are just for testing.
Write your type definitions here.
Refer to the [handbook](http://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html) or `dts-gen`'s templates for how to do this.
#### `types/tsconfig.json`
```json5
{
"compilerOptions": {
"module": "commonjs",
"lib": ["es6"],
"noImplicitAny": true,
"noImplicitThis": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"types": [],
"noEmit": true,
"forceConsistentCasingInFileNames": true,
// If the library is an external module (uses `export`), this allows your test file to import "mylib" instead of "./index".
// If the library is global (cannot be imported via `import` or `require`), leave this out.
"baseUrl": ".",
"paths": { "mylib": ["."] }
}
}
```
You may extend `"lib"` to, for example, `["es6", "dom"]` if you need those typings.
You may also have to add `"target": "es6"` if using certain language features.
#### `types/tslint.json`
If you are using the default rules, this is optional.
If present, this will override `dtslint`'s [default](https://github.com/Microsoft/dtslint/blob/master/dtslint.json) settings.
You can specify new lint [rules](https://palantir.github.io/tslint/rules/), or disable some. An example:
```json5
{
"extends": "dtslint/dtslint.json", // Or "dtslint/dt.json" if on DefinitelyTyped
"rules": {
"semicolon": false,
"indent": [true, "tabs"]
}
}
```
#### `types/test.ts`
You can have any number of test files you want, with any names. See below on what to put in them.
## Write tests
A test file should be a piece of sample code that tests using the library. Tests are type-checked, but not run.
To assert that an expression is of a given type, use `$ExpectType`.
To assert that an expression causes a compile error, use `$ExpectError`.
(Assertions will be checked by the `expect` lint rule.)
```ts
import { f } from "my-lib"; // f is(n: number) => void
// $ExpectType void
f(1);
// Can also write the assertion on the same line.
f(2); // $ExpectType void
// $ExpectError
f("one");
```
## Specify a TypeScript version
Normally packages will be tested using TypeScript 2.0.
To use a newer version, specify it by including a comment like so:
```ts
// Minimum TypeScript Version: 2.1
```
For DefinitelyTyped packages, this should go just under the header (on line 5).
For bundled typings, this can go on any line (but should be near the top).
## Run tests
- `npm install --save-dev dtslint`
- Add to your `package.json` `scripts`: `"dtslint": "dtslint types"`
- `npm run dtslint`
### Options
- `--localTs`
Use your locally installed version of TypeScript.
```sh
dtslint --localTs node_modules/typescript/lib types
```
- `--expectOnly`
Disable all the lint rules except the one that checks for type correctness.
```sh
dtslint --expectOnly types
```
# Contributing
## Build
```sh
npm link . # Global 'dts-lint' should now refer to this.
npm run watch
```
## Test
Use `npm run test` to run all tests.
To run a single test: `node node_modules/tslint/bin/tslint --rules-dir bin/rules --test test/expect`.
## Publish
1. Change the version in the `package.json`
2. Push to master
## Code of Conduct
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.
## FAQ
I'm getting an error about a missing typescript install.
```
Error: Cannot find module '/node_modules/dtslint/typescript-installs/3.1/node_modules/typescript`
```
Your dependencies may be out of date.
[@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.
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.
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).

View File

@@ -0,0 +1,88 @@
# dt-header
(This rule is specific to DefinitelyTyped.)
Checks the format of DefinitelyTyped header comments.
---
**Bad**:
```ts
// Type definitions for foo v1.2.3
```
* Don't include `v`
* Don't include a patch version
**Good**:
```ts
// Type definitions for foo 1.2
```
---
**Bad**:
```ts
// Definitions by: My Name <http://geocities.com/myname>
```
**Good**:
```ts
// Definitions by: My Name <https://github.com/myname>
```
* Prefer a GitHub username, not a personal web site.
---
**Bad**:
`foo/index.d.ts`:
```ts
// Type definitions for abs 1.2
// Project: https://github.com/foo/foo
// Definitions by: My Name <https://github.com/myname>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
export { f } from "./subModule";
```
`foo/subModule.d.ts`:
```ts
// Type definitions for abs 1.2
// Project: https://github.com/foo/foo
// Definitions by: My Name <https://github.com/myname>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
export function f(): number;
```
`foo/ts3.1/index.d.ts`:
```ts
// Type definitions for abs 1.2
// Project: https://github.com/foo/foo
// Definitions by: My Name <https://github.com/myname>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
export function f(): number;
```
**Good**:
`foo/index.d.ts`: Same
`foo/subModule.d.ts`:
```ts
export function f(): number;
```
`foo/ts3.1/index.d.ts`:
```ts
export function f(): number;
```
Don't repeat the header -- only do it in the index of the root.

View File

@@ -0,0 +1,29 @@
# export-just-namespace
Declaring a namespace is unnecessary if that is the module's only content; just use ES6 export syntax instead.
**Bad**:
```ts
namespace MyLib {
export function f(): number;
}
export = MyLib;
```
**Good**:
```ts
export function f(): number;
```
**Also good**:
```ts
namespace MyLib {
export function f(): number;
}
function MyLib(): number;
export = MyLib;
```

View File

@@ -0,0 +1,27 @@
# no-any-union
Forbids to include `any` in a union. When `any` is used in a union type, the resulting type is still `any`.
**Bad**:
```ts
function f(x: string | any): void;
```
**Good**:
```ts
function f(x: string): void;
```
Or:
```ts
function f(x: any): void;
```
Or:
```ts
function f(x: string | object): void;
```
While the `string` portion of this type annotation may _look_ useful, it in fact offers no additional typechecking over simply using `any`.

View File

@@ -0,0 +1,28 @@
# no-bad-reference
(This rule is specific to DefinitelyTyped.)
Avoid using `<reference path>`.
**Bad**:
```ts
/// <reference path="../node_modules/@types/foo/index.d.ts" />
import * as foo from "foo";
```
**Good**:
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):
```ts
// TypeScript will look for a definition for "foo" using module resolution
import * as foo from "foo";
```
If not, use `<reference types>` instead:
```ts
/// <reference types="foo" />
```
The only time `<reference path>` 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.

View File

@@ -0,0 +1,16 @@
# no-const-enum
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.
**Bad**:
```ts
const enum Bit { Off, On }
export function f(b: Bit): void;
```
**Good**:
```ts
export function f(b: 0 | 1): void;
```

View File

@@ -0,0 +1,17 @@
# no-dead-reference
A `<reference>` comment should go at the top of a file -- otherwise it is just a normal comment.
**Bad**:
```ts
console.log("Hello world!");
/// <reference types="jquery" />
```
**Good**:
```ts
/// <reference types="jquery" />
console.log("Hello world!");
```

View File

@@ -0,0 +1,35 @@
# no-declare-current-package
Avoid using `declare module`, and prefer to declare module contents in a file.
**Bad**:
```ts
// foo/index.d.ts
declare module "foo" {
export const x = 0;
}
```
**Good**:
```ts
// foo/index.d.ts
export const x = 0;
```
**Bad**:
```ts
// foo/index.d.ts
declare module "foo/bar" {
export const x = 0;
}
```
**Good**:
```ts
// foo/bar.d.ts
export const x = 0;
```

View File

@@ -0,0 +1,22 @@
# no-import-default-of-export-equals
Don't use a default import of a package that uses `export =`.
Users who do not have `--allowSyntheticDefaultExports` or `--esModuleInterop` will get different behavior.
This rule only applies to definition files -- for test files you can use a default import if you prefer.
**Bad**:
```ts
// foo/index.d.ts
declare interface I {}
export = I;
// bar/index.d.ts
import I from "foo";
```
**Good**:
```ts
import I = require("foo");
```

View File

@@ -0,0 +1,23 @@
# no-outside-dependencies
Don't import from `DefinitelyTyped/node_modules`.
**Bad**:
```ts
import * as x from "x";
// where 'x' is defined only in `DefinitelyTyped/node_modules`
```
**Good**:
Add a `package.json`:
```ts
{
"private": true,
"dependencies": {
"x": "^1.2.3"
}
}
```

View File

@@ -0,0 +1,33 @@
# no-padding
Avoid blank lines before opening tokens or after closing tokens.
**Bad**:
```ts
function f() {
return [
g(
0
)
];
}
```
**Good**:
```ts
function f() {
return [
g(
0
)
];
}
```

View File

@@ -0,0 +1,15 @@
# no-relative-import-in-test
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).
**Bad**:
```ts
import foo from "./index.d.ts";
```
**Good**:
```ts
import foo from "foo";
```

View File

@@ -0,0 +1,27 @@
# no-self-import
A package should not import components of itself using a globally-qualified name; it should use relative imports instead.
**Bad**:
```ts
import foo from "this-package/foo.d.ts";
```
**Good**:
```ts
import foo from "./foo.d.ts";
```
**Bad**:
```ts
import myself from "this-package";
```
**Good**:
```ts
import myself from ".";
```

View File

@@ -0,0 +1,19 @@
# no-single-declare-module
`declare module` should typically be avoided.
Instead, the file itself should be used as the declaration for the module.
TypeScript uses [module resolution](http://www.typescriptlang.org/docs/handbook/module-resolution.html) to determine what files are associated with what modules.
**Bad**:
```ts
declare module "mylib" {
function foo(): number;
}
```
**Good**:
```ts
export function foo(): number;
```

View File

@@ -0,0 +1,15 @@
# no-single-element-tuple-type
Some users mistakenly write `[T]` when then intend to write an array type `T[]`.
**Bad**:
```ts
export const x: [T];
```
**Good**:
```ts
export const x: T[];
```

View File

@@ -0,0 +1,69 @@
# no-unnecessary-generics
Forbids a function to use a generic type parameter only once.
Generic type parameters allow you to relate the type of one thing to another;
if they are used only once, they can be replaced with their type constraint.
**Bad**:
```ts
function logAnything<T>(x: T): void;
```
**Good**:
```ts
function logAnything(x: any): void;
```
---
**Bad**:
```ts
function useLogger<T extends Logger>(logger: T): void;
```
**Good**:
```ts
function useLogger(logger: Logger): void;
```
---
**Bad**:
```ts
function clear<T>(array: T[]): void;
```
**Good**:
```ts
function clear(array: any[]): void;
```
---
`getMeAT<T>(): T`:
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.
Prefer to use a real type assertion, e.g. `getMeAT() as number`.
Example where a type parameter is acceptable: `function id<T>(value: T): T;`.
Example where it is not acceptable: `function parseJson<T>(json: string): T;`.
Exception: `new Map<string, number>()` is OK.
**Bad**:
```ts
function parse<T>(): T;
const x = parse<number>();
```
**Good**:
```ts
function parse(): {};
const x = parse() as number;
```

View File

@@ -0,0 +1,14 @@
# no-useless-files
Don't include empty files.
**Bad**:
```ts
```
**Good**:
```ts
export function something(): void;
```

View File

@@ -0,0 +1,137 @@
# npm-naming
(This rule is specific to DefinitelyTyped.)
## Name checks
In 'name-only' mode, checks that the name of the type package matches a source package on npm.
---
**Bad**:
```ts
// Type definitions for browser-only-package 1.2
```
* If the package is really browser-only, you have to mark it with "non-npm package".
* If the package actually has a matching npm package, you must use that name.
**Good**:
```ts
// Type definitions for non-npm package browser-only-package 1.2
```
---
**Bad**:
```ts
// Type definitions for some-package 101.1
```
* The version number in the header must actually exist on npm for the source package.
**Good**:
```ts
// Type definitions for some-package 10.1
```
## Code checks
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.
---
**Bad**:
`foo/index.d.ts`:
```ts
declare function f(): void;
export default f;
```
`foo/index.js`:
```js
module.exports = function () {
};
```
* 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.
* `export default` can only be used to export a CommonJs `module.exports =` when you have `esModuleInterop` turned on, which not everybody does.
**Good**:
`foo/index.d.ts`:
```ts
declare function f(): void;
export = f;
```
---
**Bad**:
`foo/index.d.ts`:
```ts
export class C {}
```
`foo/index.js`:
```js
module.exports = class C {}
```
* The CommonJs module is a class, which means it can be constructed, like this:
```js
var C = require('foo');
var x = new C();
```
However, the way `class C` is exported in the d.ts file (using an export declaration) means it can only be used like this:
```ts
var foo = require('foo');
var x = new foo.C();
```
* The d.ts should use [`export =`](https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require)
syntax to match the CommonJs module behavior.
**Good**:
`foo/index.d.ts`:
```ts
declare class C {}
export = C;
```
* If you need to use `export =` syntax as in the example above, and the source JavaScript also exports some properties,
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:
**JavaScript**:
`foo/index.js`:
```js
function foo() {};
foo.bar = "Exported property";
module.exports = foo; // module.exports is a function, but it also has a property called `bar`
```
**Declaration**:
`foo/index.d.ts`:
```ts
declare function foo(): void;
declare namespace foo {
var bar: string;
}
export = foo;
```

View File

@@ -0,0 +1,15 @@
# prefer-declare-function
Prefer to declare a function using the `function` keyword instead of a variable of function type.
**Bad**:
```ts
export const f: () => number;
```
**Good**:
```ts
export function f(): number;
```

View File

@@ -0,0 +1,15 @@
# redundant-undefined
Avoid explicitly specifying `undefined` as a type for a parameter which is already optional.
**Bad**:
```ts
function f(s?: string | undefined): void {}
```
**Good**:
```ts
function f(s?: string): void {}
```

View File

@@ -0,0 +1,21 @@
# strict-export-declare-modifiers
Avoid adding the `declare` keyword unnecessarily.
Do add the `export` keyword unnecessarily, because sometimes it is necessary and we want to be consistent.
**Bad**:
```ts
export declare function f(): void;
declare function g(): void;
interface I {}
```
**Good**:
```ts
export function f(): void;
export function g(): void;
export interface I {}
```

View File

@@ -0,0 +1,17 @@
# trim-file
Don't include blank lines at the beginning or end of a file.
**Bad**:
```ts
export function f(): number;
```
**Good**:
```ts
export function f(): number;
```

View File

@@ -0,0 +1,15 @@
# void-return
`void` should be used as a return type, but not as a parameter type.
**Bad**:
```ts
export function f(x: string | void): undefined;
```
**Good**:
```ts
export function f(x: string | undefined): void;
```

15
packages/dtslint/dt.json Normal file
View File

@@ -0,0 +1,15 @@
{
"extends": "./dtslint.json",
"rules": {
"dt-header": true,
"no-bad-reference": true,
"no-declare-current-package": true,
"no-self-import": true,
"no-outside-dependencies": true,
"no-redundant-jsdoc": false,
"no-redundant-jsdoc-2": true,
"npm-naming": [true, { "mode": "code" }]
}
}

View File

@@ -0,0 +1,6 @@
{
"rulesDirectory": "./bin/rules",
"rules": {
"expect": true
}
}

View File

@@ -0,0 +1,131 @@
{
"extends": "tslint:all",
"rulesDirectory": "./bin/rules",
"rules": {
// Custom rules
"expect": true,
"export-just-namespace": true,
"no-bad-reference": true,
"no-const-enum": true,
"no-dead-reference": true,
"no-import-default-of-export-equals": true,
"no-padding": true,
"redundant-undefined": true,
"no-relative-import-in-test": true,
"strict-export-declare-modifiers": true,
"no-any-union": true,
"no-single-declare-module": true,
"no-unnecessary-generics": true,
"no-useless-files": true,
"prefer-declare-function": true,
"trim-file": true,
"unified-signatures": true,
"void-return": true,
"npm-naming": true,
"comment-format": [true, "check-space"], // But not check-uppercase or check-lowercase
"interface-name": [true, "never-prefix"],
"max-line-length": [true, 200],
"member-access": [true, "no-public"],
"no-consecutive-blank-lines": true,
"no-unnecessary-callback-wrapper": true,
"no-namespace": [true, "allow-declarations"],
"object-literal-key-quotes": [true, "as-needed"],
"one-line": [
true,
"check-catch",
"check-finally",
"check-else",
"check-open-brace",
"check-whitespace"
],
"one-variable-per-declaration": [true, "ignore-for-loop"],
"only-arrow-functions": [true, "allow-declarations", "allow-named-functions"],
"prefer-template": [true, "allow-single-concat"],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-module",
"check-separator",
"check-type",
"check-typecast"
],
// TODO?
"align": false, // TODO
"arrow-parens": false,
"arrow-return-shorthand": true, // TODO: "multiline"
"linebreak-style": false, // TODO
"no-void-expression": [true, "ignore-arrow-function-shorthand"],
"no-any": false, // TODO
"no-floating-promises": false, // TODO: https://github.com/palantir/tslint/issues/2879
"no-import-side-effect": false,
"no-this-assignment": false,
"no-unbound-method": false, // TODO?
"no-unsafe-any": false, // TODO
"no-restricted-globals": false,
"number-literal-format": false, // TODO
"promise-function-async": false,
"restrict-plus-operands": false, // TODO
"return-undefined": false, // TODO
"switch-final-break": false, // TODO
"prefer-method-signature": false, // TODO?
// Pretty sure we don't want these
"binary-expression-operand-order": false,
"class-name": false,
"completed-docs": false,
"curly": false,
"cyclomatic-complexity": false,
"deprecation": false,
"file-name-casing": false,
"forin": false,
"indent": false,
"match-default-export-name": false,
"max-classes-per-file": false,
"max-file-line-count": false,
"member-ordering": false,
"newline-before-return": false,
"newline-per-chained-call": false,
"no-bitwise": false,
"no-console": false,
"no-default-export": false,
"no-empty": false,
"no-implicit-dependencies": false, // See https://github.com/palantir/tslint/issues/3364
"no-inferred-empty-object-type": false,
"no-magic-numbers": false,
"no-non-null-assertion": false,
"no-null-keyword": false,
"no-parameter-properties": false,
"no-parameter-reassignment": false,
"no-reference": false, // But see no-bad-reference
"no-require-imports": false,
"no-shadowed-variable": false,
"no-string-literal": false,
"no-submodule-imports": false,
"no-tautology-expression": false,
"no-unused-expression": false,
"no-unused-variable": false,
"no-use-before-declare": false,
"object-literal-sort-keys": false,
"ordered-imports": false,
"prefer-function-over-method": false,
"quotemark": false,
"strict-boolean-expressions": false,
"strict-type-predicates": false,
"switch-default": false,
"trailing-comma": false,
"triple-equals": [true, "allow-null-check"],
"typedef": false,
"type-literal-delimiter": false,
"variable-name": false,
"increment-decrement": false,
"unnecessary-constructor": false,
"unnecessary-else": false,
"no-angle-bracket-type-assertion": false,
"no-default-import": false,
"callable-types": false
}
}

View File

@@ -0,0 +1,57 @@
{
"name": "@definitelytyped/dtslint",
"version": "0.0.94",
"description": "Runs tests on TypeScript definition files",
"files": [
"bin",
"dt.json",
"dtslint.json",
"dtslint-expect-only.json"
],
"main": "bin",
"bin": "./bin/index.js",
"contributors": [
"Nathan Shively-Sanders <nathansa@microsoft.com> (https://github.com/sandersn)",
"Andy Hanson <andy-ms@microsoft.com> (https://github.com/andy-ms)",
"Dan Vanderkam <danvdk@gmail.com> (https://github.com/danvk)"
],
"repository": {
"type": "git",
"url": "https://github.com/microsoft/DefinitelyTyped-tools.git"
},
"scripts": {
"watch": "tsc --watch",
"build": "tsc",
"lint": "eslint --ext ts src",
"test": "npm run build && node test/test.js"
},
"dependencies": {
"@definitelytyped/header-parser": "0.0.93",
"@definitelytyped/typescript-versions": "0.0.93",
"@definitelytyped/utils": "0.0.93",
"@definitelytyped/dts-critic": "0.0.94",
"fs-extra": "^6.0.1",
"json-stable-stringify": "^1.0.1",
"strip-json-comments": "^2.0.1",
"tslint": "5.14.0",
"yargs": "^15.1.0"
},
"peerDependencies": {
"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"
},
"devDependencies": {
"@types/fs-extra": "^5.0.2",
"@types/json-stable-stringify": "^1.0.32",
"@types/node": "14.0.x",
"@types/strip-json-comments": "^0.0.28",
"@types/yargs": "^15.0.3",
"@typescript-eslint/eslint-plugin": "^4.11.1",
"@typescript-eslint/parser": "^4.11.1",
"eslint": "^7.16.0",
"typescript": "next"
},
"engines": {
"node": ">=10.0.0"
},
"license": "MIT"
}

View File

@@ -0,0 +1,21 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Attach",
"port": 9229,
"sourceMaps": true
},
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${file}"
}
]
}

View File

@@ -0,0 +1,151 @@
import { makeTypesVersionsForPackageJson } from "@definitelytyped/header-parser";
import { TypeScriptVersion } from "@definitelytyped/typescript-versions";
import assert = require("assert");
import { pathExists } from "fs-extra";
import { join as joinPaths } from "path";
import { getCompilerOptions, readJson } from "./util";
export async function checkPackageJson(
dirPath: string,
typesVersions: readonly TypeScriptVersion[],
): Promise<void> {
const pkgJsonPath = joinPaths(dirPath, "package.json");
const needsTypesVersions = typesVersions.length !== 0;
if (!await pathExists(pkgJsonPath)) {
if (needsTypesVersions) {
throw new Error(`${dirPath}: Must have 'package.json' for "typesVersions"`);
}
return;
}
const pkgJson = await readJson(pkgJsonPath) as Record<string, unknown>;
if ((pkgJson as any).private !== true) {
throw new Error(`${pkgJsonPath} should set \`"private": true\``);
}
if (needsTypesVersions) {
assert.strictEqual((pkgJson as any).types, "index", `"types" in '${pkgJsonPath}' should be "index".`);
const expected = makeTypesVersionsForPackageJson(typesVersions);
assert.deepEqual((pkgJson as any).typesVersions, expected,
`"typesVersions" in '${pkgJsonPath}' is not set right. Should be: ${JSON.stringify(expected, undefined, 4)}`);
}
for (const key in pkgJson) { // tslint:disable-line forin
switch (key) {
case "private":
case "dependencies":
case "license":
case "imports":
case "exports":
case "type":
// "private"/"typesVersions"/"types" checked above, "dependencies" / "license" checked by types-publisher,
break;
case "typesVersions":
case "types":
if (!needsTypesVersions) {
throw new Error(`${pkgJsonPath} doesn't need to set "${key}" when no 'ts3.x' directories exist.`);
}
break;
default:
throw new Error(`${pkgJsonPath} should not include field ${key}`);
}
}
}
export interface DefinitelyTypedInfo {
/** "../" or "../../" or "../../../". This should use '/' even on windows. */
readonly relativeBaseUrl: string;
}
export async function checkTsconfig(dirPath: string, dt: DefinitelyTypedInfo | undefined): Promise<void> {
const options = await getCompilerOptions(dirPath);
if (dt) {
const { relativeBaseUrl } = dt;
const mustHave = {
module: "commonjs",
noEmit: true,
forceConsistentCasingInFileNames: true,
baseUrl: relativeBaseUrl,
typeRoots: [relativeBaseUrl],
types: [],
};
for (const key of Object.getOwnPropertyNames(mustHave) as (keyof typeof mustHave)[]) {
const expected = mustHave[key];
const actual = options[key];
if (!deepEquals(expected, actual)) {
throw new Error(`Expected compilerOptions[${JSON.stringify(key)}] === ${JSON.stringify(expected)}`);
}
}
for (const key in options) { // tslint:disable-line forin
switch (key) {
case "lib":
case "noImplicitAny":
case "noImplicitThis":
case "strict":
case "strictNullChecks":
case "noUncheckedIndexedAccess":
case "strictFunctionTypes":
case "esModuleInterop":
case "allowSyntheticDefaultImports":
// Allow any value
break;
case "target":
case "paths":
case "jsx":
case "jsxFactory":
case "experimentalDecorators":
case "noUnusedLocals":
case "noUnusedParameters":
// OK. "paths" checked further by types-publisher
break;
default:
if (!(key in mustHave)) {
throw new Error(`Unexpected compiler option ${key}`);
}
}
}
}
if (!("lib" in options)) {
throw new Error('Must specify "lib", usually to `"lib": ["es6"]` or `"lib": ["es6", "dom"]`.');
}
if ("strict" in options) {
if (options.strict !== true) {
throw new Error('When "strict" is present, it must be set to `true`.');
}
for (const key of ["noImplicitAny", "noImplicitThis", "strictNullChecks", "strictFunctionTypes"]) {
if (key in options) {
throw new TypeError(`Expected "${key}" to not be set when "strict" is \`true\`.`);
}
}
} else {
for (const key of ["noImplicitAny", "noImplicitThis", "strictNullChecks", "strictFunctionTypes"]) {
if (!(key in options)) {
throw new Error(`Expected \`"${key}": true\` or \`"${key}": false\`.`);
}
}
}
if (options.types && options.types.length) {
throw new Error(
'Use `/// <reference types="..." />` directives in source files and ensure ' +
'that the "types" field in your tsconfig is an empty array.');
}
}
function deepEquals(expected: unknown, actual: unknown): boolean {
if (expected instanceof Array) {
return actual instanceof Array
&& actual.length === expected.length
&& expected.every((e, i) => deepEquals(e, actual[i]));
} else {
return expected === actual;
}
}

View File

@@ -0,0 +1,266 @@
#!/usr/bin/env node
import { parseTypeScriptVersionLine } from "@definitelytyped/header-parser";
import { AllTypeScriptVersion, TypeScriptVersion } from "@definitelytyped/typescript-versions";
import assert = require("assert");
import { readdir, readFile, stat } from "fs-extra";
import { basename, dirname, join as joinPaths, resolve } from "path";
import { cleanTypeScriptInstalls, installAllTypeScriptVersions, installTypeScriptNext } from "@definitelytyped/utils";
import { checkPackageJson, checkTsconfig } from "./checks";
import { checkTslintJson, lint, TsVersion } from "./lint";
import { mapDefinedAsync, withoutPrefix } from "./util";
async function main(): Promise<void> {
const args = process.argv.slice(2);
let dirPath = process.cwd();
let onlyTestTsNext = false;
let expectOnly = false;
let shouldListen = false;
let lookingForTsLocal = false;
let tsLocal: string | undefined;
for (const arg of args) {
if (lookingForTsLocal) {
if (arg.startsWith("--")) {
throw new Error("Looking for local path for TS, but got " + arg);
}
tsLocal = resolve(arg);
lookingForTsLocal = false;
continue;
}
switch (arg) {
case "--installAll":
console.log("Cleaning old installs and installing for all TypeScript versions...");
console.log("Working...");
await cleanTypeScriptInstalls();
await installAllTypeScriptVersions();
return;
case "--localTs":
lookingForTsLocal = true;
break;
case "--version":
console.log(require("../package.json").version);
return;
case "--expectOnly":
expectOnly = true;
break;
case "--onlyTestTsNext":
onlyTestTsNext = true;
break;
// Only for use by types-publisher.
// Listens for { path, onlyTestTsNext } messages and ouputs { path, status }.
case "--listen":
shouldListen = true;
break;
default: {
if (arg.startsWith("--")) {
console.error(`Unknown option '${arg}'`);
usage();
process.exit(1);
}
const path = arg.indexOf("@") === 0 && arg.indexOf("/") !== -1
// we have a scoped module, e.g. @bla/foo
// which should be converted to bla__foo
? arg.substr(1).replace("/", "__")
: arg;
dirPath = joinPaths(dirPath, path);
}
}
}
if (lookingForTsLocal) {
throw new Error("Path for --localTs was not provided.");
}
if (shouldListen) {
listen(dirPath, tsLocal, onlyTestTsNext);
} else {
await installTypeScriptAsNeeded(tsLocal, onlyTestTsNext);
await runTests(dirPath, onlyTestTsNext, expectOnly, tsLocal);
}
}
async function installTypeScriptAsNeeded(tsLocal: string | undefined, onlyTestTsNext: boolean): Promise<void> {
if (tsLocal) return;
if (onlyTestTsNext) {
return installTypeScriptNext();
}
return installAllTypeScriptVersions();
}
function usage(): void {
console.error("Usage: dtslint [--version] [--installAll] [--onlyTestTsNext] [--expectOnly] [--localTs path]");
console.error("Args:");
console.error(" --version Print version and exit.");
console.error(" --installAll Cleans and installs all TypeScript versions.");
console.error(" --expectOnly Run only the ExpectType lint rule.");
console.error(" --onlyTestTsNext Only run with `typescript@next`, not with the minimum version.");
console.error(" --localTs path Run with *path* as the latest version of TS.");
console.error("");
console.error("onlyTestTsNext and localTs are (1) mutually exclusive and (2) test a single version of TS");
}
function listen(dirPath: string, tsLocal: string | undefined, alwaysOnlyTestTsNext: boolean): void {
// Don't await this here to ensure that messages sent during installation aren't dropped.
const installationPromise = installTypeScriptAsNeeded(tsLocal, alwaysOnlyTestTsNext);
process.on("message", async (message: unknown) => {
const { path, onlyTestTsNext, expectOnly } = message as { path: string, onlyTestTsNext: boolean, expectOnly?: boolean };
await installationPromise;
runTests(joinPaths(dirPath, path), onlyTestTsNext, !!expectOnly, tsLocal)
.catch(e => e.stack)
.then(maybeError => {
process.send!({ path, status: maybeError === undefined ? "OK" : maybeError });
})
.catch(e => console.error(e.stack));
});
}
async function runTests(
dirPath: string,
onlyTestTsNext: boolean,
expectOnly: boolean,
tsLocal: string | undefined,
): Promise<void> {
const isOlderVersion = /^v(0\.)?\d+$/.test(basename(dirPath));
const indexText = await readFile(joinPaths(dirPath, "index.d.ts"), "utf-8");
// If this *is* on DefinitelyTyped, types-publisher will fail if it can't parse the header.
const dt = indexText.includes("// Type definitions for");
if (dt) {
// Someone may have copied text from DefinitelyTyped to their type definition and included a header,
// so assert that we're really on DefinitelyTyped.
assertPathIsInDefinitelyTyped(dirPath);
assertPathIsNotBanned(dirPath);
}
const typesVersions = await mapDefinedAsync(await readdir(dirPath), async name => {
if (name === "tsconfig.json" || name === "tslint.json" || name === "tsutils") { return undefined; }
const version = withoutPrefix(name, "ts");
if (version === undefined || !(await stat(joinPaths(dirPath, name))).isDirectory()) { return undefined; }
if (!TypeScriptVersion.isTypeScriptVersion(version)) {
throw new Error(`There is an entry named ${name}, but ${version} is not a valid TypeScript version.`);
}
if (!TypeScriptVersion.isRedirectable(version)) {
throw new Error(`At ${dirPath}/${name}: TypeScript version directories only available starting with ts3.1.`);
}
return version;
});
if (dt) {
await checkPackageJson(dirPath, typesVersions);
}
const minVersion = maxVersion(
getMinimumTypeScriptVersionFromComment(indexText),
TypeScriptVersion.lowest) as TypeScriptVersion;
if (onlyTestTsNext || tsLocal) {
const tsVersion = tsLocal ? "local" : TypeScriptVersion.latest;
await testTypesVersion(dirPath, tsVersion, tsVersion, isOlderVersion, dt, expectOnly, tsLocal, /*isLatest*/ true);
} else {
// For example, typesVersions of [3.2, 3.5, 3.6] will have
// associated ts3.2, ts3.5, ts3.6 directories, for
// <=3.2, <=3.5, <=3.6 respectively; the root level is for 3.7 and above.
// so this code needs to generate ranges [lowest-3.2, 3.3-3.5, 3.6-3.6, 3.7-latest]
const lows = [TypeScriptVersion.lowest, ...typesVersions.map(next)];
const his = [...typesVersions, TypeScriptVersion.latest];
assert.strictEqual(lows.length, his.length);
for (let i = 0; i < lows.length; i++) {
const low = maxVersion(minVersion, lows[i]);
const hi = his[i];
assert(
parseFloat(hi) >= parseFloat(low),
`'// Minimum TypeScript Version: ${minVersion}' in header skips ts${hi} folder.`);
const isLatest = hi === TypeScriptVersion.latest;
const versionPath = isLatest ? dirPath : joinPaths(dirPath, `ts${hi}`);
if (lows.length > 1) {
console.log("testing from", low, "to", hi, "in", versionPath);
}
await testTypesVersion(versionPath, low, hi, isOlderVersion, dt, expectOnly, undefined, isLatest);
}
}
}
function maxVersion(v1: TypeScriptVersion | undefined, v2: TypeScriptVersion): TypeScriptVersion;
function maxVersion(v1: AllTypeScriptVersion | undefined, v2: AllTypeScriptVersion): AllTypeScriptVersion;
function maxVersion(v1: AllTypeScriptVersion | undefined, v2: AllTypeScriptVersion) {
if (!v1) return v2;
if (!v2) return v1;
if (parseFloat(v1) >= parseFloat(v2)) return v1;
return v2;
}
function next(v: TypeScriptVersion): TypeScriptVersion {
const index = TypeScriptVersion.supported.indexOf(v);
assert.notStrictEqual(index, -1);
assert(index < TypeScriptVersion.supported.length);
return TypeScriptVersion.supported[index + 1];
}
async function testTypesVersion(
dirPath: string,
lowVersion: TsVersion,
hiVersion: TsVersion,
isOlderVersion: boolean,
dt: boolean,
expectOnly: boolean,
tsLocal: string | undefined,
isLatest: boolean,
): Promise<void> {
await checkTslintJson(dirPath, dt);
await checkTsconfig(dirPath, dt
? { relativeBaseUrl: ".." + (isOlderVersion ? "/.." : "") + (isLatest ? "" : "/..") + "/" }
: undefined);
const err = await lint(dirPath, lowVersion, hiVersion, isLatest, expectOnly, tsLocal);
if (err) {
throw new Error(err);
}
}
function assertPathIsInDefinitelyTyped(dirPath: string): void {
const parent = dirname(dirPath);
const types = /^v\d+(\.\d+)?$/.test(basename(dirPath)) ? dirname(parent) : parent;
// TODO: It's not clear whether this assertion makes sense, and it's broken on Azure Pipelines
// Re-enable it later if it makes sense.
// const dt = dirname(types);
// if (basename(dt) !== "DefinitelyTyped" || basename(types) !== "types") {
if (basename(types) !== "types") {
throw new Error("Since this type definition includes a header (a comment starting with `// Type definitions for`), "
+ "assumed this was a DefinitelyTyped package.\n"
+ "But it is not in a `DefinitelyTyped/types/xxx` directory: "
+ dirPath);
}
}
function assertPathIsNotBanned(dirPath: string) {
const basedir = basename(dirPath);
if (/(^|\W)download($|\W)/.test(basedir) &&
basedir !== "download" &&
basedir !== "downloadjs" &&
basedir !== "s3-download-stream") {
// Since npm won't release their banned-words list, we'll have to manually add to this list.
throw new Error(`${dirPath}: Contains the word 'download', which is banned by npm.`);
}
}
function getMinimumTypeScriptVersionFromComment(text: string): AllTypeScriptVersion | undefined {
const match = text.match(/\/\/ (?:Minimum )?TypeScript Version: /);
if (!match) {
return undefined;
}
let line = text.slice(match.index, text.indexOf("\n", match.index));
if (line.endsWith("\r")) {
line = line.slice(0, line.length - 1);
}
return parseTypeScriptVersionLine(line);
}
if (!module.parent) {
main().catch(err => {
console.error(err.stack);
process.exit(1);
});
}

View File

@@ -0,0 +1,229 @@
import { TypeScriptVersion } from "@definitelytyped/typescript-versions";
import { typeScriptPath } from "@definitelytyped/utils";
import assert = require("assert");
import { pathExists } from "fs-extra";
import { dirname, join as joinPaths, normalize } from "path";
import { Configuration, ILinterOptions, Linter } from "tslint";
import * as TsType from "typescript";
type Configuration = typeof Configuration;
type IConfigurationFile = Configuration.IConfigurationFile;
import { getProgram, Options as ExpectOptions } from "./rules/expectRule";
import { readJson, withoutPrefix } from "./util";
export async function lint(
dirPath: string,
minVersion: TsVersion,
maxVersion: TsVersion,
isLatest: boolean,
expectOnly: boolean,
tsLocal: string | undefined): Promise<string | undefined> {
const tsconfigPath = joinPaths(dirPath, "tsconfig.json");
const lintProgram = Linter.createProgram(tsconfigPath);
for (const version of [maxVersion, minVersion]) {
const errors = testDependencies(version, dirPath, lintProgram, tsLocal);
if (errors) { return errors; }
}
const lintOptions: ILinterOptions = {
fix: false,
formatter: "stylish",
};
const linter = new Linter(lintOptions, lintProgram);
const configPath = expectOnly ? joinPaths(__dirname, "..", "dtslint-expect-only.json") : getConfigPath(dirPath);
const config = await getLintConfig(configPath, tsconfigPath, minVersion, maxVersion, tsLocal);
for (const file of lintProgram.getSourceFiles()) {
if (lintProgram.isSourceFileDefaultLibrary(file)) { continue; }
const { fileName, text } = file;
if (!fileName.includes("node_modules")) {
const err = testNoTsIgnore(text) || testNoTslintDisables(text);
if (err) {
const { pos, message } = err;
const place = file.getLineAndCharacterOfPosition(pos);
return `At ${fileName}:${JSON.stringify(place)}: ${message}`;
}
}
// External dependencies should have been handled by `testDependencies`;
// typesVersions should be handled in a separate lint
if (!isExternalDependency(file, dirPath, lintProgram) &&
(!isLatest || !isTypesVersionPath(fileName, dirPath))) {
linter.lint(fileName, text, config);
}
}
const result = linter.getResult();
return result.failures.length ? result.output : undefined;
}
function testDependencies(
version: TsVersion,
dirPath: string,
lintProgram: TsType.Program,
tsLocal: string | undefined,
): string | undefined {
const tsconfigPath = joinPaths(dirPath, "tsconfig.json");
assert(version !== "local" || tsLocal);
const ts: typeof TsType = require(typeScriptPath(version, tsLocal));
const program = getProgram(tsconfigPath, ts, version, lintProgram);
const diagnostics = ts.getPreEmitDiagnostics(program).filter(d => !d.file || isExternalDependency(d.file, dirPath, program));
if (!diagnostics.length) { return undefined; }
const showDiags = ts.formatDiagnostics(diagnostics, {
getCanonicalFileName: f => f,
getCurrentDirectory: () => dirPath,
getNewLine: () => "\n",
});
const message = `Errors in typescript@${version} for external dependencies:\n${showDiags}`;
// Add an edge-case for someone needing to `npm install` in react when they first edit a DT module which depends on it - #226
const cannotFindDepsDiags = diagnostics.find(d => d.code === 2307 && d.messageText.toString().includes("Cannot find module"));
if (cannotFindDepsDiags && cannotFindDepsDiags.file) {
const path = cannotFindDepsDiags.file.fileName;
const typesFolder = dirname(path);
return `
A module look-up failed, this often occurs when you need to run \`npm install\` on a dependent module before you can lint.
Before you debug, first try running:
npm install --prefix ${typesFolder}
Then re-run. Full error logs are below.
${message}`;
} else {
return message;
}
}
export function isExternalDependency(file: TsType.SourceFile, dirPath: string, program: TsType.Program): boolean {
return !startsWithDirectory(file.fileName, dirPath) || program.isSourceFileFromExternalLibrary(file);
}
function normalizePath(file: string) {
// replaces '\' with '/' and forces all DOS drive letters to be upper-case
return normalize(file)
.replace(/\\/g, "/")
.replace(/^[a-z](?=:)/, c => c.toUpperCase());
}
function isTypesVersionPath(fileName: string, dirPath: string) {
const normalFileName = normalizePath(fileName);
const normalDirPath = normalizePath(dirPath);
const subdirPath = withoutPrefix(normalFileName, normalDirPath);
return subdirPath && /^\/ts\d+\.\d/.test(subdirPath);
}
function startsWithDirectory(filePath: string, dirPath: string): boolean {
const normalFilePath = normalizePath(filePath);
const normalDirPath = normalizePath(dirPath).replace(/\/$/, "");
return normalFilePath.startsWith(normalDirPath + "/") || normalFilePath.startsWith(normalDirPath + "\\");
}
interface Err { pos: number; message: string; }
function testNoTsIgnore(text: string): Err | undefined {
const tsIgnore = "ts-ignore";
const pos = text.indexOf(tsIgnore);
return pos === -1 ? undefined : { pos, message: "'ts-ignore' is forbidden." };
}
function testNoTslintDisables(text: string): Err | undefined {
const tslintDisable = "tslint:disable";
let lastIndex = 0;
// eslint-disable-next-line no-constant-condition
while (true) {
const pos = text.indexOf(tslintDisable, lastIndex);
if (pos === -1) {
return undefined;
}
const end = pos + tslintDisable.length;
const nextChar = text.charAt(end);
if (nextChar !== "-" && nextChar !== ":") {
const message = "'tslint:disable' is forbidden. " +
"('tslint:disable:rulename', tslint:disable-line' and 'tslint:disable-next-line' are allowed.)";
return { pos, message };
}
lastIndex = end;
}
}
export async function checkTslintJson(dirPath: string, dt: boolean): Promise<void> {
const configPath = getConfigPath(dirPath);
const shouldExtend = `dtslint/${dt ? "dt" : "dtslint"}.json`;
const validateExtends = (extend: string | string[]) =>
extend === shouldExtend || (!dt && Array.isArray(extend) && extend.some(val => val === shouldExtend));
if (!await pathExists(configPath)) {
if (dt) {
throw new Error(
`On DefinitelyTyped, must include \`tslint.json\` containing \`{ "extends": "${shouldExtend}" }\`.\n` +
"This was inferred as a DefinitelyTyped package because it contains a `// Type definitions for` header.");
}
return;
}
const tslintJson = await readJson(configPath);
if (!validateExtends(tslintJson.extends)) {
throw new Error(`If 'tslint.json' is present, it should extend "${shouldExtend}"`);
}
}
function getConfigPath(dirPath: string): string {
return joinPaths(dirPath, "tslint.json");
}
async function getLintConfig(
expectedConfigPath: string,
tsconfigPath: string,
minVersion: TsVersion,
maxVersion: TsVersion,
tsLocal: string | undefined,
): Promise<IConfigurationFile> {
const configExists = await pathExists(expectedConfigPath);
const configPath = configExists ? expectedConfigPath : joinPaths(__dirname, "..", "dtslint.json");
// Second param to `findConfiguration` doesn't matter, since config path is provided.
const config = Configuration.findConfiguration(configPath, "").results;
if (!config) {
throw new Error(`Could not load config at ${configPath}`);
}
const expectRule = config.rules.get("expect");
if (!expectRule || expectRule.ruleSeverity !== "error") {
throw new Error("'expect' rule should be enabled, else compile errors are ignored");
}
if (expectRule) {
const versionsToTest =
range(minVersion, maxVersion).map(versionName => ({ versionName, path: typeScriptPath(versionName, tsLocal) }));
const expectOptions: ExpectOptions = { tsconfigPath, versionsToTest };
expectRule.ruleArguments = [expectOptions];
}
return config;
}
function range(minVersion: TsVersion, maxVersion: TsVersion): readonly TsVersion[] {
if (minVersion === "local") {
assert(maxVersion === "local");
return ["local"];
}
if (minVersion === TypeScriptVersion.latest) {
assert(maxVersion === TypeScriptVersion.latest);
return [TypeScriptVersion.latest];
}
assert(maxVersion !== "local");
const minIdx = TypeScriptVersion.shipped.indexOf(minVersion);
assert(minIdx >= 0);
if (maxVersion === TypeScriptVersion.latest) {
return [...TypeScriptVersion.shipped.slice(minIdx), TypeScriptVersion.latest];
}
const maxIdx = TypeScriptVersion.shipped.indexOf(maxVersion as TypeScriptVersion);
assert(maxIdx >= minIdx);
return TypeScriptVersion.shipped.slice(minIdx, maxIdx + 1);
}
export type TsVersion = TypeScriptVersion | "local";

View File

@@ -0,0 +1,45 @@
import { renderExpected, validate } from "@definitelytyped/header-parser";
import * as Lint from "tslint";
import * as ts from "typescript";
import { failure, isMainFile } from "../util";
export class Rule extends Lint.Rules.AbstractRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "dt-header",
description: "Ensure consistency of DefinitelyTyped headers.",
optionsDescription: "Not configurable.",
options: null,
type: "functionality",
typescriptOnly: true,
};
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}
function walk(ctx: Lint.WalkContext<void>): void {
const { sourceFile } = ctx;
const { text } = sourceFile;
const lookFor = (search: string, explanation: string) => {
const idx = text.indexOf(search);
if (idx !== -1) {
ctx.addFailureAt(idx, search.length, failure(Rule.metadata.ruleName, explanation));
}
};
if (!isMainFile(sourceFile.fileName, /*allowNested*/ true)) {
lookFor("// Type definitions for", "Header should only be in `index.d.ts` of the root.");
lookFor("// TypeScript Version", "TypeScript version should be specified under header in `index.d.ts`.");
lookFor("// Minimum TypeScript Version", "TypeScript version should be specified under header in `index.d.ts`.");
return;
}
lookFor("// Definitions by: My Self", "Author name should be your name, not the default.");
const error = validate(text);
if (error) {
ctx.addFailureAt(error.index, 1, failure(
Rule.metadata.ruleName,
`Error parsing header. Expected: ${renderExpected(error.expected)}.`));
}
// Don't recurse, we're done.
}

View File

@@ -0,0 +1,402 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import os = require("os");
import { basename, dirname, join, resolve as resolvePath } from "path";
import * as Lint from "tslint";
import * as TsType from "typescript";
import { last } from "../util";
type Program = TsType.Program;
type SourceFile = TsType.SourceFile;
// Based on https://github.com/danvk/typings-checker
const cacheDir = join(os.homedir(), ".dts");
const perfDir = join(os.homedir(), ".dts", "perf");
export class Rule extends Lint.Rules.TypedRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "expect",
description: "Asserts types with $ExpectType and presence of errors with $ExpectError.",
optionsDescription: "Not configurable.",
options: null,
type: "functionality",
typescriptOnly: true,
requiresTypeInfo: true,
};
static FAILURE_STRING_DUPLICATE_ASSERTION = "This line has 2 $ExpectType assertions.";
static FAILURE_STRING_ASSERTION_MISSING_NODE = "Can not match a node to this assertion.";
static FAILURE_STRING_EXPECTED_ERROR = "Expected an error on this line, but found none.";
// TODO: If this naming convention is required by tslint, dump it when switching to eslint
// eslint-disable-next-line @typescript-eslint/naming-convention
static FAILURE_STRING(expectedVersion: string, expectedType: string, actualType: string): string {
return `TypeScript@${expectedVersion} expected type to be:\n ${expectedType}\ngot:\n ${actualType}`;
}
applyWithProgram(sourceFile: SourceFile, lintProgram: Program): Lint.RuleFailure[] {
const options = this.ruleArguments[0] as Options | undefined;
if (!options) {
return this.applyWithFunction(sourceFile, ctx =>
walk(ctx, lintProgram, TsType, "next", /*nextHigherVersion*/ undefined));
}
const { tsconfigPath, versionsToTest } = options;
const getFailures = (
{ versionName, path }: VersionToTest,
nextHigherVersion: string | undefined,
writeOutput: boolean,
) => {
const ts = require(path);
const program = getProgram(tsconfigPath, ts, versionName, lintProgram);
const failures = this.applyWithFunction(sourceFile, ctx => walk(ctx, program, ts, versionName, nextHigherVersion));
if (writeOutput) {
const packageName = basename(dirname(tsconfigPath));
if (!packageName.match(/v\d+/) && !packageName.match(/ts\d\.\d/)) {
const d = {
[packageName]: {
typeCount: (program as any).getTypeCount(),
memory: ts.sys.getMemoryUsage ? ts.sys.getMemoryUsage() : 0,
},
};
if (!existsSync(cacheDir)) {
mkdirSync(cacheDir);
}
if (!existsSync(perfDir)) {
mkdirSync(perfDir);
}
writeFileSync(join(perfDir, `${packageName}.json`), JSON.stringify(d));
}
}
return failures;
};
const maxFailures = getFailures(last(versionsToTest), undefined, /*writeOutput*/ true);
if (maxFailures.length) {
return maxFailures;
}
// As an optimization, check the earliest version for errors;
// assume that if it works on min and max, it works for everything in between.
const minFailures = getFailures(versionsToTest[0], undefined, /*writeOutput*/ false);
if (!minFailures.length) {
return [];
}
// There are no failures in the max version, but there are failures in the min version.
// Work backward to find the newest version with failures.
for (let i = versionsToTest.length - 2; i >= 0; i--) {
const failures = getFailures(versionsToTest[i], options.versionsToTest[i + 1].versionName, /*writeOutput*/ false);
if (failures.length) {
return failures;
}
}
throw new Error(); // unreachable -- at least the min version should have failures.
}
}
export interface Options {
readonly tsconfigPath: string;
// These should be sorted with oldest first.
readonly versionsToTest: readonly VersionToTest[];
}
export interface VersionToTest {
readonly versionName: string;
readonly path: string;
}
const programCache = new WeakMap<Program, Map<string, Program>>();
/** Maps a tslint Program to one created with the version specified in `options`. */
export function getProgram(configFile: string, ts: typeof TsType, versionName: string, lintProgram: Program): Program {
let versionToProgram = programCache.get(lintProgram);
if (versionToProgram === undefined) {
versionToProgram = new Map<string, Program>();
programCache.set(lintProgram, versionToProgram);
}
let newProgram = versionToProgram.get(versionName);
if (newProgram === undefined) {
newProgram = createProgram(configFile, ts);
versionToProgram.set(versionName, newProgram);
}
return newProgram;
}
function createProgram(configFile: string, ts: typeof TsType): Program {
const projectDirectory = dirname(configFile);
const { config } = ts.readConfigFile(configFile, ts.sys.readFile);
const parseConfigHost: TsType.ParseConfigHost = {
fileExists: existsSync,
readDirectory: ts.sys.readDirectory,
readFile: file => readFileSync(file, "utf8"),
useCaseSensitiveFileNames: true,
};
const parsed = ts.parseJsonConfigFileContent(config, parseConfigHost, resolvePath(projectDirectory), {noEmit: true});
const host = ts.createCompilerHost(parsed.options, true);
return ts.createProgram(parsed.fileNames, parsed.options, host);
}
function walk(
ctx: Lint.WalkContext<void>,
program: Program,
ts: typeof TsType,
versionName: string,
nextHigherVersion: string | undefined): void {
const { fileName } = ctx.sourceFile;
const sourceFile = program.getSourceFile(fileName)!;
if (!sourceFile) {
ctx.addFailure(0, 0,
`Program source files differ between TypeScript versions. This may be a dtslint bug.\n` +
`Expected to find a file '${fileName}' present in ${TsType.version}, but did not find it in ts@${versionName}.`);
return;
}
const checker = program.getTypeChecker();
// Don't care about emit errors.
const diagnostics = ts.getPreEmitDiagnostics(program, sourceFile);
if (sourceFile.isDeclarationFile || !/\$Expect(Type|Error)/.test(sourceFile.text)) {
// Normal file.
for (const diagnostic of diagnostics) {
addDiagnosticFailure(diagnostic);
}
return;
}
const { errorLines, typeAssertions, duplicates } = parseAssertions(sourceFile);
for (const line of duplicates) {
addFailureAtLine(line, Rule.FAILURE_STRING_DUPLICATE_ASSERTION);
}
const seenDiagnosticsOnLine = new Set<number>();
for (const diagnostic of diagnostics) {
const line = lineOfPosition(diagnostic.start!, sourceFile);
seenDiagnosticsOnLine.add(line);
if (!errorLines.has(line)) {
addDiagnosticFailure(diagnostic);
}
}
for (const line of errorLines) {
if (!seenDiagnosticsOnLine.has(line)) {
addFailureAtLine(line, Rule.FAILURE_STRING_EXPECTED_ERROR);
}
}
const { unmetExpectations, unusedAssertions } = getExpectTypeFailures(sourceFile, typeAssertions, checker, ts);
for (const { node, expected, actual } of unmetExpectations) {
ctx.addFailureAtNode(node, Rule.FAILURE_STRING(versionName, expected, actual));
}
for (const line of unusedAssertions) {
addFailureAtLine(line, Rule.FAILURE_STRING_ASSERTION_MISSING_NODE);
}
function addDiagnosticFailure(diagnostic: TsType.Diagnostic): void {
const intro = getIntro();
if (diagnostic.file === sourceFile) {
const msg = `${intro}\n${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`;
ctx.addFailureAt(diagnostic.start!, diagnostic.length!, msg);
} else {
ctx.addFailureAt(0, 0, `${intro}\n${fileName}${diagnostic.messageText}`);
}
}
function getIntro(): string {
if (nextHigherVersion === undefined) {
return `TypeScript@${versionName} compile error: `;
} else {
const msg = `Compile error in typescript@${versionName} but not in typescript@${nextHigherVersion}.\n`;
const explain = nextHigherVersion === "next"
? "TypeScript@next features not yet supported."
: `Fix with a comment '// Minimum TypeScript Version: ${nextHigherVersion}' just under the header.`;
return msg + explain;
}
}
function addFailureAtLine(line: number, failure: string): void {
const start = sourceFile.getPositionOfLineAndCharacter(line, 0);
let end = start + sourceFile.text.split("\n")[line].length;
if (sourceFile.text[end - 1] === "\r") {
end--;
}
ctx.addFailure(start, end, `TypeScript@${versionName}: ${failure}`);
}
}
interface Assertions {
/** Lines with an $ExpectError. */
readonly errorLines: ReadonlySet<number>;
/** Map from a line number to the expected type at that line. */
readonly typeAssertions: Map<number, string>;
/** Lines with more than one assertion (these are errors). */
readonly duplicates: readonly number[];
}
function parseAssertions(sourceFile: SourceFile): Assertions {
const errorLines = new Set<number>();
const typeAssertions = new Map<number, string>();
const duplicates: number[] = [];
const { text } = sourceFile;
const commentRegexp = /\/\/(.*)/g;
const lineStarts = sourceFile.getLineStarts();
let curLine = 0;
// eslint-disable-next-line no-constant-condition
while (true) {
const commentMatch = commentRegexp.exec(text);
if (commentMatch === null) {
break;
}
// Match on the contents of that comment so we do nothing in a commented-out assertion,
// i.e. `// foo; // $ExpectType number`
const match = /^ \$Expect((Type (.*))|Error)$/.exec(commentMatch[1]);
if (match === null) {
continue;
}
const line = getLine(commentMatch.index);
if (match[1] === "Error") {
if (errorLines.has(line)) {
duplicates.push(line);
}
errorLines.add(line);
} else {
const expectedType = match[3];
// Don't bother with the assertion if there are 2 assertions on 1 line. Just fail for the duplicate.
if (typeAssertions.delete(line)) {
duplicates.push(line);
} else {
typeAssertions.set(line, expectedType);
}
}
}
return { errorLines, typeAssertions, duplicates };
function getLine(pos: number): number {
// advance curLine to be the line preceding 'pos'
while (lineStarts[curLine + 1] <= pos) {
curLine++;
}
// If this is the first token on the line, it applies to the next line.
// Otherwise, it applies to the text to the left of it.
return isFirstOnLine(text, lineStarts[curLine], pos) ? curLine + 1 : curLine;
}
}
function isFirstOnLine(text: string, lineStart: number, pos: number): boolean {
for (let i = lineStart; i < pos; i++) {
if (text[i] !== " ") {
return false;
}
}
return true;
}
interface ExpectTypeFailures {
/** Lines with an $ExpectType, but a different type was there. */
readonly unmetExpectations: readonly { node: TsType.Node, expected: string, actual: string }[];
/** Lines with an $ExpectType, but no node could be found. */
readonly unusedAssertions: Iterable<number>;
}
function matchReadonlyArray(actual: string, expected: string) {
if (!(/\breadonly\b/.test(actual) && /\bReadonlyArray\b/.test(expected))) return false;
const readonlyArrayRegExp = /\bReadonlyArray</y;
const readonlyModifierRegExp = /\breadonly /y;
// A<ReadonlyArray<B<ReadonlyArray<C>>>>
// A<readonly B<readonly C[]>[]>
let expectedPos = 0;
let actualPos = 0;
let depth = 0;
while (expectedPos < expected.length && actualPos < actual.length) {
const expectedChar = expected.charAt(expectedPos);
const actualChar = actual.charAt(actualPos);
if (expectedChar === actualChar) {
expectedPos++;
actualPos++;
continue;
}
// check for end of readonly array
if (depth > 0 && expectedChar === ">" && actualChar === "[" && actualPos < actual.length - 1 &&
actual.charAt(actualPos + 1) === "]") {
depth--;
expectedPos++;
actualPos += 2;
continue;
}
// check for start of readonly array
readonlyArrayRegExp.lastIndex = expectedPos;
readonlyModifierRegExp.lastIndex = actualPos;
if (readonlyArrayRegExp.test(expected) && readonlyModifierRegExp.test(actual)) {
depth++;
expectedPos += 14; // "ReadonlyArray<".length;
actualPos += 9; // "readonly ".length;
continue;
}
return false;
}
return true;
}
function getExpectTypeFailures(
sourceFile: SourceFile,
typeAssertions: Map<number, string>,
checker: TsType.TypeChecker,
ts: typeof TsType,
): ExpectTypeFailures {
const unmetExpectations: { node: TsType.Node, expected: string, actual: string }[] = [];
// Match assertions to the first node that appears on the line they apply to.
// `forEachChild` isn't available as a method in older TypeScript versions, so must use `ts.forEachChild` instead.
ts.forEachChild(sourceFile, function iterate(node) {
const line = lineOfPosition(node.getStart(sourceFile), sourceFile);
const expected = typeAssertions.get(line);
if (expected !== undefined) {
// https://github.com/Microsoft/TypeScript/issues/14077
if (node.kind === ts.SyntaxKind.ExpressionStatement) {
node = (node as TsType.ExpressionStatement).expression;
}
const type = checker.getTypeAtLocation(getNodeForExpectType(node, ts));
const actual = type
? checker.typeToString(type, /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.NoTruncation)
: "";
if (!expected.split(/\s*\|\|\s*/).some(s => actual === s || matchReadonlyArray(actual, s))) {
unmetExpectations.push({ node, expected, actual });
}
typeAssertions.delete(line);
}
ts.forEachChild(node, iterate);
});
return { unmetExpectations, unusedAssertions: typeAssertions.keys() };
}
function getNodeForExpectType(node: TsType.Node, ts: typeof TsType): TsType.Node {
if (node.kind === ts.SyntaxKind.VariableStatement) { // ts2.0 doesn't have `isVariableStatement`
const { declarationList: { declarations } } = node as TsType.VariableStatement;
if (declarations.length === 1) {
const { initializer } = declarations[0];
if (initializer) {
return initializer;
}
}
}
return node;
}
function lineOfPosition(pos: number, sourceFile: SourceFile): number {
return sourceFile.getLineAndCharacterOfPosition(pos).line;
}

View File

@@ -0,0 +1,96 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { failure } from "../util";
export class Rule extends Lint.Rules.AbstractRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "export-just-namespace",
description:
"Forbid to `export = foo` where `foo` is a namespace and isn't merged with a function/class/type/interface.",
optionsDescription: "Not configurable.",
options: null,
type: "functionality",
typescriptOnly: true,
};
static FAILURE_STRING = failure(
Rule.metadata.ruleName,
"Instead of `export =`-ing a namespace, use the body of the namespace as the module body.");
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}
function walk(ctx: Lint.WalkContext<void>): void {
const { sourceFile: { statements } } = ctx;
const exportEqualsNode = statements.find(isExportEquals) as ts.ExportAssignment | undefined;
if (!exportEqualsNode) {
return;
}
const expr = exportEqualsNode.expression;
if (!ts.isIdentifier(expr)) {
return;
}
const exportEqualsName = expr.text;
if (exportEqualsName && isJustNamespace(statements, exportEqualsName)) {
ctx.addFailureAtNode(exportEqualsNode, Rule.FAILURE_STRING);
}
}
function isExportEquals(node: ts.Node): boolean {
return ts.isExportAssignment(node) && !!node.isExportEquals;
}
/** Returns true if there is a namespace but there are no functions/classes with the name. */
function isJustNamespace(statements: readonly ts.Statement[], exportEqualsName: string): boolean {
let anyNamespace = false;
for (const statement of statements) {
switch (statement.kind) {
case ts.SyntaxKind.ModuleDeclaration:
anyNamespace = anyNamespace || nameMatches((statement as ts.ModuleDeclaration).name);
break;
case ts.SyntaxKind.VariableStatement:
if ((statement as ts.VariableStatement).declarationList.declarations.some(d => nameMatches(d.name))) {
// OK. It's merged with a variable.
return false;
}
break;
case ts.SyntaxKind.FunctionDeclaration:
case ts.SyntaxKind.ClassDeclaration:
case ts.SyntaxKind.TypeAliasDeclaration:
case ts.SyntaxKind.InterfaceDeclaration:
if (nameMatches((statement as ts.DeclarationStatement).name)) {
// OK. It's merged with a function/class/type/interface.
return false;
}
break;
default:
}
}
return anyNamespace;
function nameMatches(nameNode: ts.Node | undefined): boolean {
return nameNode !== undefined && ts.isIdentifier(nameNode) && nameNode.text === exportEqualsName;
}
}
/*
Tests:
OK:
export = foo;
declare namespace foo {}
declare function foo(): void; // or interface, type, class
Error:
export = foo;
declare namespace foo {}
OK: (it's assumed to come from elsewhere)
export = foo;
*/

View File

@@ -0,0 +1,32 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { failure } from "../util";
export class Rule extends Lint.Rules.AbstractRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "no-any-union",
description: "Forbid a union to contain `any`",
optionsDescription: "Not configurable.",
options: null,
type: "functionality",
typescriptOnly: true,
};
static FAILURE_STRING = failure(
Rule.metadata.ruleName,
"Including `any` in a union will override all other members of the union.");
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}
function walk(ctx: Lint.WalkContext<void>): void {
ctx.sourceFile.forEachChild(function recur(node) {
if (node.kind === ts.SyntaxKind.AnyKeyword && ts.isUnionTypeNode(node.parent!)) {
ctx.addFailureAtNode(node, Rule.FAILURE_STRING);
}
node.forEachChild(recur);
});
}

View File

@@ -0,0 +1,39 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { failure } from "../util";
export class Rule extends Lint.Rules.AbstractRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "no-bad-reference",
description: 'Forbid <reference path="../etc"/> in any file, and forbid <reference path> in test files.',
optionsDescription: "Not configurable.",
options: null,
type: "functionality",
typescriptOnly: true,
};
static FAILURE_STRING = failure(
Rule.metadata.ruleName,
"Don't use <reference path> to reference another package. Use an import or <reference types> instead.");
static FAILURE_STRING_REFERENCE_IN_TEST = failure(
Rule.metadata.ruleName,
"Don't use <reference path> in test files. Use <reference types> or include the file in 'tsconfig.json'.");
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}
function walk(ctx: Lint.WalkContext<void>): void {
const { sourceFile } = ctx;
for (const ref of sourceFile.referencedFiles) {
if (sourceFile.isDeclarationFile) {
if (ref.fileName.startsWith("..")) {
ctx.addFailure(ref.pos, ref.end, Rule.FAILURE_STRING);
}
} else {
ctx.addFailure(ref.pos, ref.end, Rule.FAILURE_STRING_REFERENCE_IN_TEST);
}
}
}

View File

@@ -0,0 +1,32 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { failure } from "../util";
export class Rule extends Lint.Rules.AbstractRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "no-const-enum",
description: "Forbid `const enum`",
optionsDescription: "Not configurable.",
options: null,
type: "functionality",
typescriptOnly: true,
};
static FAILURE_STRING = failure(
Rule.metadata.ruleName,
"Use of `const enum`s is forbidden.");
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}
function walk(ctx: Lint.WalkContext<void>): void {
ctx.sourceFile.forEachChild(function recur(node) {
if (ts.isEnumDeclaration(node) && node.modifiers && node.modifiers.some(m => m.kind === ts.SyntaxKind.ConstKeyword)) {
ctx.addFailureAtNode(node.name, Rule.FAILURE_STRING);
}
node.forEachChild(recur);
});
}

View File

@@ -0,0 +1,50 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { failure } from "../util";
export class Rule extends Lint.Rules.AbstractRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "no-dead-reference",
description: "Ensures that all `/// <reference>` comments go at the top of the file.",
rationale: "style",
optionsDescription: "Not configurable.",
options: null,
type: "functionality",
typescriptOnly: true,
};
static FAILURE_STRING = failure(
Rule.metadata.ruleName,
"`/// <reference>` directive must be at top of file to take effect.");
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}
function walk(ctx: Lint.WalkContext<void>): void {
const { sourceFile: { statements, text } } = ctx;
if (!statements.length) {
return;
}
// 'm' flag makes it multiline, so `^` matches the beginning of any line.
// 'g' flag lets us set rgx.lastIndex
const rgx = /^\s*(\/\/\/ <reference)/mg;
// Start search at the first statement. (`/// <reference>` before that is OK.)
rgx.lastIndex = statements[0].getStart();
// eslint-disable-next-line no-constant-condition
while (true) {
const match = rgx.exec(text);
if (match === null) {
break;
}
const length = match[1].length;
const start = match.index + match[0].length - length;
ctx.addFailureAt(start, length, Rule.FAILURE_STRING);
}
}

View File

@@ -0,0 +1,39 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { failure, getCommonDirectoryName } from "../util";
export class Rule extends Lint.Rules.TypedRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "no-declare-current-package",
description: "Don't use an ambient module declaration of the current package; use a normal module.",
optionsDescription: "Not configurable.",
options: null,
type: "functionality",
typescriptOnly: true,
};
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
if (!sourceFile.isDeclarationFile) {
return [];
}
const packageName = getCommonDirectoryName(program.getRootFileNames());
return this.applyWithFunction(sourceFile, ctx => walk(ctx, packageName));
}
}
function walk(ctx: Lint.WalkContext<void>, packageName: string): void {
for (const statement of ctx.sourceFile.statements) {
if (ts.isModuleDeclaration(statement) && ts.isStringLiteral(statement.name)) {
const { text } = statement.name;
if (text === packageName || text.startsWith(`${packageName}/`)) {
const preferred = text === packageName ? '"index.d.ts"' : `"${text}.d.ts" or "${text}/index.d.ts`;
ctx.addFailureAtNode(statement.name, failure(
Rule.metadata.ruleName,
`Instead of declaring a module with \`declare module "${text}"\`, ` +
`write its contents in directly in ${preferred}.`));
}
}
}
}

View File

@@ -0,0 +1,51 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { eachModuleStatement, failure, getModuleDeclarationStatements } from "../util";
export class Rule extends Lint.Rules.TypedRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "no-import-default-of-export-equals",
description: "Forbid a default import to reference an `export =` module.",
optionsDescription: "Not configurable.",
options: null,
type: "functionality",
typescriptOnly: true,
};
// eslint-disable-next-line @typescript-eslint/naming-convention
static FAILURE_STRING(importName: string, moduleName: string): string {
return failure(
Rule.metadata.ruleName,
`The module ${moduleName} uses \`export = \`. Import with \`import ${importName} = require(${moduleName})\`.`);
}
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, ctx => walk(ctx, program.getTypeChecker()));
}
}
function walk(ctx: Lint.WalkContext<void>, checker: ts.TypeChecker): void {
eachModuleStatement(ctx.sourceFile, statement => {
if (!ts.isImportDeclaration(statement)) {
return;
}
const defaultName = statement.importClause && statement.importClause.name;
if (!defaultName) {
return;
}
const sym = checker.getSymbolAtLocation(statement.moduleSpecifier);
if (sym && sym.declarations && sym.declarations.some(d => {
const statements = getStatements(d);
return statements !== undefined && statements.some(s => ts.isExportAssignment(s) && !!s.isExportEquals);
})) {
ctx.addFailureAtNode(defaultName, Rule.FAILURE_STRING(defaultName.text, statement.moduleSpecifier.getText()));
}
});
}
function getStatements(decl: ts.Declaration): readonly ts.Statement[] | undefined {
return ts.isSourceFile(decl) ? decl.statements
: ts.isModuleDeclaration(decl) ? getModuleDeclarationStatements(decl)
: undefined;
}

View File

@@ -0,0 +1,36 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { failure } from "../util";
export class Rule extends Lint.Rules.TypedRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "no-outside-dependencies",
description: "Don't import things in `DefinitelyTyped/node_modules`.",
optionsDescription: "Not configurable.",
options: null,
type: "functionality",
typescriptOnly: true,
};
applyWithProgram(_sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
if (seenPrograms.has(program)) {
return [];
}
seenPrograms.add(program);
const failures: Lint.RuleFailure[] = [];
for (const sourceFile of program.getSourceFiles()) {
const { fileName } = sourceFile;
if (fileName.includes("/DefinitelyTyped/node_modules/") && !program.isSourceFileDefaultLibrary(sourceFile)) {
const msg = failure(
Rule.metadata.ruleName,
`File ${fileName} comes from a \`node_modules\` but is not declared in this type's \`package.json\`. `);
failures.push(new Lint.RuleFailure(sourceFile, 0, 1, msg, Rule.metadata.ruleName));
}
}
return failures;
}
}
const seenPrograms = new WeakSet<ts.Program>();

View File

@@ -0,0 +1,86 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { failure } from "../util";
export class Rule extends Lint.Rules.AbstractRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "no-padding",
description: "Forbids a blank line after `(` / `[` / `{`, or before `)` / `]` / `}`.",
optionsDescription: "Not configurable.",
options: null,
type: "style",
typescriptOnly: true,
};
// eslint-disable-next-line @typescript-eslint/naming-convention
static FAILURE_STRING(kind: "before" | "after", token: ts.SyntaxKind) {
return failure(
Rule.metadata.ruleName,
`Don't leave a blank line ${kind} '${ts.tokenToString(token)}'.`);
}
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}
function walk(ctx: Lint.WalkContext<void>): void {
const { sourceFile } = ctx;
function fail(kind: "before" | "after", child: ts.Node): void {
ctx.addFailureAtNode(child, Rule.FAILURE_STRING(kind, child.kind));
}
sourceFile.forEachChild(function cb(node) {
const children = node.getChildren();
for (let i = 0; i < children.length; i++) {
const child = children[i];
switch (child.kind) {
case ts.SyntaxKind.OpenParenToken:
case ts.SyntaxKind.OpenBracketToken:
case ts.SyntaxKind.OpenBraceToken:
if (i < children.length - 1 && blankLineInBetween(child.getEnd(), children[i + 1].getStart())) {
fail("after", child);
}
break;
case ts.SyntaxKind.CloseParenToken:
case ts.SyntaxKind.CloseBracketToken:
case ts.SyntaxKind.CloseBraceToken:
if (i > 0 && blankLineInBetween(child.getStart() - 1, children[i - 1].getEnd() - 1)) {
fail("before", child);
}
break;
default:
cb(child);
}
}
});
// Looks for two newlines (with nothing else in between besides whitespace)
function blankLineInBetween(start: number, end: number): boolean {
const step = start < end ? 1 : -1;
let seenLine = false;
for (let i = start; i !== end; i += step) {
switch (sourceFile.text[i]) {
case "\n":
if (seenLine) {
return true;
} else {
seenLine = true;
}
break;
case " ": case "\t": case "\r":
break;
default:
return false;
}
}
return false;
}
}

View File

@@ -0,0 +1,253 @@
// Fixes temporarily moved here until they are published by tslint.
import assert = require("assert");
import * as Lint from "tslint";
import * as ts from "typescript";
export class Rule extends Lint.Rules.AbstractRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "no-redundant-jsdoc",
description: "Forbids JSDoc which duplicates TypeScript functionality.",
optionsDescription: "Not configurable.",
options: null,
optionExamples: [true],
type: "style",
typescriptOnly: true,
};
static readonly FAILURE_STRING_REDUNDANT_TYPE =
"Type annotation in JSDoc is redundant in TypeScript code.";
static readonly FAILURE_STRING_EMPTY =
"JSDoc comment is empty.";
// eslint-disable-next-line @typescript-eslint/naming-convention
static FAILURE_STRING_REDUNDANT_TAG(tagName: string): string {
return `JSDoc tag '@${tagName}' is redundant in TypeScript code.`;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
static FAILURE_STRING_NO_COMMENT(tagName: string): string {
return `'@${tagName}' is redundant in TypeScript code if it has no description.`;
}
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}
function walk(ctx: Lint.WalkContext<void>): void {
const { sourceFile } = ctx;
// Intentionally exclude EndOfFileToken: it can have JSDoc, but it is only relevant in JavaScript files
return sourceFile.statements.forEach(function cb(node: ts.Node): void {
if (node.kind !== ts.SyntaxKind.EndOfFileToken && (ts as any).hasJSDocNodes(node)) {
for (const jd of (node as any).jsDoc) {
const { tags } = jd as ts.JSDoc;
if (tags === undefined || tags.length === 0) {
if (jd.comment === undefined) {
ctx.addFailureAtNode(
jd,
Rule.FAILURE_STRING_EMPTY,
Lint.Replacement.deleteFromTo(jd.getStart(sourceFile), jd.getEnd()));
}
} else {
for (const tag of tags) {
checkTag(tag);
}
}
}
}
return ts.forEachChild(node, cb);
});
function checkTag(tag: ts.JSDocTag): void {
const jsdocSeeTag = (ts.SyntaxKind as any).JSDocSeeTag || 0;
const jsdocDeprecatedTag = (ts.SyntaxKind as any).JSDocDeprecatedTag || 0;
switch (tag.kind) {
case jsdocSeeTag:
case jsdocDeprecatedTag:
case ts.SyntaxKind.JSDocAuthorTag:
// @deprecated and @see always have meaning
break;
case ts.SyntaxKind.JSDocTag: {
const { tagName } = tag;
const { text } = tagName;
// Allow "default" in an ambient context (since you can't write an initializer in an ambient context)
if (redundantTags.has(text) && !(text === "default" && isInAmbientContext(tag))) {
ctx.addFailureAtNode(tagName, Rule.FAILURE_STRING_REDUNDANT_TAG(text), removeTag(tag, sourceFile));
}
break;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore (fallthrough)
case ts.SyntaxKind.JSDocTemplateTag:
if (tag.comment !== "") {
break;
}
// falls through
case ts.SyntaxKind.JSDocPublicTag:
case ts.SyntaxKind.JSDocPrivateTag:
case ts.SyntaxKind.JSDocProtectedTag:
case ts.SyntaxKind.JSDocClassTag:
case ts.SyntaxKind.JSDocTypeTag:
case ts.SyntaxKind.JSDocTypedefTag:
case ts.SyntaxKind.JSDocReadonlyTag:
case ts.SyntaxKind.JSDocPropertyTag:
case ts.SyntaxKind.JSDocAugmentsTag:
case ts.SyntaxKind.JSDocImplementsTag:
case ts.SyntaxKind.JSDocCallbackTag:
case ts.SyntaxKind.JSDocThisTag:
case ts.SyntaxKind.JSDocEnumTag:
// Always redundant
ctx.addFailureAtNode(
tag.tagName,
Rule.FAILURE_STRING_REDUNDANT_TAG(tag.tagName.text),
removeTag(tag, sourceFile));
break;
case ts.SyntaxKind.JSDocReturnTag:
case ts.SyntaxKind.JSDocParameterTag: {
const { typeExpression, comment } = tag as ts.JSDocReturnTag | ts.JSDocParameterTag;
const noComment = comment === "";
if (typeExpression !== undefined) {
// If noComment, we will just completely remove it in the other fix
const fix = noComment ? undefined : removeTypeExpression(typeExpression, sourceFile);
ctx.addFailureAtNode(typeExpression, Rule.FAILURE_STRING_REDUNDANT_TYPE, fix);
}
if (noComment) {
// Redundant if no documentation
ctx.addFailureAtNode(
tag.tagName,
Rule.FAILURE_STRING_NO_COMMENT(tag.tagName.text),
removeTag(tag, sourceFile));
}
break;
}
default:
throw new Error(`Unexpected tag kind: ${ts.SyntaxKind[tag.kind]}`);
}
}
}
function removeTag(tag: ts.JSDocTag, sourceFile: ts.SourceFile): Lint.Replacement | undefined {
const { text } = sourceFile;
const jsdoc = tag.parent;
if (jsdoc.kind === ts.SyntaxKind.JSDocTypeLiteral) {
return undefined;
}
if (jsdoc.comment === undefined && jsdoc.tags!.length === 1) {
// This is the only tag -- remove the whole comment
return Lint.Replacement.deleteFromTo(jsdoc.getStart(sourceFile), jsdoc.getEnd());
}
let start = tag.getStart(sourceFile);
assert(text[start] === "@");
start--;
while (ts.isWhiteSpaceSingleLine(text.charCodeAt(start))) {
start--;
}
if (text[start] !== "*") {
return undefined;
}
let end = tag.getEnd();
// For some tags, like `@param`, `end` will be the start of the next tag.
// For some tags, like `@name`, `end` will be before the start of the comment.
// And `@typedef` doesn't end until the last `@property` tag attached to it ends.
switch (tag.tagName.text) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore (fallthrough)
case "param": {
const { isBracketed, isNameFirst, typeExpression } = tag as ts.JSDocParameterTag;
if (!isBracketed && !(isNameFirst && typeExpression !== undefined)) {
break;
}
// falls through
}
// eslint-disable-next-line no-fallthrough
case "name":
case "return":
case "returns":
case "interface":
case "default":
case "memberof":
case "memberOf":
case "method":
case "type":
case "class":
case "property":
case "function":
end--; // Might end with "\n" (test with just `@return` with no comment or type)
// For some reason, for "@name", "end" is before the start of the comment part of the tag.
// Also for "param" if the name is optional as in `@param {number} [x]`
while (!ts.isLineBreak(text.charCodeAt(end))) {
end++;
}
end++;
}
while (ts.isWhiteSpaceSingleLine(text.charCodeAt(end))) {
end++;
}
if (text[end] !== "*") {
return undefined;
}
return Lint.Replacement.deleteFromTo(start, end);
}
function removeTypeExpression(
typeExpression: ts.JSDocTypeExpression,
sourceFile: ts.SourceFile,
): Lint.Replacement | undefined {
const start = typeExpression.getStart(sourceFile);
let end = typeExpression.getEnd();
const { text } = sourceFile;
if (text[start] !== "{" || text[end - 1] !== "}") {
// TypeScript parser messed up -- give up
return undefined;
}
if (ts.isWhiteSpaceSingleLine(text.charCodeAt(end))) {
end++;
}
return Lint.Replacement.deleteFromTo(start, end);
}
// TODO: improve once https://github.com/Microsoft/TypeScript/pull/17831 is in
function isInAmbientContext(node: ts.Node): boolean {
return ts.isSourceFile(node)
? node.isDeclarationFile
: Lint.hasModifier(node.modifiers, ts.SyntaxKind.DeclareKeyword) || isInAmbientContext(node.parent!);
}
const redundantTags = new Set([
"abstract",
"access",
"class",
"constant",
"constructs",
"default",
"enum",
"export",
"exports",
"function",
"global",
"inherits",
"interface",
"instance",
"member",
"method",
"memberof",
"memberOf",
"mixes",
"mixin",
"module",
"name",
"namespace",
"override",
"property",
"requires",
"static",
"this",
]);

View File

@@ -0,0 +1,52 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { failure } from "../util";
export class Rule extends Lint.Rules.TypedRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "no-relative-import-in-test",
description: "Forbids test (non-declaration) files to use relative imports.",
optionsDescription: "Not configurable.",
options: null,
type: "functionality",
typescriptOnly: false,
};
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
if (sourceFile.isDeclarationFile) {
return [];
}
return this.applyWithFunction(sourceFile, ctx => walk(ctx, program.getTypeChecker()));
}
}
const failureMessage = failure(
Rule.metadata.ruleName,
"Test file should not use a relative import. Use a global import as if this were a user of the package.");
function walk(ctx: Lint.WalkContext<void>, checker: ts.TypeChecker): void {
const { sourceFile } = ctx;
for (const i of sourceFile.imports) {
if (i.text.startsWith(".")) {
const moduleSymbol = checker.getSymbolAtLocation(i);
if (!moduleSymbol || !moduleSymbol.declarations) {
continue;
}
for (const decl of moduleSymbol.declarations) {
if (decl.kind === ts.SyntaxKind.SourceFile && (decl as ts.SourceFile).isDeclarationFile) {
ctx.addFailureAtNode(i, failureMessage);
}
}
}
}
}
declare module "typescript" {
interface SourceFile {
imports: readonly ts.StringLiteral[];
}
}

View File

@@ -0,0 +1,36 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { failure, getCommonDirectoryName } from "../util";
export class Rule extends Lint.Rules.TypedRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "no-self-import",
description: "Forbids declaration files to import the current package using a global import.",
optionsDescription: "Not configurable.",
options: null,
type: "functionality",
typescriptOnly: false,
};
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
if (!sourceFile.isDeclarationFile) {
return [];
}
const name = getCommonDirectoryName(program.getRootFileNames());
return this.applyWithFunction(sourceFile, ctx => walk(ctx, name));
}
}
const failureMessage = failure(
Rule.metadata.ruleName,
"Declaration file should not use a global import of itself. Use a relative import.");
function walk(ctx: Lint.WalkContext<void>, packageName: string): void {
for (const i of ctx.sourceFile.imports) {
if (i.text === packageName || i.text.startsWith(packageName + "/")) {
ctx.addFailureAtNode(i, failureMessage);
}
}
}

View File

@@ -0,0 +1,54 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { failure } from "../util";
export class Rule extends Lint.Rules.AbstractRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "no-single-declare-module",
description: "Don't use an ambient module declaration if there's just one -- write it as a normal module.",
rationale: "Cuts down on nesting",
optionsDescription: "Not configurable.",
options: null,
type: "style",
typescriptOnly: true,
};
static FAILURE_STRING = failure(
Rule.metadata.ruleName,
"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.");
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}
function walk(ctx: Lint.WalkContext<void>): void {
const { sourceFile } = ctx;
// If it's an external module, any module declarations inside are augmentations.
if (ts.isExternalModule(sourceFile)) {
return;
}
let moduleDecl: ts.ModuleDeclaration | undefined;
for (const statement of sourceFile.statements) {
if (ts.isModuleDeclaration(statement) && ts.isStringLiteral(statement.name)) {
if (statement.name.text.indexOf('*') !== -1) {
// Ignore wildcard module declarations
return;
}
if (moduleDecl === undefined) {
moduleDecl = statement;
} else {
// Has more than 1 declaration
return;
}
}
}
if (moduleDecl) {
ctx.addFailureAtNode(moduleDecl, Rule.FAILURE_STRING);
}
}

View File

@@ -0,0 +1,31 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { failure } from "../util";
export class Rule extends Lint.Rules.AbstractRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "no-single-element-tuple-type",
description: "Forbids `[T]`, which should be `T[]`.",
optionsDescription: "Not configurable.",
options: null,
type: "functionality",
typescriptOnly: true,
};
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}
function walk(ctx: Lint.WalkContext<void>): void {
const { sourceFile } = ctx;
sourceFile.forEachChild(function cb(node) {
if (ts.isTupleTypeNode(node) && (node.elements ?? (node as any).elementTypes).length === 1) {
ctx.addFailureAtNode(node, failure(
Rule.metadata.ruleName,
"Type [T] is a single-element tuple type. You probably meant T[]."));
}
node.forEachChild(cb);
});
}

View File

@@ -0,0 +1,121 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { failure } from "../util";
export class Rule extends Lint.Rules.TypedRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "no-unnecessary-generics",
description: "Forbids signatures using a generic parameter only once.",
optionsDescription: "Not configurable.",
options: null,
type: "style",
typescriptOnly: true,
};
// eslint-disable-next-line @typescript-eslint/naming-convention
static FAILURE_STRING(typeParameter: string) {
return failure(
Rule.metadata.ruleName,
`Type parameter ${typeParameter} is used only once.`);
}
// eslint-disable-next-line @typescript-eslint/naming-convention
static FAILURE_STRING_NEVER(typeParameter: string) {
return failure(
Rule.metadata.ruleName,
`Type parameter ${typeParameter} is never used.`);
}
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, ctx => walk(ctx, program.getTypeChecker()));
}
}
function walk(ctx: Lint.WalkContext<void>, checker: ts.TypeChecker): void {
const { sourceFile } = ctx;
sourceFile.forEachChild(function cb(node) {
if (ts.isFunctionLike(node)) {
checkSignature(node);
}
node.forEachChild(cb);
});
function checkSignature(sig: ts.SignatureDeclaration) {
if (!sig.typeParameters) {
return;
}
for (const tp of sig.typeParameters) {
const typeParameter = tp.name.text;
const res = getSoleUse(sig, assertDefined(checker.getSymbolAtLocation(tp.name)), checker);
switch (res.type) {
case "ok":
break;
case "sole":
ctx.addFailureAtNode(res.soleUse, Rule.FAILURE_STRING(typeParameter));
break;
case "never":
ctx.addFailureAtNode(tp, Rule.FAILURE_STRING_NEVER(typeParameter));
break;
default:
assertNever(res);
}
}
}
}
type Result =
| { type: "ok" | "never" }
| { type: "sole", soleUse: ts.Identifier };
function getSoleUse(sig: ts.SignatureDeclaration, typeParameterSymbol: ts.Symbol, checker: ts.TypeChecker): Result {
const exit = {};
let soleUse: ts.Identifier | undefined;
try {
if (sig.typeParameters) {
for (const tp of sig.typeParameters) {
if (tp.constraint) {
recur(tp.constraint);
}
}
}
for (const param of sig.parameters) {
if (param.type) {
recur(param.type);
}
}
if (sig.type) {
recur(sig.type);
}
} catch (err) {
if (err === exit) {
return { type: "ok" };
}
throw err;
}
return soleUse ? { type: "sole", soleUse } : { type: "never" };
function recur(node: ts.TypeNode): void {
if (ts.isIdentifier(node)) {
if (checker.getSymbolAtLocation(node) === typeParameterSymbol) {
if (soleUse === undefined) {
soleUse = node;
} else {
throw exit;
}
}
} else {
node.forEachChild(recur);
}
}
}
function assertDefined<T>(value: T | undefined): T {
if (value === undefined) {
throw new Error("unreachable");
}
return value;
}
function assertNever(_: never) {
throw new Error("unreachable");
}

View File

@@ -0,0 +1,31 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { failure } from "../util";
// Same functionality as https://github.com/palantir/tslint/pull/1654, but simpler implementation.
// Remove when that PR is in.
export class Rule extends Lint.Rules.AbstractRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "no-useless-files",
description: "Forbids files with no content.",
optionsDescription: "Not configurable.",
options: null,
type: "functionality",
typescriptOnly: false,
};
static FAILURE_STRING = failure(
Rule.metadata.ruleName,
"File has no content.");
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
const { statements, referencedFiles, typeReferenceDirectives } = sourceFile;
if (statements.length + referencedFiles.length + typeReferenceDirectives.length !== 0) {
return [];
}
return [new Lint.RuleFailure(sourceFile, 0, 1, Rule.FAILURE_STRING, this.ruleName)];
}
}

View File

@@ -0,0 +1,309 @@
import {
CheckOptions as CriticOptions,
CriticError,
defaultErrors,
dtsCritic as critic,
ErrorKind,
ExportErrorKind,
Mode,
parseExportErrorKind,
parseMode } from "@definitelytyped/dts-critic";
import * as Lint from "tslint";
import * as ts from "typescript";
import { addSuggestion } from "../suggestions";
import { failure, isMainFile } from "../util";
/** Options as parsed from the rule configuration. */
type ConfigOptions = {
mode: Mode.NameOnly,
singleLine?: boolean,
} | {
mode: Mode.Code,
errors: [ExportErrorKind, boolean][],
singleLine?: boolean,
};
type Options = CriticOptions & { singleLine?: boolean };
const defaultOptions: ConfigOptions = {
mode: Mode.NameOnly,
};
export class Rule extends Lint.Rules.AbstractRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "npm-naming",
description: "Ensure that package name and DefinitelyTyped header match npm package info.",
optionsDescription: `An object with a \`mode\` property should be provided.
If \`mode\` is '${Mode.Code}', then option \`errors\` can be provided.
\`errors\` should be an array specifying which code checks should be enabled or disabled.`,
options: {
oneOf: [
{
type: "object",
properties: {
"mode": {
type: "string",
enum: [Mode.NameOnly],
},
"single-line": {
description: "Whether to print error messages in a single line. Used for testing.",
type: "boolean",
},
"required": ["mode"],
},
},
{
type: "object",
properties: {
"mode": {
type: "string",
enum: [Mode.Code],
},
"errors": {
type: "array",
items: {
type: "array",
items: [
{ description: "Name of the check.",
type: "string",
enum: [ErrorKind.NeedsExportEquals, ErrorKind.NoDefaultExport] as ExportErrorKind[],
},
{
description: "Whether the check is enabled or disabled.",
type: "boolean",
},
],
minItems: 2,
maxItems: 2,
},
default: [],
},
"single-line": {
description: "Whether to print error messages in a single line. Used for testing.",
type: "boolean",
},
"required": ["mode"],
},
},
],
},
optionExamples: [
true,
[true, { mode: Mode.NameOnly }],
[
true,
{
mode: Mode.Code,
errors: [[ErrorKind.NeedsExportEquals, true], [ErrorKind.NoDefaultExport, false]],
},
],
],
type: "functionality",
typescriptOnly: true,
};
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk, toCriticOptions(parseOptions(this.ruleArguments)));
}
}
function parseOptions(args: unknown[]): ConfigOptions {
if (args.length === 0) {
return defaultOptions;
}
const arg = args[0] as { [prop: string]: unknown } | null | undefined;
// eslint-disable-next-line eqeqeq
if (arg == null) {
return defaultOptions;
}
if (!arg.mode || typeof arg.mode !== "string") {
return defaultOptions;
}
const mode = parseMode(arg.mode);
if (!mode) {
return defaultOptions;
}
const singleLine = !!arg["single-line"];
switch (mode) {
case Mode.NameOnly:
return { mode, singleLine };
case Mode.Code:
if (!arg.errors || !Array.isArray(arg.errors)) {
return { mode, errors: [], singleLine };
}
return { mode, errors: parseEnabledErrors(arg.errors), singleLine };
}
}
function parseEnabledErrors(errors: unknown[]): [ExportErrorKind, boolean][] {
const enabledChecks: [ExportErrorKind, boolean][] = [];
for (const tuple of errors) {
if (Array.isArray(tuple)
&& tuple.length === 2
&& typeof tuple[0] === "string"
&& typeof tuple[1] === "boolean") {
const error = parseExportErrorKind(tuple[0]);
if (error) {
enabledChecks.push([error, tuple[1]]);
}
}
}
return enabledChecks;
}
function toCriticOptions(options: ConfigOptions): Options {
switch (options.mode) {
case Mode.NameOnly:
return options;
case Mode.Code:
return { ...options, errors: new Map(options.errors) };
}
}
function walk(ctx: Lint.WalkContext<CriticOptions>): void {
const { sourceFile } = ctx;
const { text } = sourceFile;
const lookFor = (search: string, explanation: string) => {
const idx = text.indexOf(search);
if (idx !== -1) {
ctx.addFailureAt(idx, search.length, failure(Rule.metadata.ruleName, explanation));
}
};
if (isMainFile(sourceFile.fileName, /*allowNested*/ false)) {
try {
const optionsWithSuggestions = toOptionsWithSuggestions(ctx.options);
const diagnostics = critic(sourceFile.fileName, /* sourcePath */ undefined, optionsWithSuggestions);
const errors = filterErrors(diagnostics, ctx);
for (const error of errors) {
switch (error.kind) {
case ErrorKind.NoMatchingNpmPackage:
case ErrorKind.NoMatchingNpmVersion:
case ErrorKind.NonNpmHasMatchingPackage:
lookFor("// Type definitions for", errorMessage(error, ctx.options));
break;
case ErrorKind.DtsPropertyNotInJs:
case ErrorKind.DtsSignatureNotInJs:
case ErrorKind.JsPropertyNotInDts:
case ErrorKind.JsSignatureNotInDts:
case ErrorKind.NeedsExportEquals:
case ErrorKind.NoDefaultExport:
if (error.position) {
ctx.addFailureAt(
error.position.start,
error.position.length,
failure(Rule.metadata.ruleName, errorMessage(error, ctx.options)));
} else {
ctx.addFailure(0, 1, failure(Rule.metadata.ruleName, errorMessage(error, ctx.options)));
}
break;
}
}
} catch (e) {
// We're ignoring exceptions.
}
}
// Don't recur, we're done.
}
const enabledSuggestions: ExportErrorKind[] = [
ErrorKind.JsPropertyNotInDts,
ErrorKind.JsSignatureNotInDts,
];
function toOptionsWithSuggestions(options: CriticOptions): CriticOptions {
if (options.mode === Mode.NameOnly) {
return options;
}
const optionsWithSuggestions = { mode: options.mode, errors: new Map(options.errors) };
enabledSuggestions.forEach(err => optionsWithSuggestions.errors.set(err, true));
return optionsWithSuggestions;
}
function filterErrors(diagnostics: CriticError[], ctx: Lint.WalkContext<Options>): CriticError[] {
const errors: CriticError[] = [];
diagnostics.forEach(diagnostic => {
if (isSuggestion(diagnostic, ctx.options)) {
addSuggestion(ctx, diagnostic.message, diagnostic.position?.start, diagnostic.position?.length);
} else {
errors.push(diagnostic);
}
});
return errors;
}
function isSuggestion(diagnostic: CriticError, options: Options): boolean {
return options.mode === Mode.Code
&& (enabledSuggestions as ErrorKind[]).includes(diagnostic.kind)
&& !(options.errors as Map<ErrorKind, boolean>).get(diagnostic.kind);
}
function tslintDisableOption(error: ErrorKind): string {
switch (error) {
case ErrorKind.NoMatchingNpmPackage:
case ErrorKind.NoMatchingNpmVersion:
case ErrorKind.NonNpmHasMatchingPackage:
return `false`;
case ErrorKind.NoDefaultExport:
case ErrorKind.NeedsExportEquals:
case ErrorKind.JsSignatureNotInDts:
case ErrorKind.JsPropertyNotInDts:
case ErrorKind.DtsSignatureNotInJs:
case ErrorKind.DtsPropertyNotInJs:
return JSON.stringify([true, { mode: Mode.Code, errors: [[error, false]]}]);
}
}
function errorMessage(error: CriticError, opts: Options): string {
const message = error.message +
`\nIf 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": ${tslintDisableOption(error.kind)}
`;
if (opts.singleLine) {
return message.replace(/(\r\n|\n|\r|\t)/gm, " ");
}
return message;
}
/**
* Given npm-naming lint failures, returns a rule configuration that prevents such failures.
*/
export function disabler(failures: Lint.IRuleFailureJson[]): false | [true, ConfigOptions] {
const disabledErrors = new Set<ExportErrorKind>();
for (const ruleFailure of failures) {
if (ruleFailure.ruleName !== "npm-naming") {
throw new Error(`Expected failures of rule "npm-naming", found failures of rule ${ruleFailure.ruleName}.`);
}
const message = ruleFailure.failure;
// Name errors.
if (message.includes("must have a matching npm package")
|| message.includes("must match a version that exists on npm")
|| message.includes("conflicts with the existing npm package")) {
return false;
}
// Code errors.
if (message.includes("declaration should use 'export =' syntax")) {
disabledErrors.add(ErrorKind.NeedsExportEquals);
} else if (message.includes("declaration specifies 'export default' but the JavaScript source \
does not mention 'default' anywhere")) {
disabledErrors.add(ErrorKind.NoDefaultExport);
} else {
return [true, { mode: Mode.NameOnly }];
}
}
if ((defaultErrors as ExportErrorKind[]).every(error => disabledErrors.has(error))) {
return [true, { mode: Mode.NameOnly }];
}
const errors: [ExportErrorKind, boolean][] = [];
disabledErrors.forEach(error => errors.push([error, false]));
return [true, { mode: Mode.Code, errors }];
}

View File

@@ -0,0 +1,35 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { eachModuleStatement, failure } from "../util";
export class Rule extends Lint.Rules.AbstractRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "prefer-declare-function",
description: "Forbids `export const x = () => void`.",
optionsDescription: "Not configurable.",
options: null,
type: "style",
typescriptOnly: true,
};
static FAILURE_STRING = failure(
Rule.metadata.ruleName,
"Use a function declaration instead of a variable of function type.");
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}
function walk(ctx: Lint.WalkContext<void>): void {
eachModuleStatement(ctx.sourceFile, statement => {
if (ts.isVariableStatement(statement)) {
for (const varDecl of statement.declarationList.declarations) {
if (varDecl.type !== undefined && varDecl.type.kind === ts.SyntaxKind.FunctionType) {
ctx.addFailureAtNode(varDecl, Rule.FAILURE_STRING);
}
}
}
});
}

View File

@@ -0,0 +1,40 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { failure } from "../util";
export class Rule extends Lint.Rules.AbstractRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "redundant-undefined",
description: "Forbids optional parameters to include an explicit `undefined` in their type; requires it in optional properties.",
optionsDescription: "Not configurable.",
options: null,
type: "style",
typescriptOnly: true,
};
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}
function walk(ctx: Lint.WalkContext<void>): void {
if (ctx.sourceFile.fileName.includes('node_modules')) return;
ctx.sourceFile.forEachChild(function recur(node) {
if (node.kind === ts.SyntaxKind.UndefinedKeyword
&& ts.isUnionTypeNode(node.parent!)
&& isOptionalParameter(node.parent!.parent!)) {
ctx.addFailureAtNode(
node,
failure(
Rule.metadata.ruleName,
`Parameter is optional, so no need to include \`undefined\` in the type.`));
}
node.forEachChild(recur);
});
}
function isOptionalParameter(node: ts.Node): boolean {
return node.kind === ts.SyntaxKind.Parameter
&& (node as ts.ParameterDeclaration).questionToken !== undefined;
}

View File

@@ -0,0 +1,163 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { failure } from "../util";
export class Rule extends Lint.Rules.AbstractRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "strict-export-declare-modifiers",
description: "Enforces strict rules about where the 'export' and 'declare' modifiers may appear.",
optionsDescription: "Not configurable.",
options: null,
type: "style",
typescriptOnly: true,
};
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}
function walk(ctx: Lint.WalkContext<void>): void {
const { sourceFile } = ctx;
const isExternal = sourceFile.isDeclarationFile
&& !sourceFile.statements.some(
s => s.kind === ts.SyntaxKind.ExportAssignment ||
s.kind === ts.SyntaxKind.ExportDeclaration && !!(s as ts.ExportDeclaration).exportClause)
&& ts.isExternalModule(sourceFile);
for (const node of sourceFile.statements) {
if (isExternal) {
checkInExternalModule(node, isAutomaticExport(sourceFile));
} else {
checkInOther(node, sourceFile.isDeclarationFile);
}
if (isModuleDeclaration(node) && (sourceFile.isDeclarationFile || isDeclare(node))) {
checkModule(node);
}
}
function checkInExternalModule(node: ts.Statement, autoExportEnabled: boolean) {
// Ignore certain node kinds (these can't have 'export' or 'default' modifiers)
switch (node.kind) {
case ts.SyntaxKind.ImportDeclaration:
case ts.SyntaxKind.ImportEqualsDeclaration:
case ts.SyntaxKind.ExportDeclaration:
case ts.SyntaxKind.NamespaceExportDeclaration:
return;
}
// `declare global` and `declare module "foo"` OK. `declare namespace N` not OK, should be `export namespace`.
if (!isDeclareGlobalOrExternalModuleDeclaration(node)) {
if (isDeclare(node)) {
fail(mod(node, ts.SyntaxKind.DeclareKeyword), "'declare' keyword is redundant here.");
}
if (autoExportEnabled && !isExport(node)) {
fail(
(node as ts.DeclarationStatement).name || node,
"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.");
}
}
}
function checkInOther(node: ts.Statement, inDeclarationFile: boolean): void {
// Compiler will enforce presence of 'declare' where necessary. But types do not need 'declare'.
if (isDeclare(node)) {
if (isExport(node) && inDeclarationFile ||
node.kind === ts.SyntaxKind.InterfaceDeclaration ||
node.kind === ts.SyntaxKind.TypeAliasDeclaration) {
fail(mod(node, ts.SyntaxKind.DeclareKeyword), "'declare' keyword is redundant here.");
}
}
}
function fail(node: ts.Node, reason: string): void {
ctx.addFailureAtNode(node, failure(Rule.metadata.ruleName, reason));
}
function mod(node: ts.Statement, kind: ts.SyntaxKind): ts.Node {
return node.modifiers!.find(m => m.kind === kind)!;
}
function checkModule(moduleDeclaration: ts.ModuleDeclaration): void {
const body = moduleDeclaration.body;
if (!body) {
return;
}
switch (body.kind) {
case ts.SyntaxKind.ModuleDeclaration:
checkModule(body);
break;
case ts.SyntaxKind.ModuleBlock:
checkBlock(body, isAutomaticExport(moduleDeclaration));
break;
}
}
function checkBlock(block: ts.ModuleBlock, autoExportEnabled: boolean): void {
for (const s of block.statements) {
// Compiler will error for 'declare' here anyway, so just check for 'export'.
if (isExport(s) && autoExportEnabled && !isDefault(s)) {
fail(mod(s, ts.SyntaxKind.ExportKeyword),
"'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.");
}
if (isModuleDeclaration(s)) {
checkModule(s);
}
}
}
}
function isDeclareGlobalOrExternalModuleDeclaration(node: ts.Node): boolean {
return isModuleDeclaration(node) && (
node.name.kind === ts.SyntaxKind.StringLiteral ||
node.name.kind === ts.SyntaxKind.Identifier && node.name.text === "global");
}
function isModuleDeclaration(node: ts.Node): node is ts.ModuleDeclaration {
return node.kind === ts.SyntaxKind.ModuleDeclaration;
}
function isDeclare(node: ts.Node): boolean {
return Lint.hasModifier(node.modifiers, ts.SyntaxKind.DeclareKeyword);
}
function isExport(node: ts.Node): boolean {
return Lint.hasModifier(node.modifiers, ts.SyntaxKind.ExportKeyword);
}
function isDefault(node: ts.Node): boolean {
return Lint.hasModifier(node.modifiers, ts.SyntaxKind.DefaultKeyword);
}
// tslint:disable-next-line:max-line-length
// Copied from https://github.com/Microsoft/TypeScript/blob/dd9b8cab34a3e389e924d768eb656cf50656f582/src/compiler/binder.ts#L1571-L1581
function hasExportDeclarations(node: ts.SourceFile | ts.ModuleDeclaration): boolean {
const body = node.kind === ts.SyntaxKind.SourceFile ? node : node.body;
if (body && (body.kind === ts.SyntaxKind.SourceFile || body.kind === ts.SyntaxKind.ModuleBlock)) {
for (const stat of (body as ts.BlockLike).statements) {
if (stat.kind === ts.SyntaxKind.ExportDeclaration || stat.kind === ts.SyntaxKind.ExportAssignment) {
return true;
}
}
}
return false;
}
function isAutomaticExport(node: ts.SourceFile | ts.ModuleDeclaration): boolean {
// We'd like to just test ts.NodeFlags.ExportContext, but we don't run the
// binder, so that flag won't be set, so duplicate the logic instead. :(
//
// ts.NodeFlags.Ambient is @internal, but all modules that get here should
// be ambient.
return !hasExportDeclarations(node);
}

View File

@@ -0,0 +1,36 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { failure } from "../util";
export class Rule extends Lint.Rules.AbstractRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "trim-file",
description: "Forbids leading/trailing blank lines in a file. Allows file to end in '\n'.",
optionsDescription: "Not configurable.",
options: null,
type: "style",
typescriptOnly: false,
};
static FAILURE_STRING_LEADING = failure(Rule.metadata.ruleName, "File should not begin with a blank line.");
static FAILURE_STRING_TRAILING = failure(
Rule.metadata.ruleName,
"File should not end with a blank line. (Ending in one newline OK, ending in two newlines not OK.)");
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}
function walk(ctx: Lint.WalkContext<void>): void {
const { sourceFile: { text } } = ctx;
if (text.startsWith("\r") || text.startsWith("\n")) {
ctx.addFailureAt(0, 0, Rule.FAILURE_STRING_LEADING);
}
if (text.endsWith("\n\n") || text.endsWith("\r\n\r\n")) {
const start = text.endsWith("\r\n") ? text.length - 2 : text.length - 1;
ctx.addFailureAt(start, 0, Rule.FAILURE_STRING_TRAILING);
}
}

View File

@@ -0,0 +1,70 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { failure } from "../util";
export class Rule extends Lint.Rules.AbstractRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "void-return",
description: "`void` may only be used as a return type.",
rationale: "style",
optionsDescription: "Not configurable.",
options: null,
type: "style",
typescriptOnly: true,
};
static FAILURE_STRING = failure(
Rule.metadata.ruleName,
"Use the `void` type for return types only. Otherwise, use `undefined`.");
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}
function walk(ctx: Lint.WalkContext<void>): void {
ctx.sourceFile.forEachChild(function cb(node) {
if (node.kind === ts.SyntaxKind.VoidKeyword && !mayContainVoid(node.parent!) && !isReturnType(node)) {
ctx.addFailureAtNode(node, Rule.FAILURE_STRING);
} else {
node.forEachChild(cb);
}
});
}
function mayContainVoid({ kind }: ts.Node): boolean {
switch (kind) {
case ts.SyntaxKind.TypeReference:
case ts.SyntaxKind.ExpressionWithTypeArguments:
case ts.SyntaxKind.NewExpression:
case ts.SyntaxKind.CallExpression:
case ts.SyntaxKind.TypeParameter: // Allow f<T = void>
return true;
default:
return false;
}
}
function isReturnType(node: ts.Node): boolean {
let parent = node.parent!;
if (parent.kind === ts.SyntaxKind.UnionType) {
[node, parent] = [parent, parent.parent!];
}
return isSignatureDeclaration(parent) && parent.type === node;
}
function isSignatureDeclaration(node: ts.Node): node is ts.SignatureDeclaration {
switch (node.kind) {
case ts.SyntaxKind.ArrowFunction:
case ts.SyntaxKind.CallSignature:
case ts.SyntaxKind.FunctionDeclaration:
case ts.SyntaxKind.FunctionExpression:
case ts.SyntaxKind.FunctionType:
case ts.SyntaxKind.MethodDeclaration:
case ts.SyntaxKind.MethodSignature:
return true;
default:
return false;
}
}

View File

@@ -0,0 +1,75 @@
import fs = require("fs");
import os = require("os");
import path = require("path");
import { WalkContext } from "tslint";
const suggestionsDir = path.join(os.homedir(), ".dts", "suggestions");
export interface Suggestion {
fileName: string;
ruleName: string;
message: string;
start?: number;
width?: number;
}
// Packages for which suggestions were already added in this run of dtslint.
const existingPackages = new Set();
/**
* A rule should call this function to provide a suggestion instead of a lint failure.
*/
export function addSuggestion<T>(ctx: WalkContext<T>, message: string, start?: number, width?: number) {
const suggestion: Suggestion = {
fileName: ctx.sourceFile.fileName,
ruleName: ctx.ruleName,
message,
start,
width,
};
const packageName = dtPackageName(ctx.sourceFile.fileName);
if (!packageName) {
return;
}
let flag = "a";
if (!existingPackages.has(packageName)) {
flag = "w";
existingPackages.add(packageName);
}
try {
if (!fs.existsSync(suggestionsDir)) {
fs.mkdirSync(suggestionsDir, { recursive: true });
}
fs.writeFileSync(
path.join(suggestionsDir, packageName + ".txt"),
flag === "a" ? "\n" + formatSuggestion(suggestion) : formatSuggestion(suggestion),
{ flag, encoding: "utf8" });
} catch (e) {
console.log(`Could not write suggestions for package ${packageName}. ${e.message || ""}`);
}
}
const dtPath = path.join("DefinitelyTyped", "types");
function dtPackageName(filePath: string): string | undefined {
const dtIndex = filePath.indexOf(dtPath);
if (dtIndex === -1) {
return undefined;
}
const basePath = filePath.substr(dtIndex + dtPath.length);
const dirs = basePath.split(path.sep).filter(dir => dir !== "");
if (dirs.length === 0) {
return undefined;
}
const packageName = dirs[0];
// Check if this is an old version of a package.
if (dirs.length > 1 && /^v\d+(\.\d+)?$/.test(dirs[1])) {
return packageName + dirs[1];
}
return packageName;
}
function formatSuggestion(suggestion: Suggestion): string {
return JSON.stringify(suggestion, /*replacer*/ undefined, 0);
}

View File

@@ -0,0 +1,249 @@
// This is a stand-alone script that updates TSLint configurations for DefinitelyTyped packages.
// It runs all rules specified in `dt.json`, and updates the existing configuration for a package
// by adding rule exemptions only for the rules that caused a lint failure.
// For example, if a configuration specifies `"no-trailing-whitespace": false` and this rule
// no longer produces an error, then it will not be disabled in the new configuration.
// If you update or create a rule and now it causes new failures in DT, you can update the `dt.json`
// configuration with your rule, then register a disabler function for your rule
// (check `disableRules` function below), then run this script with your rule as argument.
import cp = require("child_process");
import fs = require("fs");
import stringify = require("json-stable-stringify");
import path = require("path");
import { Configuration as Config, ILinterOptions, IRuleFailureJson, Linter, LintResult, RuleFailure } from "tslint";
import * as ts from "typescript";
import yargs = require("yargs");
import { isExternalDependency } from "./lint";
import { disabler as npmNamingDisabler } from "./rules/npmNamingRule";
// Rule "expect" needs TypeScript version information, which this script doesn't collect.
const ignoredRules: string[] = ["expect"];
function main() {
const args = yargs
.usage(`\`$0 --dt=path-to-dt\` or \`$0 --package=path-to-dt-package\`
'dt.json' is used as the base tslint config for running the linter.`)
.option("package", {
describe: "Path of DT package.",
type: "string",
conflicts: "dt",
})
.option("dt", {
describe: "Path of local DefinitelyTyped repository.",
type: "string",
conflicts: "package",
})
.option("rules", {
describe: "Names of the rules to be updated. Leave this empty to update all rules.",
type: "array",
string: true,
default: [] as string[],
})
.check(arg => {
if (!arg.package && !arg.dt) {
throw new Error("You must provide either argument 'package' or 'dt'.");
}
const unsupportedRules = arg.rules.filter(rule => ignoredRules.includes(rule));
if (unsupportedRules.length > 0) {
throw new Error(`Rules ${unsupportedRules.join(", ")} are not supported at the moment.`);
}
return true;
}).argv;
if (args.package) {
updatePackage(args.package, dtConfig(args.rules));
} else if (args.dt) {
updateAll(args.dt, dtConfig(args.rules));
}
}
const dtConfigPath = "dt.json";
function dtConfig(updatedRules: string[]): Config.IConfigurationFile {
const config = Config.findConfiguration(dtConfigPath).results;
if (!config) {
throw new Error(`Could not load config at ${dtConfigPath}.`);
}
// Disable ignored or non-updated rules.
for (const entry of config.rules.entries()) {
const [rule, ruleOpts] = entry;
if (ignoredRules.includes(rule) || (updatedRules.length > 0 && !updatedRules.includes(rule))) {
ruleOpts.ruleSeverity = "off";
}
}
return config;
}
function updateAll(dtPath: string, config: Config.IConfigurationFile): void {
const packages = fs.readdirSync(path.join(dtPath, "types"));
for (const pkg of packages) {
updatePackage(path.join(dtPath, "types", pkg), config);
}
}
function updatePackage(pkgPath: string, baseConfig: Config.IConfigurationFile): void {
installDependencies(pkgPath);
const packages = walkPackageDir(pkgPath);
const linterOpts: ILinterOptions = {
fix: false,
};
for (const pkg of packages) {
const results = pkg.lint(linterOpts, baseConfig);
if (results.failures.length > 0) {
const disabledRules = disableRules(results.failures);
const newConfig = mergeConfigRules(pkg.config(), disabledRules, baseConfig);
pkg.updateConfig(newConfig);
}
}
}
function installDependencies(pkgPath: string): void {
if (fs.existsSync(path.join(pkgPath, "package.json"))) {
cp.execSync(
"npm install --ignore-scripts --no-shrinkwrap --no-package-lock --no-bin-links",
{
encoding: "utf8",
cwd: pkgPath,
});
}
}
function mergeConfigRules(
config: Config.RawConfigFile,
newRules: Config.RawRulesConfig,
baseConfig: Config.IConfigurationFile): Config.RawConfigFile {
const activeRules: string[] = [];
baseConfig.rules.forEach((ruleOpts, ruleName) => {
if (ruleOpts.ruleSeverity !== "off") {
activeRules.push(ruleName);
}
});
const oldRules: Config.RawRulesConfig = config.rules || {};
let newRulesConfig: Config.RawRulesConfig = {};
for (const rule of Object.keys(oldRules)) {
if (activeRules.includes(rule)) {
continue;
}
newRulesConfig[rule] = oldRules[rule];
}
newRulesConfig = { ...newRulesConfig, ...newRules };
return { ...config, rules: newRulesConfig };
}
/**
* Represents a package from the linter's perspective.
* For example, `DefinitelyTyped/types/react` and `DefinitelyTyped/types/react/v15` are different
* packages.
*/
class LintPackage {
private files: ts.SourceFile[] = [];
private program: ts.Program;
constructor(private rootDir: string) {
this.program = Linter.createProgram(path.join(this.rootDir, "tsconfig.json"));
}
config(): Config.RawConfigFile {
return Config.readConfigurationFile(path.join(this.rootDir, "tslint.json"));
}
addFile(filePath: string): void {
const file = this.program.getSourceFile(filePath);
if (file) {
this.files.push(file);
}
}
lint(opts: ILinterOptions, config: Config.IConfigurationFile): LintResult {
const linter = new Linter(opts, this.program);
for (const file of this.files) {
if (ignoreFile(file, this.rootDir, this.program)) {
continue;
}
linter.lint(file.fileName, file.text, config);
}
return linter.getResult();
}
updateConfig(config: Config.RawConfigFile): void {
fs.writeFileSync(
path.join(this.rootDir, "tslint.json"),
stringify(config, { space: 4 }),
{ encoding: "utf8", flag: "w" });
}
}
function ignoreFile(file: ts.SourceFile, dirPath: string, program: ts.Program): boolean {
return program.isSourceFileDefaultLibrary(file) || isExternalDependency(file, path.resolve(dirPath), program);
}
function walkPackageDir(rootDir: string): LintPackage[] {
const packages: LintPackage[] = [];
function walk(curPackage: LintPackage, dir: string): void {
for (const ent of fs.readdirSync(dir, { encoding: "utf8", withFileTypes: true })) {
const entPath = path.join(dir, ent.name);
if (ent.isFile()) {
curPackage.addFile(entPath);
} else if (ent.isDirectory() && ent.name !== "node_modules") {
if (isVersionDir(ent.name)) {
const newPackage = new LintPackage(entPath);
packages.push(newPackage);
walk(newPackage, entPath);
} else {
walk(curPackage, entPath);
}
}
}
}
const lintPackage = new LintPackage(rootDir);
packages.push(lintPackage);
walk(lintPackage, rootDir);
return packages;
}
/**
* Returns true if directory name matches a TypeScript or package version directory.
* Examples: `ts3.5`, `v11`, `v0.6` are all version names.
*/
function isVersionDir(dirName: string): boolean {
return /^ts\d+\.\d$/.test(dirName) || /^v\d+(\.\d+)?$/.test(dirName);
}
type RuleOptions = boolean | unknown[];
type RuleDisabler = (failures: IRuleFailureJson[]) => RuleOptions;
const defaultDisabler: RuleDisabler = () => {
return false;
};
function disableRules(allFailures: RuleFailure[]): Config.RawRulesConfig {
const ruleToFailures: Map<string, IRuleFailureJson[]> = new Map();
for (const failure of allFailures) {
const failureJson = failure.toJson();
if (ruleToFailures.has(failureJson.ruleName)) {
ruleToFailures.get(failureJson.ruleName)!.push(failureJson);
} else {
ruleToFailures.set(failureJson.ruleName, [failureJson]);
}
}
const newRulesConfig: Config.RawRulesConfig = {};
ruleToFailures.forEach((failures, rule) => {
if (ignoredRules.includes(rule)) {
return;
}
const disabler = rule === "npm-naming" ? npmNamingDisabler : defaultDisabler;
const opts: RuleOptions = disabler(failures);
newRulesConfig[rule] = opts;
});
return newRulesConfig;
}
if (!module.parent) {
main();
}

View File

@@ -0,0 +1,109 @@
import assert = require("assert");
import { pathExists, readFile } from "fs-extra";
import { basename, dirname, join } from "path";
import stripJsonComments = require("strip-json-comments");
import * as ts from "typescript";
export async function readJson(path: string) {
const text = await readFile(path, "utf-8");
return JSON.parse(stripJsonComments(text));
}
export function failure(ruleName: string, s: string): string {
return `${s} See: https://github.com/Microsoft/dtslint/blob/master/docs/${ruleName}.md`;
}
export function getCommonDirectoryName(files: readonly string[]): string {
let minLen = 999;
let minDir = "";
for (const file of files) {
const dir = dirname(file);
if (dir.length < minLen) {
minDir = dir;
minLen = dir.length;
}
}
return basename(minDir);
}
export function eachModuleStatement(sourceFile: ts.SourceFile, action: (statement: ts.Statement) => void): void {
if (!sourceFile.isDeclarationFile) {
return;
}
for (const node of sourceFile.statements) {
if (ts.isModuleDeclaration(node)) {
const statements = getModuleDeclarationStatements(node);
if (statements) {
for (const statement of statements) {
action(statement);
}
}
} else {
action(node);
}
}
}
export function getModuleDeclarationStatements(node: ts.ModuleDeclaration): readonly ts.Statement[] | undefined {
let { body } = node;
while (body && body.kind === ts.SyntaxKind.ModuleDeclaration) {
body = body.body;
}
return body && ts.isModuleBlock(body) ? body.statements : undefined;
}
export async function getCompilerOptions(dirPath: string): Promise<ts.CompilerOptions> {
const tsconfigPath = join(dirPath, "tsconfig.json");
if (!await pathExists(tsconfigPath)) {
throw new Error(`Need a 'tsconfig.json' file in ${dirPath}`);
}
return (await readJson(tsconfigPath)).compilerOptions;
}
export function withoutPrefix(s: string, prefix: string): string | undefined {
return s.startsWith(prefix) ? s.slice(prefix.length) : undefined;
}
export function last<T>(a: readonly T[]): T {
assert(a.length !== 0);
return a[a.length - 1];
}
export function assertDefined<T>(a: T | undefined): T {
if (a === undefined) { throw new Error(); }
return a;
}
export async function mapDefinedAsync<T, U>(
arr: Iterable<T>, mapper: (t: T) => Promise<U | undefined>): Promise<U[]> {
const out = [];
for (const a of arr) {
const res = await mapper(a);
if (res !== undefined) {
out.push(res);
}
}
return out;
}
export function isMainFile(fileName: string, allowNested: boolean) {
// Linter may be run with cwd of the package. We want `index.d.ts` but not `submodule/index.d.ts` to match.
if (fileName === "index.d.ts") {
return true;
}
if (basename(fileName) !== "index.d.ts") {
return false;
}
let parent = dirname(fileName);
// May be a directory for an older version, e.g. `v0`.
// Note a types redirect `foo/ts3.1` should not have its own header.
if (allowNested && /^v(0\.)?\d+$/.test(basename(parent))) {
parent = dirname(parent);
}
// Allow "types/foo/index.d.ts", not "types/foo/utils/index.d.ts"
return basename(dirname(parent)) === "types";
}

Some files were not shown because too many files have changed in this diff Show More