Skip to content

Commit d84fa55

Browse files
authored
feat: validate type comments and generate .d.ts (take 2) (#251)
1 parent f3bc7a4 commit d84fa55

23 files changed

+785
-213
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
/node_modules
44
/index.*
55
/test.*
6+
/dist
67

78
!.vitepress
89
/docs/.vitepress/dist

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
/node_modules
66
/index.*
77
/test.*
8+
/dist

.knip.jsonc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"$schema": "https://unpkg.com/knip@5/schema.json",
3+
"entry": ["src/index.mjs", "test/**/*.mjs"],
34
"ignore": ["./docs/.vitepress/**/*"]
45
}

.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
package-lock = false
2+
force = true

.vscode/settings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
"editor.formatOnSave": true,
55
"editor.codeActionsOnSave": ["source.organizeImports", "source.fixAll"]
66
},
7+
"[typescript]": {
8+
"editor.defaultFormatter": "esbenp.prettier-vscode",
9+
"editor.formatOnSave": true,
10+
"editor.codeActionsOnSave": ["source.organizeImports", "source.fixAll"]
11+
},
712
"[json]": {
813
"editor.defaultFormatter": "esbenp.prettier-vscode",
914
"editor.formatOnSave": true,

package.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@
3030
],
3131
"scripts": {
3232
"prebuild": "npm run -s clean",
33-
"build": "rollup -c",
34-
"clean": "rimraf .nyc_output coverage index.*",
33+
"build": "npm run build:dts && npm run build:rollup",
34+
"build:dts": "tsc -p tsconfig.build.json",
35+
"build:rollup": "rollup -c",
36+
"clean": "rimraf .nyc_output coverage index.* dist",
3537
"coverage": "opener ./coverage/lcov-report/index.html",
3638
"docs:build": "vitepress build docs",
3739
"docs:watch": "vitepress dev docs",
@@ -40,7 +42,7 @@
4042
"format:check": "npm run -s format:prettier -- --check",
4143
"lint:eslint": "eslint .",
4244
"lint:format": "npm run -s format:check",
43-
"lint:installed-check": "installed-check -v -i installed-check -i npm-run-all2 -i knip",
45+
"lint:installed-check": "installed-check -v -i installed-check -i npm-run-all2 -i knip -i rollup-plugin-dts",
4446
"lint:knip": "knip",
4547
"lint": "run-p lint:*",
4648
"test": "c8 mocha --reporter dot \"test/*.mjs\"",
@@ -54,6 +56,8 @@
5456
},
5557
"devDependencies": {
5658
"@eslint-community/eslint-plugin-mysticatea": "^15.6.1",
59+
"@types/eslint": "^9.6.1",
60+
"@types/estree": "^1.0.7",
5761
"c8": "^8.0.1",
5862
"dot-prop": "^7.2.0",
5963
"eslint": "^8.57.1",
@@ -65,8 +69,10 @@
6569
"prettier": "2.8.8",
6670
"rimraf": "^3.0.2",
6771
"rollup": "^2.79.2",
72+
"rollup-plugin-dts": "^4.2.3",
6873
"rollup-plugin-sourcemaps": "^0.6.3",
6974
"semver": "^7.6.3",
75+
"typescript": "^4.9.5",
7076
"vitepress": "^1.4.1",
7177
"warun": "^1.0.0"
7278
},

rollup.config.js

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,54 @@
22
* @author Toru Nagashima
33
* See LICENSE file in root directory for full license.
44
*/
5+
import fs from "fs"
6+
import path from "path"
7+
import dts from "rollup-plugin-dts"
58
import sourcemaps from "rollup-plugin-sourcemaps"
69
import packageInfo from "./package.json"
710

811
/**
912
* Define the output configuration.
1013
* @param {string} ext The extension for generated files.
11-
* @returns {object} The output configuration
14+
* @returns {object[]} The output configuration
1215
*/
1316
function config(ext) {
14-
return {
15-
input: "src/index.mjs",
16-
output: {
17-
exports: ext === ".mjs" ? undefined : "named",
18-
file: `index${ext}`,
19-
format: ext === ".mjs" ? "es" : "cjs",
20-
sourcemap: true,
17+
return [
18+
{
19+
input: "src/index.mjs",
20+
output: {
21+
exports: ext === ".mjs" ? undefined : "named",
22+
file: `index${ext}`,
23+
format: ext === ".mjs" ? "es" : "cjs",
24+
sourcemap: true,
25+
},
26+
plugins: [sourcemaps()],
27+
external: Object.keys(packageInfo.dependencies),
2128
},
22-
plugins: [sourcemaps()],
23-
external: Object.keys(packageInfo.dependencies),
29+
{
30+
input: "./dist/index.d.ts",
31+
output: {
32+
exports: "named",
33+
file: `index.d${ext.replace(/js$/u, "ts")}`,
34+
format: "es",
35+
},
36+
plugins: [dts()],
37+
},
38+
]
39+
}
40+
41+
/* eslint-disable @eslint-community/mysticatea/node/no-sync */
42+
// Replace extension `.mts` to `.ts` in the `dist/*.d.mts` file name.
43+
// This is needed because rollup-plugin-dts<=v4 doesn't support `.mts` extension.
44+
for (const file of fs.readdirSync(path.resolve("dist"))) {
45+
if (file.endsWith(".d.mts")) {
46+
const content = fs.readFileSync(path.resolve("dist", file), "utf8")
47+
const newContent = content.replace(/\.mjs(['"])/gu, ".js$1")
48+
const newName = file.replace(/\.d\.mts$/u, ".d.ts")
49+
fs.writeFileSync(path.resolve("dist", newName), newContent, "utf8")
50+
fs.unlinkSync(path.resolve("dist", file))
2451
}
2552
}
53+
/* eslint-enable @eslint-community/mysticatea/node/no-sync */
2654

27-
export default [config(".js"), config(".mjs")]
55+
export default [...config(".js"), ...config(".mjs")]

src/find-variable.mjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import { getInnermostScope } from "./get-innermost-scope.mjs"
2+
/** @typedef {import("eslint").Scope.Scope} Scope */
3+
/** @typedef {import("eslint").Scope.Variable} Variable */
4+
/** @typedef {import("estree").Identifier} Identifier */
25

36
/**
47
* Find the variable of a given name.
58
* @param {Scope} initialScope The scope to start finding.
6-
* @param {string|Node} nameOrNode The variable name to find. If this is a Node object then it should be an Identifier node.
9+
* @param {string|Identifier} nameOrNode The variable name to find. If this is a Node object then it should be an Identifier node.
710
* @returns {Variable|null} The found variable or null.
811
*/
912
export function findVariable(initialScope, nameOrNode) {
1013
let name = ""
14+
/** @type {Scope|null} */
1115
let scope = initialScope
1216

1317
if (typeof nameOrNode === "string") {

src/get-function-head-location.mjs

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,47 @@
11
import { isArrowToken, isOpeningParenToken } from "./token-predicate.mjs"
2+
/** @typedef {import("eslint").Rule.Node} RuleNode */
3+
/** @typedef {import("eslint").SourceCode} SourceCode */
4+
/** @typedef {import("eslint").AST.Token} Token */
5+
/** @typedef {import("estree").Function} FunctionNode */
6+
/** @typedef {import("estree").FunctionDeclaration} FunctionDeclaration */
7+
/** @typedef {import("estree").FunctionExpression} FunctionExpression */
8+
/** @typedef {import("estree").SourceLocation} SourceLocation */
9+
/** @typedef {import("estree").Position} Position */
210

311
/**
412
* Get the `(` token of the given function node.
5-
* @param {Node} node - The function node to get.
13+
* @param {FunctionExpression | FunctionDeclaration} node - The function node to get.
614
* @param {SourceCode} sourceCode - The source code object to get tokens.
715
* @returns {Token} `(` token.
816
*/
917
function getOpeningParenOfParams(node, sourceCode) {
1018
return node.id
11-
? sourceCode.getTokenAfter(node.id, isOpeningParenToken)
12-
: sourceCode.getFirstToken(node, isOpeningParenToken)
19+
? /** @type {Token} */ (
20+
sourceCode.getTokenAfter(node.id, isOpeningParenToken)
21+
)
22+
: /** @type {Token} */ (
23+
sourceCode.getFirstToken(node, isOpeningParenToken)
24+
)
1325
}
1426

1527
/**
1628
* Get the location of the given function node for reporting.
17-
* @param {Node} node - The function node to get.
29+
* @param {FunctionNode} node - The function node to get.
1830
* @param {SourceCode} sourceCode - The source code object to get tokens.
19-
* @returns {string} The location of the function node for reporting.
31+
* @returns {SourceLocation|null} The location of the function node for reporting.
2032
*/
2133
export function getFunctionHeadLocation(node, sourceCode) {
22-
const parent = node.parent
34+
const parent = /** @type {RuleNode} */ (node).parent
35+
36+
/** @type {Position|null} */
2337
let start = null
38+
/** @type {Position|null} */
2439
let end = null
2540

2641
if (node.type === "ArrowFunctionExpression") {
27-
const arrowToken = sourceCode.getTokenBefore(node.body, isArrowToken)
42+
const arrowToken = /** @type {Token} */ (
43+
sourceCode.getTokenBefore(node.body, isArrowToken)
44+
)
2845

2946
start = arrowToken.loc.start
3047
end = arrowToken.loc.end
@@ -33,11 +50,11 @@ export function getFunctionHeadLocation(node, sourceCode) {
3350
parent.type === "MethodDefinition" ||
3451
parent.type === "PropertyDefinition"
3552
) {
36-
start = parent.loc.start
37-
end = getOpeningParenOfParams(node, sourceCode).loc.start
53+
start = /** @type {SourceLocation} */ (parent.loc).start
54+
end = getOpeningParenOfParams(node, sourceCode)?.loc.start
3855
} else {
39-
start = node.loc.start
40-
end = getOpeningParenOfParams(node, sourceCode).loc.start
56+
start = /** @type {SourceLocation} */ (node.loc).start
57+
end = getOpeningParenOfParams(node, sourceCode)?.loc.start
4158
}
4259

4360
return {

src/get-function-name-with-kind.mjs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import { getPropertyName } from "./get-property-name.mjs"
2+
/** @typedef {import("eslint").Rule.Node} RuleNode */
3+
/** @typedef {import("eslint").SourceCode} SourceCode */
4+
/** @typedef {import("estree").Function} FunctionNode */
5+
/** @typedef {import("estree").FunctionDeclaration} FunctionDeclaration */
6+
/** @typedef {import("estree").FunctionExpression} FunctionExpression */
7+
/** @typedef {import("estree").Identifier} Identifier */
28

39
/**
410
* Get the name and kind of the given function node.
5-
* @param {ASTNode} node - The function node to get.
11+
* @param {FunctionNode} node - The function node to get.
612
* @param {SourceCode} [sourceCode] The source code object to get the code of computed property keys.
713
* @returns {string} The name and kind of the function node.
814
*/
915
// eslint-disable-next-line complexity
1016
export function getFunctionNameWithKind(node, sourceCode) {
11-
const parent = node.parent
17+
const parent = /** @type {RuleNode} */ (node).parent
1218
const tokens = []
1319
const isObjectMethod = parent.type === "Property" && parent.value === node
1420
const isClassMethod =
@@ -68,7 +74,7 @@ export function getFunctionNameWithKind(node, sourceCode) {
6874
}
6975
}
7076
}
71-
} else if (node.id) {
77+
} else if (hasId(node)) {
7278
tokens.push(`'${node.id.name}'`)
7379
} else if (
7480
parent.type === "VariableDeclarator" &&
@@ -92,3 +98,14 @@ export function getFunctionNameWithKind(node, sourceCode) {
9298

9399
return tokens.join(" ")
94100
}
101+
102+
/**
103+
* @param {FunctionNode} node
104+
* @returns {node is FunctionDeclaration | FunctionExpression & { id: Identifier }}
105+
*/
106+
function hasId(node) {
107+
return Boolean(
108+
/** @type {Partial<FunctionDeclaration | FunctionExpression>} */ (node)
109+
.id,
110+
)
111+
}

src/get-innermost-scope.mjs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1+
/** @typedef {import("eslint").Scope.Scope} Scope */
2+
/** @typedef {import("estree").Node} Node */
3+
14
/**
25
* Get the innermost scope which contains a given location.
36
* @param {Scope} initialScope The initial scope to search.
47
* @param {Node} node The location to search.
58
* @returns {Scope} The innermost scope.
69
*/
710
export function getInnermostScope(initialScope, node) {
8-
const location = node.range[0]
11+
const location = /** @type {[number, number]} */ (node.range)[0]
912

1013
let scope = initialScope
1114
let found = false
1215
do {
1316
found = false
1417
for (const childScope of scope.childScopes) {
15-
const range = childScope.block.range
18+
const range = /** @type {[number, number]} */ (
19+
childScope.block.range
20+
)
1621

1722
if (range[0] <= location && location < range[1]) {
1823
scope = childScope

src/get-property-name.mjs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { getStringIfConstant } from "./get-string-if-constant.mjs"
2+
/** @typedef {import("eslint").Scope.Scope} Scope */
3+
/** @typedef {import("estree").MemberExpression} MemberExpression */
4+
/** @typedef {import("estree").MethodDefinition} MethodDefinition */
5+
/** @typedef {import("estree").Property} Property */
6+
/** @typedef {import("estree").PropertyDefinition} PropertyDefinition */
7+
/** @typedef {import("estree").Identifier} Identifier */
28

39
/**
410
* Get the property name from a MemberExpression node or a Property node.
5-
* @param {Node} node The node to get.
11+
* @param {MemberExpression | MethodDefinition | Property | PropertyDefinition} node The node to get.
612
* @param {Scope} [initialScope] The scope to start finding variable. Optional. If the node is a computed property node and this scope was given, this checks the computed property name by the `getStringIfConstant` function with the scope, and returns the value of it.
7-
* @returns {string|null} The property name of the node.
13+
* @returns {string|null|undefined} The property name of the node.
814
*/
915
export function getPropertyName(node, initialScope) {
1016
switch (node.type) {
@@ -15,7 +21,7 @@ export function getPropertyName(node, initialScope) {
1521
if (node.property.type === "PrivateIdentifier") {
1622
return null
1723
}
18-
return node.property.name
24+
return /** @type {Partial<Identifier>} */ (node.property).name
1925

2026
case "Property":
2127
case "MethodDefinition":
@@ -29,9 +35,10 @@ export function getPropertyName(node, initialScope) {
2935
if (node.key.type === "PrivateIdentifier") {
3036
return null
3137
}
32-
return node.key.name
38+
return /** @type {Partial<Identifier>} */ (node.key).name
3339

34-
// no default
40+
default:
41+
break
3542
}
3643

3744
return null

0 commit comments

Comments
 (0)