@lewishowles/lint-config 0.2.0 → 0.3.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @lewishowles/lint-config
2
2
 
3
- Shared oxlint configuration for Lewis Howles projects. One package that every repo extends, so lint setup stops being duplicated and drifting across the ecosystem.
3
+ Shared Oxlint configuration for Lewis Howles projects. Projects extend this package to keep their lint configuration consistent across projects, instead of copying and maintaining the same rules everywhere.
4
4
 
5
5
  ## Installation
6
6
 
@@ -19,28 +19,86 @@ Create a `.oxlintrc.json` in your project root that extends the appropriate laye
19
19
  ```json
20
20
  {
21
21
  "extends": ["./node_modules/@lewishowles/lint-config/base.json"],
22
+ "env": { "builtin": true, "browser": true },
22
23
  "ignorePatterns": ["**/dist/*", ".codebase-memory/**"]
23
24
  }
24
25
  ```
25
26
 
27
+ Note that `env` has to be redeclared here: Oxlint doesn't yet merge it through `extends`, so `base.json`'s own `env` never reaches your project. See [known limitations](docs/limitations.md) for why.
28
+
26
29
  ### Vue layer (Vue 3 projects)
27
30
 
28
31
  ```json
29
32
  {
30
33
  "extends": ["./node_modules/@lewishowles/lint-config/vue.json"],
34
+ "env": { "builtin": true, "browser": true },
35
+ "globals": {
36
+ "defineEmits": "readonly",
37
+ "defineExpose": "readonly",
38
+ "defineModel": "readonly",
39
+ "defineOptions": "readonly",
40
+ "defineProps": "readonly",
41
+ "defineSlots": "readonly",
42
+ "withDefaults": "readonly"
43
+ },
44
+ "ignorePatterns": ["**/dist/*", ".codebase-memory/**"]
45
+ }
46
+ ```
47
+
48
+ The Vue layer extends `base.json` internally, so you only need to extend `vue.json`. The same `env`/`globals` limitation applies here too, which is why both are redeclared above.
49
+
50
+ ### Comment formatting (optional)
51
+
52
+ Add the comments layer alongside the base or Vue layer to enforce the comment-formatting rules, variable-declaration documentation, JSDoc on named functions and first-level object methods, documentation directly after each Vue `<script setup>` opening tag, and block comments for runtime `defineProps` properties:
53
+
54
+ ```json
55
+ {
56
+ "extends": [
57
+ "./node_modules/@lewishowles/lint-config/base.json",
58
+ "./node_modules/@lewishowles/lint-config/comments.json"
59
+ ],
60
+ "env": { "builtin": true, "browser": true },
31
61
  "ignorePatterns": ["**/dist/*", ".codebase-memory/**"]
32
62
  }
33
63
  ```
34
64
 
35
- The Vue layer extends `base.json` internally, so you only need to extend `vue.json`.
65
+ The Vue component rule reads the raw `.vue` file because Oxlint's JS Plugin API only receives the extracted script block. The comments layer loads its plugin for you, so there's no relative `jsPlugins` path to add. To pick rules yourself instead, add the plugin directly:
66
+
67
+ ```json
68
+ {
69
+ "jsPlugins": [
70
+ {
71
+ "name": "comments",
72
+ "specifier": "@lewishowles/lint-config/comments/plugin"
73
+ }
74
+ ],
75
+ "rules": {
76
+ "comments/line-comments": "error"
77
+ }
78
+ }
79
+ ```
80
+
81
+ The `comments/vue-prop-documentation` rule requires an indented block comment immediately before every runtime property in `defineProps`. A matching comment on a `defineProps` property also documents its `withDefaults` entry; type-only props are not checked.
82
+
83
+ The `comments/vue-emit-documentation` rule requires an indented block comment immediately before every runtime property in `defineEmits`. Function-valued events also require the normal JSDoc tags; array-form and type-only emits are not checked.
84
+
85
+ The `comments/configured-api-calls` rule requires an immediately preceding line comment before configured bare-identifier calls such as Vue lifecycle hooks, reactive effects, and `onClickOutside`. A documented variable declaration covers a direct call initializer; member-expression calls are out of scope. Add project-specific APIs without replacing the built-in list:
86
+
87
+ ```json
88
+ {
89
+ "rules": {
90
+ "comments/configured-api-calls": ["error", { "additionalApis": ["subscribe"] }]
91
+ }
92
+ }
93
+ ```
36
94
 
37
95
  ## Customising
38
96
 
39
- Your `.oxlintrc.json` stub can override rules, add ignore patterns, add overrides, or add plugins on top of the shared layer.
97
+ Your project's `.oxlintrc.json` can override rules, add ignore patterns, add overrides, or add plugins on top of the shared layer.
40
98
 
41
99
  ### Overriding a rule
42
100
 
43
- To change the severity or options of a rule defined in the shared layer, redeclare it in your stub: your value wins.
101
+ To change the severity or options of a rule defined in the shared layer, redeclare it in your project config: your value wins.
44
102
 
45
103
  ```json
46
104
  {
@@ -53,7 +111,7 @@ To change the severity or options of a rule defined in the shared layer, redecla
53
111
 
54
112
  ### Adding ignore patterns
55
113
 
56
- Ignore patterns are repo-specific, so they always live in your stub:
114
+ Ignore patterns are project-specific, so they always live in your project config:
57
115
 
58
116
  ```json
59
117
  {
@@ -84,75 +142,32 @@ Overrides are additive: shared overrides (if any) still apply, and your local on
84
142
 
85
143
  ### Adding plugins
86
144
 
87
- Plugins are additive and deduplicated: your local plugins are added to the shared ones. Note that oxlint only supports its built-in plugin names (`oxc`, `typescript`, `unicorn`, `vue`, etc.); there is no `playwright` or `vitest` plugin. Test-file-specific behaviour is handled via `overrides`, not plugins.
145
+ Plugins are additive and deduplicated: your local plugins are added to the shared ones. Oxlint's `plugins` field only accepts built-in plugin names, such as `oxc`, `typescript`, `unicorn`, and `vue`; there's no `playwright` or `vitest` plugin. Custom JS plugins, like this package's `comments` plugin, load through `jsPlugins` instead. Test-file-specific behaviour is handled via `overrides`, not plugins.
88
146
 
89
147
  ## Layers
90
148
 
91
- | Layer | File | Contents |
92
- | ------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
93
- | `base` | `base.json` | Correctness rules, named-import member sorting, `@stylistic` formatting rules, `vite-plus/prefer-vite-plus-imports`, `oxc` + `typescript` + `unicorn` plugins, browser env, node env for config files (`vite.config.*`, `vitest.config.*`, `playwright*.config.*`) |
94
- | `vue` | `vue.json` | Extends `base`. Adds `vue` plugin, Vue compiler macros as globals, Vue-specific rules |
149
+ | Layer | File | Contents |
150
+ | ---------- | --------------- | -------------------------------------------------------------------------------------- |
151
+ | `base` | `base.json` | Correctness and formatting rules, import sorting, `oxc`/`typescript`/`unicorn` plugins |
152
+ | `comments` | `comments.json` | Optional comment-formatting rules, variable-declaration documentation, JSDoc checks |
153
+ | `vue` | `vue.json` | Extends `base`, adds the `vue` plugin, Vue compiler macro globals, Vue-specific rules |
95
154
 
96
- ### Import sorting ownership
155
+ ### Import sorting
97
156
 
98
- The base layer sorts named members within each import declaration. It sets `ignoreDeclarationSort: true` because Oxlint reports declaration-row ordering but does not auto-fix it.
99
-
100
- Consumers that want import declaration rows sorted should enable Oxfmt's `sortImports` option in their local `.oxfmtrc.json`. This keeps member sorting in the shared Oxlint layer and declaration ordering in the formatter that can fix it.
157
+ The base layer sorts named members within each import statement, but leaves declaration order (which import comes first) to Oxfmt: enable Oxfmt's `sortImports` option in your local `.oxfmtrc.json` if you want that sorted and fixed automatically.
101
158
 
102
159
  ## What stays repo-local
103
160
 
104
- - `ignorePatterns`, since every repo has different build output and tool directories
105
- - `overrides` for repo-specific directories (e.g. `bin/**/*.js`, `src/cli/**/*.js`, `src/playwright/**/*.js`), since the file paths differ per repo and can't be generalised
106
- - Test-file rule relaxations (e.g. turning off `vite-plus/prefer-vite-plus-imports` in `*.d.ts`)
107
- - Additional plugins, only for repos that need them
161
+ - `ignorePatterns`, since every project has different build output and tool directories
162
+ - `overrides` for project-specific directories (e.g. `bin/**/*.js`, `src/cli/**/*.js`, `src/playwright/**/*.js`), since the file paths differ per project and can't be generalised
163
+ - Rule relaxations for specific file patterns (e.g. turning off `vite-plus/prefer-vite-plus-imports` in generated `.d.ts` files)
164
+ - Additional plugins, only for projects that need them
108
165
 
109
166
  ## Merge semantics
110
167
 
111
- When a consumer stub extends a shared layer:
168
+ When a project's `.oxlintrc.json` extends a shared layer:
112
169
 
113
- - **Rules** shallow-merge by key: the consumer's value wins for any rule defined in both
170
+ - **Rules** shallow-merge by key: your value wins for any rule defined in both
114
171
  - **Overrides** are additive: both shared and local `overrides` entries apply, including any `env` declared inside an override block
115
- - **Plugins** are additive: both shared and local `plugins`/`jsPlugins` are loaded (deduplicated)
116
-
117
- ### Known oxlint limitation: top-level `env`, `globals`, and `ignorePatterns` don't merge through `extends`
118
-
119
- oxlint currently drops top-level `env`, `globals`, and `ignorePatterns` from an extended config file entirely: they only take effect if declared directly in the file oxlint is invoked with. This is an open upstream bug: [oxc-project/oxc#20087](https://github.com/oxc-project/oxc/issues/20087) (open as of oxlint 1.72.0).
120
-
121
- In practice this means:
122
-
123
- - `base.json`'s `env` (`builtin`, `browser`) and `vue.json`'s Vue macro `globals` (`defineProps`, `defineEmits`, etc.) will **not** reach a consumer that only does `{ "extends": ["./node_modules/@lewishowles/lint-config/vue.json"] }`: every global from the shared layer will be flagged by `no-undef`.
124
- - Any `ignorePatterns` this package might declare would be silently dropped the same way, so it deliberately ships none. See "What stays repo-local" below.
125
-
126
- Until this is fixed upstream, redeclare the `env`/`globals` you need directly in your project's `.oxlintrc.json`, even though `base.json`/`vue.json` already declare them:
127
-
128
- ```json
129
- {
130
- "extends": ["./node_modules/@lewishowles/lint-config/vue.json"],
131
- "env": { "builtin": true, "browser": true },
132
- "globals": {
133
- "defineEmits": "readonly",
134
- "defineExpose": "readonly",
135
- "defineModel": "readonly",
136
- "defineOptions": "readonly",
137
- "defineProps": "readonly",
138
- "defineSlots": "readonly",
139
- "withDefaults": "readonly"
140
- },
141
- "ignorePatterns": ["**/dist/*", ".codebase-memory/**"]
142
- }
143
- ```
144
-
145
- ### Known limitation: `vite-plus`'s `lint` config field requires resolved objects, not string paths
146
-
147
- Raw oxlint (CLI, editor integrations) accepts `"extends": ["./node_modules/@lewishowles/lint-config/vue.json"]` as string paths and resolves them at load time. `vite-plus`, when a project routes its oxlint config through `vite.config.js`'s `lint` field (importing `.oxlintrc.json` as JSON and handing it to `vp check`/`vp lint`), does not resolve string paths in `extends`: every entry, at every nesting level, must already be a plain object. This means `vue.json`'s own internal `extends: ["./base.json"]` also breaks one level deeper.
148
-
149
- If your project uses `vite-plus`'s `lint` field rather than raw oxlint, resolve the chain yourself in `vite.config.js`:
150
-
151
- ```js
152
- import base from "@lewishowles/lint-config/base.json" with { type: "json" };
153
- import vue from "@lewishowles/lint-config/vue.json" with { type: "json" };
154
-
155
- const lint = { ...vue, extends: [base, ...(vue.extends ?? [])] };
156
- ```
157
-
158
- `.oxlintrc.json` itself should stay untouched (string `extends`) for raw oxlint/editor consumption; this only applies to the `vite-plus` config path.
172
+ - **Plugins** are additive: both shared and local `plugins`/`jsPlugins` load, deduplicated
173
+ - **`env`, `globals`, and `ignorePatterns` don't merge through `extends` at all** (an open Oxlint bug), which is why the usage examples above redeclare `env`/`globals` directly. See [known limitations](docs/limitations.md) for the full detail, including the separate `vite-plus` caveat around resolving `extends` paths.
package/base.json CHANGED
@@ -1,61 +1,61 @@
1
1
  {
2
- "plugins": ["oxc", "typescript", "unicorn"],
3
- "jsPlugins": [
4
- "@stylistic/eslint-plugin",
5
- {
6
- "name": "vite-plus",
7
- "specifier": "vite-plus/oxlint-plugin"
8
- }
9
- ],
10
- "categories": {
11
- "correctness": "error"
12
- },
13
- "env": {
14
- "builtin": true,
15
- "browser": true
16
- },
17
- "rules": {
18
- "no-case-declarations": "error",
19
- "no-empty": "error",
20
- "no-fallthrough": "error",
21
- "no-prototype-builtins": "error",
22
- "no-redeclare": "error",
23
- "no-regex-spaces": "error",
24
- "no-undef": "error",
25
- "no-unexpected-multiline": "error",
26
- "no-useless-assignment": "error",
27
- "preserve-caught-error": "error",
28
- "sort-imports": [
29
- "error",
30
- {
31
- "ignoreDeclarationSort": true
32
- }
33
- ],
2
+ "plugins": ["oxc", "typescript", "unicorn"],
3
+ "jsPlugins": [
4
+ "@stylistic/eslint-plugin",
5
+ {
6
+ "name": "vite-plus",
7
+ "specifier": "vite-plus/oxlint-plugin"
8
+ }
9
+ ],
10
+ "categories": {
11
+ "correctness": "error"
12
+ },
13
+ "env": {
14
+ "builtin": true,
15
+ "browser": true
16
+ },
17
+ "rules": {
18
+ "no-case-declarations": "error",
19
+ "no-empty": "error",
20
+ "no-fallthrough": "error",
21
+ "no-prototype-builtins": "error",
22
+ "no-redeclare": "error",
23
+ "no-regex-spaces": "error",
24
+ "no-undef": "error",
25
+ "no-unexpected-multiline": "error",
26
+ "no-useless-assignment": "error",
27
+ "preserve-caught-error": "error",
28
+ "sort-imports": [
29
+ "error",
30
+ {
31
+ "ignoreDeclarationSort": true
32
+ }
33
+ ],
34
34
 
35
- "@stylistic/no-confusing-arrow": "error",
36
- "@stylistic/padding-line-between-statements": [
37
- "error",
38
- { "blankLine": "always", "prev": "const", "next": "let" },
39
- { "blankLine": "always", "prev": "let", "next": "const" },
40
- { "blankLine": "always", "prev": "*", "next": "break" },
41
- { "blankLine": "always", "prev": ["const", "let"], "next": "*" },
42
- { "blankLine": "always", "prev": "*", "next": "return" },
43
- { "blankLine": "any", "prev": "const", "next": "const" },
44
- { "blankLine": "any", "prev": "let", "next": "let" },
45
- { "blankLine": "always", "prev": "multiline-const", "next": "*" },
46
- { "blankLine": "always", "prev": "*", "next": "multiline-const" }
47
- ],
35
+ "@stylistic/no-confusing-arrow": "error",
36
+ "@stylistic/padding-line-between-statements": [
37
+ "error",
38
+ { "blankLine": "always", "prev": "const", "next": "let" },
39
+ { "blankLine": "always", "prev": "let", "next": "const" },
40
+ { "blankLine": "always", "prev": "*", "next": "break" },
41
+ { "blankLine": "always", "prev": ["const", "let"], "next": "*" },
42
+ { "blankLine": "always", "prev": "*", "next": "return" },
43
+ { "blankLine": "any", "prev": "const", "next": "const" },
44
+ { "blankLine": "any", "prev": "let", "next": "let" },
45
+ { "blankLine": "always", "prev": "multiline-const", "next": "*" },
46
+ { "blankLine": "always", "prev": "*", "next": "multiline-const" }
47
+ ],
48
48
 
49
- "vite-plus/prefer-vite-plus-imports": "error"
50
- },
51
- "overrides": [
52
- {
53
- "files": ["**/vite.config.*", "**/vitest.config.*", "**/playwright*.config.*"],
54
- "env": { "node": true }
55
- }
56
- ],
57
- "options": {
58
- "typeAware": false,
59
- "typeCheck": false
60
- }
49
+ "vite-plus/prefer-vite-plus-imports": "error"
50
+ },
51
+ "overrides": [
52
+ {
53
+ "files": ["**/vite.config.*", "**/vitest.config.*", "**/playwright*.config.*"],
54
+ "env": { "node": true }
55
+ }
56
+ ],
57
+ "options": {
58
+ "typeAware": false,
59
+ "typeCheck": false
60
+ }
61
61
  }
@@ -0,0 +1,30 @@
1
+ import blockComments from "./rules/block-comments.js";
2
+ import configuredApiCalls from "./rules/configured-api-calls.js";
3
+ import functionDocumentation from "./rules/function-documentation.js";
4
+ import jsdocTagFormatting from "./rules/jsdoc-tag-formatting.js";
5
+ import lineComments from "./rules/line-comments.js";
6
+ import maxLineLength from "./rules/max-line-length.js";
7
+ import placement from "./rules/placement.js";
8
+ import sentencePunctuation from "./rules/sentence-punctuation.js";
9
+ import variableDeclarations from "./rules/variable-declarations.js";
10
+ import vueComponentDocumentation from "./rules/vue-component-documentation.js";
11
+ import vueEmitDocumentation from "./rules/vue-emit-documentation.js";
12
+ import vuePropDocumentation from "./rules/vue-prop-documentation.js";
13
+
14
+ export default {
15
+ meta: { name: "comments" },
16
+ rules: {
17
+ "block-comments": blockComments,
18
+ "configured-api-calls": configuredApiCalls,
19
+ "function-documentation": functionDocumentation,
20
+ "jsdoc-tag-formatting": jsdocTagFormatting,
21
+ "line-comments": lineComments,
22
+ "max-line-length": maxLineLength,
23
+ placement,
24
+ "sentence-punctuation": sentencePunctuation,
25
+ "variable-declarations": variableDeclarations,
26
+ "vue-component-documentation": vueComponentDocumentation,
27
+ "vue-emit-documentation": vueEmitDocumentation,
28
+ "vue-prop-documentation": vuePropDocumentation,
29
+ },
30
+ };
@@ -0,0 +1,70 @@
1
+ import { formatJSDocBlockStructure, isJSDoc } from "../utils/jsdoc.js";
2
+ import { getCommentText, replaceMinimalComment } from "../utils/source.js";
3
+
4
+ /**
5
+ * Create the JSDoc block-comment formatting rule.
6
+ *
7
+ * @returns {object}
8
+ * The Oxlint rule definition.
9
+ */
10
+ export default {
11
+ meta: {
12
+ docs: { description: "Format JSDoc block comments." },
13
+ fixable: "code",
14
+ type: "layout",
15
+ },
16
+ /**
17
+ * Create the rule's node visitors.
18
+ *
19
+ * @param {object} context
20
+ * The Oxlint rule context.
21
+ *
22
+ * @returns {object}
23
+ * The visitor functions for this rule.
24
+ */
25
+ createOnce(context) {
26
+ return {
27
+ /**
28
+ * Format every JSDoc comment's block structure in the file.
29
+ */
30
+ Program() {
31
+ for (const comment of context.sourceCode.getAllComments()) {
32
+ if (comment.type !== "Block") {
33
+ continue;
34
+ }
35
+
36
+ // The comment's raw source text.
37
+ const commentText = getCommentText(context.sourceCode, comment);
38
+
39
+ if (!isJSDoc(commentText)) {
40
+ continue;
41
+ }
42
+
43
+ // The comment, with its block structure and delimiters normalised.
44
+ const formattedComment = formatJSDocBlockStructure(context.sourceCode, comment);
45
+
46
+ if (formattedComment === commentText) {
47
+ continue;
48
+ }
49
+
50
+ context.report({
51
+ /**
52
+ * Apply the formatted replacement to the comment.
53
+ *
54
+ * @param {object} fixer
55
+ * The Oxlint fixer.
56
+ *
57
+ * @returns {object}
58
+ * The fix to apply.
59
+ */
60
+ fix: (fixer) => {
61
+ return replaceMinimalComment(fixer, comment, commentText, formattedComment);
62
+ },
63
+ message: "JSDoc comments must use the configured block format.",
64
+ node: comment,
65
+ });
66
+ }
67
+ },
68
+ };
69
+ },
70
+ };
@@ -0,0 +1,137 @@
1
+ import { hasImmediateLineComment } from "../utils/source.js";
2
+
3
+ // The built-in APIs that require a preceding comment by default.
4
+ const builtInApis = new Set([
5
+ "onBeforeMount",
6
+ "onMounted",
7
+ "onBeforeUpdate",
8
+ "onUpdated",
9
+ "onBeforeUnmount",
10
+ "onUnmounted",
11
+ "onActivated",
12
+ "onDeactivated",
13
+ "onErrorCaptured",
14
+ "onRenderTracked",
15
+ "onRenderTriggered",
16
+ "onServerPrefetch",
17
+ "watch",
18
+ "watchEffect",
19
+ "watchPostEffect",
20
+ "watchSyncEffect",
21
+ "onClickOutside",
22
+ ]);
23
+
24
+ /**
25
+ * Return whether a call is the documented initializer of a variable
26
+ * declaration.
27
+ *
28
+ * @param {object} sourceCode
29
+ * The Oxlint source code object.
30
+ * @param {object} node
31
+ * The call expression node.
32
+ *
33
+ * @returns {boolean}
34
+ * Whether the call is directly initialized by a documented declaration.
35
+ */
36
+ function hasDocumentedVariableDeclaration(sourceCode, node) {
37
+ // The call's enclosing variable declarator, when there is one.
38
+ const declarator = node.parent;
39
+
40
+ if (declarator?.type !== "VariableDeclarator" || declarator.init !== node) {
41
+ return false;
42
+ }
43
+
44
+ // The declarator's enclosing variable declaration.
45
+ const declaration = declarator.parent;
46
+
47
+ return (
48
+ declaration?.type === "VariableDeclaration" && hasImmediateLineComment(sourceCode, declaration)
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Return the configured API names for the rule.
54
+ *
55
+ * @param {object} context
56
+ * The Oxlint rule context.
57
+ *
58
+ * @returns {Set<string>}
59
+ * The built-in and configured API names.
60
+ */
61
+ function getConfiguredApis(context) {
62
+ // The rule's resolved options for the file currently being visited.
63
+ const options = context.options?.[0];
64
+ // The project-configured API names to add to the built-in list.
65
+ const additionalApis = options?.additionalApis ?? [];
66
+
67
+ return new Set([...builtInApis, ...additionalApis]);
68
+ }
69
+
70
+ /**
71
+ * Create the configured API call comment rule.
72
+ *
73
+ * @returns {object}
74
+ * The Oxlint rule definition.
75
+ */
76
+ export default {
77
+ meta: {
78
+ docs: { description: "Require comments before configured API calls." },
79
+ type: "suggestion",
80
+ schema: [
81
+ {
82
+ type: "object",
83
+ properties: {
84
+ additionalApis: {
85
+ type: "array",
86
+ items: { type: "string" },
87
+ uniqueItems: true,
88
+ },
89
+ },
90
+ additionalProperties: false,
91
+ },
92
+ ],
93
+ defaultOptions: [{ additionalApis: [] }],
94
+ },
95
+
96
+ /**
97
+ * Create the rule's node visitors.
98
+ *
99
+ * @param {object} context
100
+ * The Oxlint rule context.
101
+ *
102
+ * @returns {object}
103
+ * The visitor functions for this rule.
104
+ */
105
+ createOnce(context) {
106
+ return {
107
+ /**
108
+ * Check a configured API call for a preceding comment.
109
+ *
110
+ * @param {object} node
111
+ * The call expression node.
112
+ */
113
+ CallExpression(node) {
114
+ // Read fresh for every call: createOnce's visitor is shared across every file
115
+ // in the run, so caching this at closure-creation time would freeze the first
116
+ // file's options.
117
+ const configuredApis = getConfiguredApis(context);
118
+
119
+ if (node.callee.type !== "Identifier" || !configuredApis.has(node.callee.name)) {
120
+ return;
121
+ }
122
+
123
+ if (
124
+ hasDocumentedVariableDeclaration(context.sourceCode, node) ||
125
+ hasImmediateLineComment(context.sourceCode, node)
126
+ ) {
127
+ return;
128
+ }
129
+
130
+ context.report({
131
+ message: "Configured API calls require an immediately preceding line comment.",
132
+ node,
133
+ });
134
+ },
135
+ };
136
+ },
137
+ };
@@ -0,0 +1,111 @@
1
+ import { isFunctionValue, reportFunctionDocumentation } from "../utils/documentation.js";
2
+
3
+ /**
4
+ * Return the declaration node that owns the documentation position.
5
+ *
6
+ * @param {object} node
7
+ * The function declaration node.
8
+ *
9
+ * @returns {object}
10
+ * The node immediately following the documentation block.
11
+ */
12
+ function getDocumentationNode(node) {
13
+ // Walks up through export wrappers to find the documented position.
14
+ let documentationNode = node;
15
+
16
+ while (
17
+ documentationNode.parent?.type === "ExportDefaultDeclaration" ||
18
+ documentationNode.parent?.type === "ExportNamedDeclaration"
19
+ ) {
20
+ documentationNode = documentationNode.parent;
21
+ }
22
+
23
+ return documentationNode;
24
+ }
25
+
26
+ /**
27
+ * Return whether an object property belongs to the outermost object literal.
28
+ *
29
+ * @param {object} node
30
+ * The property node to inspect.
31
+ *
32
+ * @returns {boolean}
33
+ * Whether the property is not nested inside another object literal.
34
+ */
35
+ function isFirstLevelObjectProperty(node) {
36
+ // Finds the object literal that owns the property.
37
+ const object = node.parent;
38
+
39
+ if (object?.type !== "ObjectExpression") {
40
+ return false;
41
+ }
42
+
43
+ return object.parent?.parent?.type !== "ObjectExpression";
44
+ }
45
+
46
+ /**
47
+ * Create the function-documentation rule.
48
+ *
49
+ * @returns {object}
50
+ * The Oxlint rule definition.
51
+ */
52
+ export default {
53
+ meta: {
54
+ docs: { description: "Require JSDoc documentation for named functions and methods." },
55
+ type: "suggestion",
56
+ },
57
+ /**
58
+ * Create the rule's node visitors.
59
+ *
60
+ * @param {object} context
61
+ * The Oxlint rule context.
62
+ *
63
+ * @returns {object}
64
+ * The visitor functions for this rule.
65
+ */
66
+ createOnce(context) {
67
+ return {
68
+ /**
69
+ * Check a named function declaration for documentation.
70
+ *
71
+ * @param {object} node
72
+ * The function declaration node.
73
+ */
74
+ FunctionDeclaration(node) {
75
+ if (node.id) {
76
+ reportFunctionDocumentation(context, getDocumentationNode(node), node);
77
+ }
78
+ },
79
+ /**
80
+ * Check a first-level object method for documentation.
81
+ *
82
+ * @param {object} node
83
+ * The property node.
84
+ */
85
+ Property(node) {
86
+ if (!isFirstLevelObjectProperty(node) || !isFunctionValue(node.value)) {
87
+ return;
88
+ }
89
+
90
+ reportFunctionDocumentation(context, node, node.value);
91
+ },
92
+ /**
93
+ * Check a const function variable for documentation.
94
+ *
95
+ * @param {object} node
96
+ * The variable declarator node.
97
+ */
98
+ VariableDeclarator(node) {
99
+ if (
100
+ node.id.type !== "Identifier" ||
101
+ node.parent?.kind !== "const" ||
102
+ !isFunctionValue(node.init)
103
+ ) {
104
+ return;
105
+ }
106
+
107
+ reportFunctionDocumentation(context, node.parent, node.init);
108
+ },
109
+ };
110
+ },
111
+ };