@api-extractor-tools/eslint-plugin 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +183 -0
- package/api-extractor.json +10 -0
- package/dist/configs/index.d.ts +6 -0
- package/dist/configs/index.d.ts.map +1 -0
- package/dist/configs/index.js +11 -0
- package/dist/configs/index.js.map +1 -0
- package/dist/configs/recommended.d.ts +31 -0
- package/dist/configs/recommended.d.ts.map +1 -0
- package/dist/configs/recommended.js +45 -0
- package/dist/configs/recommended.js.map +1 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +68 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/index.d.ts +14 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +20 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/missing-release-tag.d.ts +8 -0
- package/dist/rules/missing-release-tag.d.ts.map +1 -0
- package/dist/rules/missing-release-tag.js +148 -0
- package/dist/rules/missing-release-tag.js.map +1 -0
- package/dist/rules/override-keyword.d.ts +8 -0
- package/dist/rules/override-keyword.d.ts.map +1 -0
- package/dist/rules/override-keyword.js +106 -0
- package/dist/rules/override-keyword.js.map +1 -0
- package/dist/rules/package-documentation.d.ts +8 -0
- package/dist/rules/package-documentation.d.ts.map +1 -0
- package/dist/rules/package-documentation.js +70 -0
- package/dist/rules/package-documentation.js.map +1 -0
- package/dist/types.d.ts +90 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/config-loader.d.ts +47 -0
- package/dist/utils/config-loader.d.ts.map +1 -0
- package/dist/utils/config-loader.js +163 -0
- package/dist/utils/config-loader.js.map +1 -0
- package/dist/utils/entry-point.d.ts +56 -0
- package/dist/utils/entry-point.d.ts.map +1 -0
- package/dist/utils/entry-point.js +198 -0
- package/dist/utils/entry-point.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +21 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/tsdoc-parser.d.ts +58 -0
- package/dist/utils/tsdoc-parser.d.ts.map +1 -0
- package/dist/utils/tsdoc-parser.js +137 -0
- package/dist/utils/tsdoc-parser.js.map +1 -0
- package/package.json +44 -0
- package/src/configs/index.ts +6 -0
- package/src/configs/recommended.ts +46 -0
- package/src/index.ts +111 -0
- package/src/rules/index.ts +18 -0
- package/src/rules/missing-release-tag.ts +203 -0
- package/src/rules/override-keyword.ts +139 -0
- package/src/rules/package-documentation.ts +90 -0
- package/src/types.ts +104 -0
- package/src/utils/config-loader.ts +194 -0
- package/src/utils/entry-point.ts +247 -0
- package/src/utils/index.ts +17 -0
- package/src/utils/tsdoc-parser.ts +163 -0
- package/temp/eslint-plugin.api.md +118 -0
- package/test/index.test.ts +66 -0
- package/test/rules/missing-release-tag.test.ts +184 -0
- package/test/rules/override-keyword.test.ts +171 -0
- package/test/rules/package-documentation.test.ts +152 -0
- package/test/tsconfig.json +11 -0
- package/test/utils/config-loader.test.ts +199 -0
- package/test/utils/entry-point.test.ts +172 -0
- package/test/utils/tsdoc-parser.test.ts +113 -0
- package/tsconfig.json +12 -0
- package/vitest.config.mts +25 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tsdoc-parser.js","sourceRoot":"","sources":["../../src/utils/tsdoc-parser.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@api-extractor-tools/eslint-plugin",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "ESLint plugin providing authoring-time feedback for API Extractor",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/eslint-plugin-public.d.ts",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"eslint",
|
|
9
|
+
"eslint-plugin",
|
|
10
|
+
"api-extractor",
|
|
11
|
+
"tsdoc",
|
|
12
|
+
"typescript"
|
|
13
|
+
],
|
|
14
|
+
"author": "",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"eslint": ">=8.0.0",
|
|
18
|
+
"typescript": ">=5.5.0"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@microsoft/tsdoc": "^0.15.1",
|
|
22
|
+
"@typescript-eslint/utils": "^8.48.1"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@microsoft/api-extractor": "^7.55.1",
|
|
26
|
+
"@types/node": "^22.10.2",
|
|
27
|
+
"@typescript-eslint/parser": "^8.48.1",
|
|
28
|
+
"@typescript-eslint/rule-tester": "^8.48.1",
|
|
29
|
+
"eslint": "^9.39.1",
|
|
30
|
+
"typescript": "5.8.3",
|
|
31
|
+
"@vitest/coverage-v8": "^4.0.15",
|
|
32
|
+
"vitest": "^4.0.15"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"generate:api-report": "api-extractor run --local --verbose",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:coverage": "vitest run --coverage",
|
|
39
|
+
"check": "pnpm check:eslint && pnpm check:typecheck-tests && pnpm check:api-report",
|
|
40
|
+
"check:eslint": "eslint src",
|
|
41
|
+
"check:typecheck-tests": "tsc -p test/tsconfig.json",
|
|
42
|
+
"check:api-report": "api-extractor run"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recommended ESLint configuration for API Extractor.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* This configuration works with both flat config (eslint.config.js) and
|
|
6
|
+
* legacy config (.eslintrc.js) formats.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { TSESLint } from '@typescript-eslint/utils'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Recommended rule configuration.
|
|
15
|
+
* These are the rules enabled by default with appropriate severity.
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
export const recommendedRules: TSESLint.Linter.RulesRecord = {
|
|
19
|
+
'@api-extractor-tools/missing-release-tag': 'warn',
|
|
20
|
+
'@api-extractor-tools/override-keyword': 'error',
|
|
21
|
+
'@api-extractor-tools/package-documentation': 'warn',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Flat-config recommended configuration.
|
|
26
|
+
*
|
|
27
|
+
* @remarks
|
|
28
|
+
* Use with eslint.config.js:
|
|
29
|
+
* ```js
|
|
30
|
+
* import apiExtractorPlugin from '@api-extractor-tools/eslint-plugin';
|
|
31
|
+
*
|
|
32
|
+
* export default [
|
|
33
|
+
* apiExtractorPlugin.configs.recommended,
|
|
34
|
+
* ];
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function createFlatRecommendedConfig(
|
|
38
|
+
plugin: Record<string, unknown>,
|
|
39
|
+
): TSESLint.FlatConfig.Config {
|
|
40
|
+
return {
|
|
41
|
+
plugins: {
|
|
42
|
+
'@api-extractor-tools': plugin as TSESLint.FlatConfig.Plugin,
|
|
43
|
+
},
|
|
44
|
+
rules: recommendedRules,
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint plugin providing authoring-time feedback for API Extractor.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* This plugin provides ESLint rules that mirror API Extractor's validations,
|
|
6
|
+
* enabling developers to catch issues during development rather than at build time.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* Using with flat config (eslint.config.js):
|
|
10
|
+
* ```js
|
|
11
|
+
* import apiExtractorPlugin from '@api-extractor-tools/eslint-plugin';
|
|
12
|
+
*
|
|
13
|
+
* export default [
|
|
14
|
+
* apiExtractorPlugin.configs.recommended,
|
|
15
|
+
* // Or configure rules individually:
|
|
16
|
+
* {
|
|
17
|
+
* plugins: {
|
|
18
|
+
* '@api-extractor-tools': apiExtractorPlugin,
|
|
19
|
+
* },
|
|
20
|
+
* rules: {
|
|
21
|
+
* '@api-extractor-tools/missing-release-tag': 'error',
|
|
22
|
+
* },
|
|
23
|
+
* },
|
|
24
|
+
* ];
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* Using with legacy config (.eslintrc.js):
|
|
29
|
+
* ```js
|
|
30
|
+
* module.exports = {
|
|
31
|
+
* plugins: ['@api-extractor-tools'],
|
|
32
|
+
* extends: ['plugin:@api-extractor-tools/recommended-legacy'],
|
|
33
|
+
* };
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @packageDocumentation
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import type { TSESLint } from '@typescript-eslint/utils'
|
|
40
|
+
import { rules } from './rules'
|
|
41
|
+
import { recommendedRules, createFlatRecommendedConfig } from './configs'
|
|
42
|
+
|
|
43
|
+
// Re-export types
|
|
44
|
+
export type {
|
|
45
|
+
ApiExtractorConfig,
|
|
46
|
+
ApiExtractorLogLevel,
|
|
47
|
+
ApiExtractorMessagesConfig,
|
|
48
|
+
MessageConfig,
|
|
49
|
+
ReleaseTag,
|
|
50
|
+
MissingReleaseTagRuleOptions,
|
|
51
|
+
OverrideKeywordRuleOptions,
|
|
52
|
+
PackageDocumentationRuleOptions,
|
|
53
|
+
ResolvedEntryPoints,
|
|
54
|
+
} from './types'
|
|
55
|
+
|
|
56
|
+
export { RELEASE_TAGS } from './types'
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Plugin configuration type.
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
interface PluginConfigs {
|
|
63
|
+
recommended: TSESLint.FlatConfig.Config
|
|
64
|
+
'recommended-legacy': {
|
|
65
|
+
plugins: string[]
|
|
66
|
+
rules: TSESLint.Linter.RulesRecord
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The ESLint plugin type.
|
|
72
|
+
* @public
|
|
73
|
+
*/
|
|
74
|
+
export interface ApiExtractorEslintPlugin {
|
|
75
|
+
meta: {
|
|
76
|
+
name: string
|
|
77
|
+
version: string
|
|
78
|
+
}
|
|
79
|
+
rules: typeof rules
|
|
80
|
+
configs: PluginConfigs
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The ESLint plugin object.
|
|
85
|
+
* @public
|
|
86
|
+
*/
|
|
87
|
+
const plugin: ApiExtractorEslintPlugin = {
|
|
88
|
+
meta: {
|
|
89
|
+
name: '@api-extractor-tools/eslint-plugin',
|
|
90
|
+
version: '0.0.1',
|
|
91
|
+
},
|
|
92
|
+
rules,
|
|
93
|
+
configs: {
|
|
94
|
+
recommended: null as unknown as TSESLint.FlatConfig.Config,
|
|
95
|
+
'recommended-legacy': {
|
|
96
|
+
plugins: ['@api-extractor-tools'],
|
|
97
|
+
rules: recommendedRules,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Add flat config after plugin is defined (needed for self-reference)
|
|
103
|
+
plugin.configs.recommended = createFlatRecommendedConfig(
|
|
104
|
+
plugin as unknown as Record<string, unknown>,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
export default plugin
|
|
108
|
+
|
|
109
|
+
// Named exports for CommonJS compatibility
|
|
110
|
+
export { rules }
|
|
111
|
+
export { recommendedRules }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rules for API Extractor.
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { missingReleaseTag } from './missing-release-tag'
|
|
7
|
+
import { overrideKeyword } from './override-keyword'
|
|
8
|
+
import { packageDocumentation } from './package-documentation'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* All available ESLint rules.
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
export const rules = {
|
|
15
|
+
'missing-release-tag': missingReleaseTag,
|
|
16
|
+
'override-keyword': overrideKeyword,
|
|
17
|
+
'package-documentation': packageDocumentation,
|
|
18
|
+
} as const
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule for detecting missing release tags on exported symbols.
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'
|
|
7
|
+
import {
|
|
8
|
+
resolveConfig,
|
|
9
|
+
getMessageLogLevel,
|
|
10
|
+
getLeadingTSDocComment,
|
|
11
|
+
parseTSDocComment,
|
|
12
|
+
extractReleaseTag,
|
|
13
|
+
} from '../utils'
|
|
14
|
+
import type { MissingReleaseTagRuleOptions } from '../types'
|
|
15
|
+
|
|
16
|
+
const createRule = ESLintUtils.RuleCreator(
|
|
17
|
+
(name) =>
|
|
18
|
+
`https://github.com/mike-north/api-extractor-tools/blob/main/tools/eslint-plugin/docs/rules/${name}.md`,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
type MessageIds = 'missingReleaseTag'
|
|
22
|
+
|
|
23
|
+
export const missingReleaseTag = createRule<
|
|
24
|
+
[MissingReleaseTagRuleOptions],
|
|
25
|
+
MessageIds
|
|
26
|
+
>({
|
|
27
|
+
name: 'missing-release-tag',
|
|
28
|
+
meta: {
|
|
29
|
+
type: 'problem',
|
|
30
|
+
docs: {
|
|
31
|
+
description:
|
|
32
|
+
'Require exported symbols to have a release tag (@public, @beta, @alpha, or @internal)',
|
|
33
|
+
},
|
|
34
|
+
messages: {
|
|
35
|
+
missingReleaseTag:
|
|
36
|
+
'Exported symbol "{{name}}" is missing a release tag. Add @public, @beta, @alpha, or @internal to its TSDoc comment.',
|
|
37
|
+
},
|
|
38
|
+
schema: [
|
|
39
|
+
{
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
configPath: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
description: 'Path to api-extractor.json configuration file',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
defaultOptions: [{}],
|
|
52
|
+
create(context) {
|
|
53
|
+
const options = context.options[0] ?? {}
|
|
54
|
+
const filename = context.filename
|
|
55
|
+
const config = resolveConfig(filename, options.configPath)
|
|
56
|
+
const logLevel = getMessageLogLevel(config, 'ae-missing-release-tag')
|
|
57
|
+
|
|
58
|
+
// If configured to 'none', disable the rule
|
|
59
|
+
if (logLevel === 'none') {
|
|
60
|
+
return {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const sourceCode = context.sourceCode
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Checks if a node has a release tag in its TSDoc comment.
|
|
67
|
+
*/
|
|
68
|
+
function hasReleaseTag(node: TSESTree.Node): boolean {
|
|
69
|
+
const commentText = getLeadingTSDocComment(sourceCode, node)
|
|
70
|
+
if (!commentText) {
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const parsed = parseTSDocComment(commentText)
|
|
75
|
+
if (parsed.docComment) {
|
|
76
|
+
const tag = extractReleaseTag(parsed.docComment)
|
|
77
|
+
return tag !== undefined
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Gets the name of a declaration.
|
|
85
|
+
*/
|
|
86
|
+
function getDeclarationName(
|
|
87
|
+
node:
|
|
88
|
+
| TSESTree.FunctionDeclaration
|
|
89
|
+
| TSESTree.ClassDeclaration
|
|
90
|
+
| TSESTree.TSInterfaceDeclaration
|
|
91
|
+
| TSESTree.TSTypeAliasDeclaration
|
|
92
|
+
| TSESTree.TSEnumDeclaration
|
|
93
|
+
| TSESTree.VariableDeclaration,
|
|
94
|
+
): string {
|
|
95
|
+
if (node.type === AST_NODE_TYPES.VariableDeclaration) {
|
|
96
|
+
const firstDeclarator = node.declarations[0]
|
|
97
|
+
if (firstDeclarator?.id.type === AST_NODE_TYPES.Identifier) {
|
|
98
|
+
return firstDeclarator.id.name
|
|
99
|
+
}
|
|
100
|
+
return '<unknown>'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if ('id' in node && node.id) {
|
|
104
|
+
return node.id.name
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return '<anonymous>'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Reports a missing release tag error.
|
|
112
|
+
*/
|
|
113
|
+
function reportMissingTag(node: TSESTree.Node, name: string): void {
|
|
114
|
+
context.report({
|
|
115
|
+
node,
|
|
116
|
+
messageId: 'missingReleaseTag',
|
|
117
|
+
data: { name },
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Checks if a node is exported.
|
|
123
|
+
*/
|
|
124
|
+
function isExported(node: TSESTree.Node): boolean {
|
|
125
|
+
const parent = node.parent
|
|
126
|
+
|
|
127
|
+
// Direct export: export function foo() {}
|
|
128
|
+
if (parent?.type === AST_NODE_TYPES.ExportNamedDeclaration) {
|
|
129
|
+
return true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Default export: export default function foo() {}
|
|
133
|
+
if (parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration) {
|
|
134
|
+
return true
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return false
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Checks a declaration node for missing release tag.
|
|
142
|
+
*/
|
|
143
|
+
function checkDeclaration(
|
|
144
|
+
node:
|
|
145
|
+
| TSESTree.FunctionDeclaration
|
|
146
|
+
| TSESTree.ClassDeclaration
|
|
147
|
+
| TSESTree.TSInterfaceDeclaration
|
|
148
|
+
| TSESTree.TSTypeAliasDeclaration
|
|
149
|
+
| TSESTree.TSEnumDeclaration
|
|
150
|
+
| TSESTree.VariableDeclaration,
|
|
151
|
+
): void {
|
|
152
|
+
if (!isExported(node)) {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// For exported declarations, check the export statement for the comment
|
|
157
|
+
const exportNode = node.parent
|
|
158
|
+
const nodeToCheck = exportNode ?? node
|
|
159
|
+
|
|
160
|
+
if (!hasReleaseTag(nodeToCheck) && !hasReleaseTag(node)) {
|
|
161
|
+
const name = getDeclarationName(node)
|
|
162
|
+
reportMissingTag(node, name)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
// Check exported function declarations
|
|
168
|
+
FunctionDeclaration(node): void {
|
|
169
|
+
checkDeclaration(node)
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// Check exported class declarations
|
|
173
|
+
ClassDeclaration(node): void {
|
|
174
|
+
checkDeclaration(node)
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
// Check exported interface declarations
|
|
178
|
+
TSInterfaceDeclaration(node): void {
|
|
179
|
+
checkDeclaration(node)
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// Check exported type alias declarations
|
|
183
|
+
TSTypeAliasDeclaration(node): void {
|
|
184
|
+
checkDeclaration(node)
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
// Check exported enum declarations
|
|
188
|
+
TSEnumDeclaration(node): void {
|
|
189
|
+
checkDeclaration(node)
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
// Check exported variable declarations
|
|
193
|
+
VariableDeclaration(node): void {
|
|
194
|
+
checkDeclaration(node)
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
// Note: We intentionally don't handle ExportNamedDeclaration with specifiers
|
|
198
|
+
// (e.g., `export { foo }` or `export { foo } from './bar'`) because:
|
|
199
|
+
// - Re-exports should have release tags on the original declaration
|
|
200
|
+
// - Named exports reference declarations that are checked elsewhere
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
})
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule requiring the TypeScript `override` keyword when `@override` TSDoc tag is used.
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'
|
|
7
|
+
import {
|
|
8
|
+
getLeadingTSDocComment,
|
|
9
|
+
parseTSDocComment,
|
|
10
|
+
hasOverrideTag,
|
|
11
|
+
} from '../utils'
|
|
12
|
+
import type { OverrideKeywordRuleOptions } from '../types'
|
|
13
|
+
|
|
14
|
+
const createRule = ESLintUtils.RuleCreator(
|
|
15
|
+
(name) =>
|
|
16
|
+
`https://github.com/mike-north/api-extractor-tools/blob/main/tools/eslint-plugin/docs/rules/${name}.md`,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
type MessageIds = 'missingOverrideKeyword'
|
|
20
|
+
|
|
21
|
+
export const overrideKeyword = createRule<
|
|
22
|
+
[OverrideKeywordRuleOptions],
|
|
23
|
+
MessageIds
|
|
24
|
+
>({
|
|
25
|
+
name: 'override-keyword',
|
|
26
|
+
meta: {
|
|
27
|
+
type: 'problem',
|
|
28
|
+
docs: {
|
|
29
|
+
description:
|
|
30
|
+
'Require the TypeScript `override` keyword when the `@override` TSDoc tag is present',
|
|
31
|
+
},
|
|
32
|
+
fixable: 'code',
|
|
33
|
+
messages: {
|
|
34
|
+
missingOverrideKeyword:
|
|
35
|
+
'Member "{{name}}" has @override TSDoc tag but is missing the TypeScript `override` keyword.',
|
|
36
|
+
},
|
|
37
|
+
schema: [
|
|
38
|
+
{
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
configPath: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: 'Path to api-extractor.json configuration file',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
defaultOptions: [{}],
|
|
51
|
+
create(context) {
|
|
52
|
+
const sourceCode = context.sourceCode
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Checks if a node has the @override TSDoc tag.
|
|
56
|
+
*/
|
|
57
|
+
function nodeHasOverrideTag(node: TSESTree.Node): boolean {
|
|
58
|
+
const commentText = getLeadingTSDocComment(sourceCode, node)
|
|
59
|
+
if (!commentText) {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const parsed = parseTSDocComment(commentText)
|
|
64
|
+
if (parsed.docComment) {
|
|
65
|
+
return hasOverrideTag(parsed.docComment)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Checks if a method or property has the override keyword.
|
|
73
|
+
*/
|
|
74
|
+
function hasOverrideModifier(
|
|
75
|
+
node: TSESTree.MethodDefinition | TSESTree.PropertyDefinition,
|
|
76
|
+
): boolean {
|
|
77
|
+
return node.override === true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Gets the name of a class member.
|
|
82
|
+
*/
|
|
83
|
+
function getMemberName(
|
|
84
|
+
node: TSESTree.MethodDefinition | TSESTree.PropertyDefinition,
|
|
85
|
+
): string {
|
|
86
|
+
if (node.key.type === AST_NODE_TYPES.Identifier) {
|
|
87
|
+
return node.key.name
|
|
88
|
+
}
|
|
89
|
+
if (node.key.type === AST_NODE_TYPES.Literal) {
|
|
90
|
+
return String(node.key.value)
|
|
91
|
+
}
|
|
92
|
+
return '<computed>'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Checks a class member for missing override keyword.
|
|
97
|
+
*/
|
|
98
|
+
function checkMember(
|
|
99
|
+
node: TSESTree.MethodDefinition | TSESTree.PropertyDefinition,
|
|
100
|
+
): void {
|
|
101
|
+
// Skip constructors
|
|
102
|
+
if (
|
|
103
|
+
node.type === AST_NODE_TYPES.MethodDefinition &&
|
|
104
|
+
node.kind === 'constructor'
|
|
105
|
+
) {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!nodeHasOverrideTag(node)) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (hasOverrideModifier(node)) {
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const name = getMemberName(node)
|
|
118
|
+
|
|
119
|
+
context.report({
|
|
120
|
+
node,
|
|
121
|
+
messageId: 'missingOverrideKeyword',
|
|
122
|
+
data: { name },
|
|
123
|
+
fix(fixer) {
|
|
124
|
+
// Insert override keyword at the start of the member declaration
|
|
125
|
+
return fixer.insertTextBefore(node, 'override ')
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
MethodDefinition(node): void {
|
|
132
|
+
checkMember(node)
|
|
133
|
+
},
|
|
134
|
+
PropertyDefinition(node): void {
|
|
135
|
+
checkMember(node)
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
})
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule requiring @packageDocumentation in package entry point files.
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ESLintUtils } from '@typescript-eslint/utils'
|
|
7
|
+
import {
|
|
8
|
+
findPackageJson,
|
|
9
|
+
isEntryPoint,
|
|
10
|
+
findAllTSDocComments,
|
|
11
|
+
hasPackageDocumentation,
|
|
12
|
+
} from '../utils'
|
|
13
|
+
import type { PackageDocumentationRuleOptions } from '../types'
|
|
14
|
+
|
|
15
|
+
const createRule = ESLintUtils.RuleCreator(
|
|
16
|
+
(name) =>
|
|
17
|
+
`https://github.com/mike-north/api-extractor-tools/blob/main/tools/eslint-plugin/docs/rules/${name}.md`,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
type MessageIds = 'missingPackageDocumentation'
|
|
21
|
+
|
|
22
|
+
export const packageDocumentation = createRule<
|
|
23
|
+
[PackageDocumentationRuleOptions],
|
|
24
|
+
MessageIds
|
|
25
|
+
>({
|
|
26
|
+
name: 'package-documentation',
|
|
27
|
+
meta: {
|
|
28
|
+
type: 'suggestion',
|
|
29
|
+
docs: {
|
|
30
|
+
description:
|
|
31
|
+
'Require @packageDocumentation tag in package entry point files',
|
|
32
|
+
},
|
|
33
|
+
messages: {
|
|
34
|
+
missingPackageDocumentation:
|
|
35
|
+
'Entry point file is missing a @packageDocumentation comment. Add a TSDoc comment with @packageDocumentation at the top of the file.',
|
|
36
|
+
},
|
|
37
|
+
schema: [
|
|
38
|
+
{
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
configPath: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: 'Path to api-extractor.json configuration file',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
defaultOptions: [{}],
|
|
51
|
+
create(context) {
|
|
52
|
+
const filename = context.filename
|
|
53
|
+
const sourceCode = context.sourceCode
|
|
54
|
+
|
|
55
|
+
// Find package.json
|
|
56
|
+
const pkgPath = findPackageJson(filename)
|
|
57
|
+
if (!pkgPath) {
|
|
58
|
+
// No package.json found, nothing to check
|
|
59
|
+
return {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check if this file is an entry point
|
|
63
|
+
if (!isEntryPoint(filename, pkgPath)) {
|
|
64
|
+
// Not an entry point, skip
|
|
65
|
+
return {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
Program(node): void {
|
|
70
|
+
// Look for @packageDocumentation in any TSDoc comment in the file
|
|
71
|
+
const tsdocComments = findAllTSDocComments(sourceCode)
|
|
72
|
+
|
|
73
|
+
for (const { parsed } of tsdocComments) {
|
|
74
|
+
if (parsed.docComment && hasPackageDocumentation(parsed.docComment)) {
|
|
75
|
+
// Found @packageDocumentation, all good
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// No @packageDocumentation found
|
|
81
|
+
// Report at the first line of the file
|
|
82
|
+
context.report({
|
|
83
|
+
node,
|
|
84
|
+
loc: { line: 1, column: 0 },
|
|
85
|
+
messageId: 'missingPackageDocumentation',
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
})
|