Skip to content

Commit d5fa20d

Browse files
committed
refactor: migrate deprecated doctrine to comment-parser
close #313
1 parent 4b7e2f4 commit d5fa20d

10 files changed

+460
-439
lines changed

.changeset/kind-bananas-poke.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-import-x": minor
3+
---
4+
5+
refactor: migrate deprecated `doctrine` to `comment-parser`

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,6 @@ lib
2626
!.yarn/plugins
2727
!.yarn/releases
2828
!.yarn/sdks
29+
30+
# Local test
31+
test.local.*

package.json

+17-18
Original file line numberDiff line numberDiff line change
@@ -72,23 +72,22 @@
7272
},
7373
"dependencies": {
7474
"@pkgr/core": "^0.2.4",
75-
"@types/doctrine": "^0.0.9",
76-
"@typescript-eslint/utils": "^8.30.1",
75+
"@typescript-eslint/utils": "^8.31.0",
76+
"comment-parser": "^1.4.1",
7777
"debug": "^4.4.0",
78-
"doctrine": "^3.0.0",
7978
"eslint-import-resolver-node": "^0.3.9",
8079
"get-tsconfig": "^4.10.0",
8180
"is-glob": "^4.0.3",
8281
"minimatch": "^9.0.3 || ^10.0.1",
8382
"semver": "^7.7.1",
8483
"stable-hash": "^0.0.5",
8584
"tslib": "^2.8.1",
86-
"unrs-resolver": "^1.6.0"
85+
"unrs-resolver": "^1.7.0"
8786
},
8887
"devDependencies": {
8988
"@1stg/commitlint-config": "^5.0.6",
9089
"@1stg/lint-staged": "^4.0.9",
91-
"@1stg/prettier-config": "^5.1.3",
90+
"@1stg/prettier-config": "^5.1.4",
9291
"@1stg/remark-preset": "^3.1.1",
9392
"@1stg/simple-git-hooks": "^2.0.1",
9493
"@1stg/tsconfig": "^3.0.3",
@@ -106,10 +105,10 @@
106105
"@changesets/cli": "^2.29.2",
107106
"@commitlint/cli": "^19.8.0",
108107
"@eslint/import-test-order-redirect-scoped": "link:./test/fixtures/order-redirect-scoped",
109-
"@eslint/js": "^9.25.0",
108+
"@eslint/js": "^9.25.1",
110109
"@pkgr/rollup": "^6.0.3",
111110
"@swc-node/jest": "^1.8.13",
112-
"@swc/core": "^1.11.21",
111+
"@swc/core": "^1.11.22",
113112
"@swc/helpers": "^0.5.17",
114113
"@test-scope/some-module": "link:./test/fixtures/symlinked-module",
115114
"@total-typescript/ts-reset": "^0.6.1",
@@ -122,26 +121,26 @@
122121
"@types/klaw-sync": "^6.0.5",
123122
"@types/node": "^22.14.1",
124123
"@types/pnpapi": "^0.0.5",
125-
"@typescript-eslint/eslint-plugin": "^8.30.1",
126-
"@typescript-eslint/parser": "^8.30.1",
127-
"@typescript-eslint/rule-tester": "^8.30.1",
124+
"@typescript-eslint/eslint-plugin": "^8.31.0",
125+
"@typescript-eslint/parser": "^8.31.0",
126+
"@typescript-eslint/rule-tester": "^8.31.0",
128127
"@unts/patch-package": "^8.1.1",
129-
"clean-pkg-json": "^1.2.1",
130-
"eslint": "^9.25.0",
128+
"clean-pkg-json": "^1.3.0",
129+
"eslint": "^9.25.1",
131130
"eslint-config-prettier": "^10.1.2",
132131
"eslint-doc-generator": "^2.1.2",
133-
"eslint-import-resolver-typescript": "^4.3.3",
132+
"eslint-import-resolver-typescript": "^4.3.4",
134133
"eslint-import-resolver-webpack": "^0.13.10",
135134
"eslint-import-test-order-redirect": "link:./test/fixtures/order-redirect",
136135
"eslint-plugin-eslint-plugin": "^6.4.0",
137136
"eslint-plugin-import-x": "link:.",
138137
"eslint-plugin-jest": "^28.11.0",
139138
"eslint-plugin-json": "^4.0.1",
140-
"eslint-plugin-mdx": "^3.4.0",
139+
"eslint-plugin-mdx": "^3.4.1",
141140
"eslint-plugin-n": "^17.17.0",
142141
"eslint-plugin-prettier": "^5.2.6",
143142
"eslint-plugin-unicorn": "^58.0.0",
144-
"eslint-plugin-yml": "^1.17.0",
143+
"eslint-plugin-yml": "^1.18.0",
145144
"eslint8.56": "npm:eslint@~8.56.0",
146145
"eslint9": "npm:eslint@^9.24.0",
147146
"globals": "^16.0.0",
@@ -150,16 +149,16 @@
150149
"klaw-sync": "^7.0.0",
151150
"nano-staged": "^0.8.0",
152151
"npm-run-all2": "^7.0.2",
153-
"path-serializer": "^0.3.4",
152+
"path-serializer": "^0.4.0",
154153
"prettier": "^3.5.3",
155154
"redux": "^5.0.1",
156155
"rimraf": "^6.0.1",
157-
"simple-git-hooks": "^2.12.1",
156+
"simple-git-hooks": "^2.13.0",
158157
"tinyexec": "^1.0.1",
159158
"ts-node": "^10.9.2",
160159
"type-fest": "^4.40.0",
161160
"typescript": "^5.8.3",
162-
"typescript-eslint": "^8.30.1",
161+
"typescript-eslint": "^8.31.0",
163162
"yarn-berry-deduplicate": "^6.1.3",
164163
"zod": "^3.24.3"
165164
},

src/meta.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
import { createRequire } from 'node:module'
22

3-
import { EVAL_FILENAMES } from '@pkgr/core'
43
import type { CjsRequire } from '@pkgr/core'
54

6-
const cjsRequire: CjsRequire =
7-
typeof require === 'undefined' ||
8-
// workaround for #296
9-
EVAL_FILENAMES.has(__filename)
10-
? createRequire(import.meta.url)
11-
: /* istanbul ignore next */ require
5+
const cjsRequire: CjsRequire = import.meta.url
6+
? createRequire(import.meta.url)
7+
: /* istanbul ignore next */ require
128

139
export const { name, version } = cjsRequire<{ name: string; version: string }>(
1410
'../package.json',

src/rules/no-deprecated.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { TSESTree } from '@typescript-eslint/utils'
2-
import type { Tag } from 'doctrine'
2+
import type { Spec } from 'comment-parser'
33

44
import type { ModuleNamespace } from '../utils/index.js'
55
import {
@@ -9,7 +9,7 @@ import {
99
getValue,
1010
} from '../utils/index.js'
1111

12-
function message(deprecation: Tag) {
12+
function message(deprecation: Spec) {
1313
if (deprecation.description) {
1414
return {
1515
messageId: 'deprecatedDesc',
@@ -25,7 +25,7 @@ function getDeprecation(metadata?: ModuleNamespace | null) {
2525
return
2626
}
2727

28-
return metadata.doc.tags.find(t => t.title === 'deprecated')
28+
return metadata.doc.tags.find(t => t.tag === 'deprecated')
2929
}
3030

3131
export default createRule({
@@ -45,7 +45,7 @@ export default createRule({
4545
},
4646
defaultOptions: [],
4747
create(context) {
48-
const deprecated = new Map<string, Tag>()
48+
const deprecated = new Map<string, Spec>()
4949
const namespaces = new Map<string, ExportMap | null>()
5050

5151
return {
@@ -66,7 +66,7 @@ export default createRule({
6666
}
6767

6868
const moduleDeprecation = imports.doc?.tags.find(
69-
t => t.title === 'deprecated',
69+
t => t.tag === 'deprecated',
7070
)
7171
if (moduleDeprecation) {
7272
context.report({

src/utils/export-map.ts

+44-24
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import fs from 'node:fs'
22
import path from 'node:path'
33

44
import type { TSESLint, TSESTree } from '@typescript-eslint/utils'
5+
import { parse as parseComment_ } from 'comment-parser'
6+
import type { Block } from 'comment-parser'
57
import debug from 'debug'
6-
import type { Annotation } from 'doctrine'
7-
import * as doctrine from 'doctrine'
88
import type { AST } from 'eslint'
99
import { SourceCode } from 'eslint'
1010
import type { TsConfigJsonResolved, TsConfigResult } from 'get-tsconfig'
@@ -36,7 +36,7 @@ const tsconfigCache = new Map<string, TsConfigJsonResolved | null | undefined>()
3636

3737
export type DocStyleParsers = Record<
3838
DocStyle,
39-
(comments: TSESTree.Comment[]) => Annotation | undefined
39+
(comments: TSESTree.Comment[]) => Block | undefined
4040
>
4141

4242
export interface DeclarationMetadata {
@@ -47,7 +47,7 @@ export interface DeclarationMetadata {
4747
}
4848

4949
export interface ModuleNamespace {
50-
doc?: Annotation
50+
doc?: Block
5151
namespace?: ExportMap | null
5252
}
5353

@@ -67,6 +67,26 @@ const declTypes = new Set([
6767
'TSModuleDeclaration',
6868
])
6969

70+
// https://github.com/syavorsky/comment-parser/issues/172
71+
const fixup = new Set(['deprecated', 'module'])
72+
73+
const parseComment = (comment: string): Block => {
74+
const restored = `/**${comment.split('\n').reduce((acc, line) => {
75+
line = line.trim()
76+
return line && line !== '*' ? acc + '\n ' + line : acc
77+
}, '')}
78+
*/`
79+
const [doc] = parseComment_(restored)
80+
return {
81+
...doc,
82+
tags: doc.tags.map(t =>
83+
t.name && fixup.has(t.tag)
84+
? { ...t, description: `${t.name} ${t.description}` }
85+
: t,
86+
),
87+
}
88+
}
89+
7090
export class ExportMap {
7191
static for(context: ChildContext) {
7292
const filepath = context.path
@@ -663,20 +683,20 @@ export class ExportMap {
663683

664684
// attempt to collect module doc
665685
defineLazyProperty(m, 'doc', () => {
666-
if (ast.comments) {
667-
for (let i = 0, len = ast.comments.length; i < len; i++) {
668-
const c = ast.comments[i]
669-
if (c.type !== 'Block') {
670-
continue
671-
}
672-
try {
673-
const doc = doctrine.parse(c.value, { unwrap: true })
674-
if (doc.tags.some(t => t.title === 'module')) {
675-
return doc
676-
}
677-
} catch {
678-
/* ignore */
686+
if (!ast.comments?.length) {
687+
return
688+
}
689+
for (const c of ast.comments) {
690+
if (c.type !== 'Block') {
691+
continue
692+
}
693+
try {
694+
const doc = parseComment(c.value)
695+
if (doc.tags.some(t => t.tag === 'module')) {
696+
return doc
679697
}
698+
} catch {
699+
/* ignore */
680700
}
681701
}
682702
})
@@ -727,7 +747,7 @@ export class ExportMap {
727747

728748
declare private mtime: number
729749

730-
declare doc: Annotation | undefined
750+
declare doc: Block | undefined
731751

732752
constructor(public path: string) {}
733753

@@ -943,7 +963,7 @@ function captureDoc(
943963
...nodes: Array<TSESTree.Node | undefined>
944964
) {
945965
const metadata: {
946-
doc?: Annotation | undefined
966+
doc?: Block | undefined
947967
} = {}
948968

949969
defineLazyProperty(metadata, 'doc', () => {
@@ -1002,15 +1022,15 @@ function captureJsDoc(comments: TSESTree.Comment[]) {
10021022
continue
10031023
}
10041024
try {
1005-
return doctrine.parse(comment.value, { unwrap: true })
1025+
return parseComment(comment.value)
10061026
} catch {
10071027
/* don't care, for now? maybe add to `errors?` */
10081028
}
10091029
}
10101030
}
10111031

10121032
/** Parse TomDoc section from comments */
1013-
function captureTomDoc(comments: TSESTree.Comment[]): Annotation | undefined {
1033+
function captureTomDoc(comments: TSESTree.Comment[]): Block | undefined {
10141034
// collect lines up to first paragraph break
10151035
const lines = []
10161036
for (const comment of comments) {
@@ -1020,7 +1040,7 @@ function captureTomDoc(comments: TSESTree.Comment[]): Annotation | undefined {
10201040
lines.push(comment.value.trim())
10211041
}
10221042

1023-
// return doctrine-like object
1043+
// return comment-parser-like object
10241044
const statusMatch = lines
10251045
.join(' ')
10261046
.match(/^(Public|Internal|Deprecated):\s*(.+)/)
@@ -1029,11 +1049,11 @@ function captureTomDoc(comments: TSESTree.Comment[]): Annotation | undefined {
10291049
description: statusMatch[2],
10301050
tags: [
10311051
{
1032-
title: statusMatch[1].toLowerCase(),
1052+
tag: statusMatch[1].toLowerCase(),
10331053
description: statusMatch[2],
10341054
},
10351055
],
1036-
}
1056+
} as Block
10371057
}
10381058
}
10391059

src/utils/resolve.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,9 @@ import {
1818
} from './legacy-resolver-settings.js'
1919
import { ModuleCache } from './module-cache.js'
2020

21-
const _filename =
22-
typeof __filename === 'undefined'
23-
? fileURLToPath(import.meta.url)
24-
: /* istanbul ignore next */ __filename
21+
const _filename = import.meta.url
22+
? fileURLToPath(import.meta.url)
23+
: /* istanbul ignore next */ __filename
2524
const _dirname = path.dirname(_filename)
2625

2726
export const CASE_SENSITIVE_FS = !fs.existsSync(

test/rules/no-extraneous-dependencies.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ ruleTester.run('no-extraneous-dependencies', rule, {
109109
}),
110110
tValid({ code: 'require(6)' }),
111111
tValid({
112-
code: 'import "doctrine"',
112+
code: 'import "comment-parser"',
113113
options: [{ packageDir: path.join(_dirname, '../../') }],
114114
}),
115115
tValid({

test/utils/export-map.spec.ts

+6-9
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,7 @@ function jsdocTests(parseContext: ChildContext, lineEnding: string) {
3131
it('works with named imports.', () => {
3232
expect(imports.has('fn')).toBe(true)
3333

34-
expect(imports.get('fn')).toHaveProperty(
35-
'doc.tags[0].title',
36-
'deprecated',
37-
)
34+
expect(imports.get('fn')).toHaveProperty('doc.tags[0].tag', 'deprecated')
3835
expect(imports.get('fn')).toHaveProperty(
3936
'doc.tags[0].description',
4037
"Please use 'x' instead.",
@@ -45,7 +42,7 @@ function jsdocTests(parseContext: ChildContext, lineEnding: string) {
4542
expect(imports.has('default')).toBe(true)
4643
const importMeta = imports.get('default')
4744

48-
expect(importMeta).toHaveProperty('doc.tags[0].title', 'deprecated')
45+
expect(importMeta).toHaveProperty('doc.tags[0].tag', 'deprecated')
4946
expect(importMeta).toHaveProperty(
5047
'doc.tags[0].description',
5148
'This is awful, use NotAsBadClass.',
@@ -56,7 +53,7 @@ function jsdocTests(parseContext: ChildContext, lineEnding: string) {
5653
expect(imports.has('MY_TERRIBLE_ACTION')).toBe(true)
5754
const importMeta = imports.get('MY_TERRIBLE_ACTION')
5855

59-
expect(importMeta).toHaveProperty('doc.tags[0].title', 'deprecated')
56+
expect(importMeta).toHaveProperty('doc.tags[0].tag', 'deprecated')
6057
expect(importMeta).toHaveProperty(
6158
'doc.tags[0].description',
6259
'Please stop sending/handling this action type.',
@@ -68,7 +65,7 @@ function jsdocTests(parseContext: ChildContext, lineEnding: string) {
6865
expect(imports.has('CHAIN_A')).toBe(true)
6966
const importMeta = imports.get('CHAIN_A')
7067

71-
expect(importMeta).toHaveProperty('doc.tags[0].title', 'deprecated')
68+
expect(importMeta).toHaveProperty('doc.tags[0].tag', 'deprecated')
7269
expect(importMeta).toHaveProperty(
7370
'doc.tags[0].description',
7471
'This chain is awful',
@@ -78,14 +75,14 @@ function jsdocTests(parseContext: ChildContext, lineEnding: string) {
7875
expect(imports.has('CHAIN_B')).toBe(true)
7976
const importMeta = imports.get('CHAIN_B')
8077

81-
expect(importMeta).toHaveProperty('doc.tags[0].title', 'deprecated')
78+
expect(importMeta).toHaveProperty('doc.tags[0].tag', 'deprecated')
8279
expect(importMeta).toHaveProperty('doc.tags[0].description', 'So awful')
8380
})
8481
it('works for the third one, etc.', () => {
8582
expect(imports.has('CHAIN_C')).toBe(true)
8683
const importMeta = imports.get('CHAIN_C')
8784

88-
expect(importMeta).toHaveProperty('doc.tags[0].title', 'deprecated')
85+
expect(importMeta).toHaveProperty('doc.tags[0].tag', 'deprecated')
8986
expect(importMeta).toHaveProperty(
9087
'doc.tags[0].description',
9188
'Still terrible',

0 commit comments

Comments
 (0)