@zgeoff/oxlint-config 0.0.3 → 0.0.5

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
@@ -2,9 +2,11 @@
2
2
 
3
3
  Shareable [oxlint](https://oxc.rs/docs/guide/usage/linter.html) config: every category at `error`,
4
4
  restriction-rule cherry-picks, comment-style enforcement via `@stylistic`, and a bundled JS plugin
5
- (`zgeoff/*` rules) banning AST shapes no native oxlint rule can express — top-level arrows, nested
5
+ (`zgeoff/*` rules) banning shapes no native oxlint rule can express — top-level arrows, nested
6
6
  function declarations, bare named exports, destructured and inline-typed parameters, ternary and
7
- await arguments, and awaits hidden inside a control-flow condition or a `&&`/`||`/`??` chain.
7
+ await arguments, awaits hidden inside a control-flow condition or a `&&`/`||`/`??` chain, and
8
+ single-line `/** … */` blocks (auto-fixed to multi-line; inline `@type`/`@lends` casts exempt).
9
+ `zgeoff/function-verb` enforces the function-naming taxonomy from the shared agent guidelines.
8
10
 
9
11
  ## Usage
10
12
 
@@ -33,6 +35,25 @@ import config from '@zgeoff/oxlint-config';
33
35
  The plugin is addressable on its own at `@zgeoff/oxlint-config/plugin` for configs that want the
34
36
  `zgeoff/*` rules without the rest.
35
37
 
38
+ ## `zgeoff/function-verb`
39
+
40
+ Every function declaration, function-valued variable, and class method must start with a verb from
41
+ the function-naming taxonomy. Banned verbs report their taxonomy replacement (`fetchUser` → use
42
+ `read`). Object-literal properties are exempt — they overwhelmingly implement externally-defined
43
+ shapes whose names the author doesn't choose — as are names not starting with a lowercase letter.
44
+ Exempt an algorithm-native name (`walk`, `backtrack`) with a disable comment in the module
45
+ implementing that algorithm.
46
+
47
+ Options extend the shipped set per repo:
48
+
49
+ ```jsonc
50
+ {
51
+ "rules": {
52
+ "zgeoff/function-verb": ["error", { "verbs": ["walk"], "exemptNames": ["main"] }],
53
+ },
54
+ }
55
+ ```
56
+
36
57
  ## Notes
37
58
 
38
59
  - The config enables the `typescript`, `unicorn`, `oxc`, `import`, and `promise` plugins and loads
@@ -0,0 +1,218 @@
1
+ // the closed verb list from the shared function-naming taxonomy, one entry
2
+ // per table row; extending it here without the matching taxonomy edit fails
3
+ // the drift-guard test
4
+ const taxonomyVerbs = [
5
+ 'is',
6
+ 'has',
7
+ 'can',
8
+ 'should',
9
+ 'needs',
10
+ 'build',
11
+ 'define',
12
+ 'parse',
13
+ 'encode',
14
+ 'decode',
15
+ 'derive',
16
+ 'plan',
17
+ 'pick',
18
+ 'find',
19
+ 'get',
20
+ 'collect',
21
+ 'count',
22
+ 'split',
23
+ 'merge',
24
+ 'sort',
25
+ 'format',
26
+ 'render',
27
+ 'normalize',
28
+ 'resolve',
29
+ 'expand',
30
+ 'compress',
31
+ 'decompress',
32
+ 'to',
33
+ 'transform',
34
+ 'apply',
35
+ 'create',
36
+ 'claim',
37
+ 'read',
38
+ 'load',
39
+ 'write',
40
+ 'remove',
41
+ 'update',
42
+ 'upsert',
43
+ 'set',
44
+ 'toggle',
45
+ 'reset',
46
+ 'print',
47
+ 'run',
48
+ 'check',
49
+ 'try',
50
+ 'register',
51
+ 'subscribe',
52
+ 'unsubscribe',
53
+ 'assert',
54
+ 'require',
55
+ 'verify',
56
+ 'emit',
57
+ 'send',
58
+ 'wait',
59
+ 'setup',
60
+ 'teardown',
61
+ 'start',
62
+ 'stop',
63
+ 'drain',
64
+ 'with',
65
+ 'make',
66
+ 'use',
67
+ 'on',
68
+ 'handle',
69
+ ];
70
+
71
+ // the templated taxonomy entries (`with<X>`, `handle<Event>`, …): the verb is
72
+ // only valid with a suffix, so the bare word is not a function name
73
+ const suffixRequiredVerbs = new Set([
74
+ 'build',
75
+ 'define',
76
+ 'to',
77
+ 'toggle',
78
+ 'try',
79
+ 'with',
80
+ 'make',
81
+ 'use',
82
+ 'on',
83
+ 'handle',
84
+ ]);
85
+
86
+ // banned verb → the taxonomy verb to use instead; null means the verb is too
87
+ // vague to map and the name should say what the function does
88
+ const bannedVerbs = new Map([
89
+ ['process', null],
90
+ ['manage', null],
91
+ ['do', null],
92
+ ['perform', null],
93
+ ['execute', 'run'],
94
+ ['compute', 'build'],
95
+ ['fetch', 'read'],
96
+ ['save', 'write'],
97
+ ['store', 'write'],
98
+ ['delete', 'remove'],
99
+ ['search', 'find'],
100
+ ['lookup', 'find'],
101
+ ]);
102
+
103
+ function isVerbMatch(name, verb, bareAllowed) {
104
+ if (name === verb) {
105
+ return bareAllowed;
106
+ }
107
+
108
+ return name.startsWith(verb) && /[A-Z]/u.test(name.charAt(verb.length));
109
+ }
110
+
111
+ function findAllowedVerb(name, verbs) {
112
+ return verbs.find((verb) => isVerbMatch(name, verb, !suffixRequiredVerbs.has(verb))) ?? null;
113
+ }
114
+
115
+ function findBannedVerb(name) {
116
+ for (const [verb, replacement] of bannedVerbs) {
117
+ if (isVerbMatch(name, verb, true)) {
118
+ return { verb, replacement };
119
+ }
120
+ }
121
+
122
+ return null;
123
+ }
124
+
125
+ function planReport(name, verbs, exemptNames) {
126
+ if (exemptNames.has(name) || !/^[a-z]/u.test(name)) {
127
+ return null;
128
+ }
129
+
130
+ if (findAllowedVerb(name, verbs) !== null) {
131
+ return null;
132
+ }
133
+
134
+ const banned = findBannedVerb(name);
135
+
136
+ if (banned === null) {
137
+ return { messageId: 'unknownVerb', data: { name } };
138
+ }
139
+
140
+ if (banned.replacement === null) {
141
+ return { messageId: 'vagueVerb', data: { name, verb: banned.verb } };
142
+ }
143
+
144
+ return {
145
+ messageId: 'bannedVerb',
146
+ data: { name, verb: banned.verb, replacement: banned.replacement },
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Enforces the function-naming taxonomy on function declarations,
152
+ * function-valued variables, and class methods. Object-literal properties are
153
+ * exempt — they overwhelmingly implement externally-defined shapes (rule
154
+ * visitors, route tables) whose names the author doesn't choose — as are
155
+ * names not starting with a lowercase letter (components, classes).
156
+ * Options: `verbs` appends repo-local verbs to the shipped taxonomy;
157
+ * `exemptNames` skips exact names.
158
+ */
159
+ const functionVerb = {
160
+ meta: {
161
+ type: 'suggestion',
162
+ messages: {
163
+ unknownVerb:
164
+ "'{{name}}' does not start with a taxonomy verb — pick one, or extend the taxonomy and the `verbs` option in the same PR.",
165
+ bannedVerb: "'{{name}}' starts with the banned verb '{{verb}}' — use `{{replacement}}`.",
166
+ vagueVerb: "'{{name}}' starts with the banned verb '{{verb}}' — name what the function does.",
167
+ },
168
+ schema: [
169
+ {
170
+ type: 'object',
171
+ properties: {
172
+ verbs: { type: 'array', items: { type: 'string' }, uniqueItems: true },
173
+ exemptNames: { type: 'array', items: { type: 'string' }, uniqueItems: true },
174
+ },
175
+ additionalProperties: false,
176
+ },
177
+ ],
178
+ },
179
+ create(context) {
180
+ const options = context.options[0] ?? {};
181
+ const verbs =
182
+ options.verbs === undefined ? taxonomyVerbs : [...taxonomyVerbs, ...options.verbs];
183
+ const exemptNames = new Set(options.exemptNames);
184
+
185
+ const checkName = (id) => {
186
+ const report = planReport(id.name, verbs, exemptNames);
187
+
188
+ if (report !== null) {
189
+ context.report({ node: id, messageId: report.messageId, data: report.data });
190
+ }
191
+ };
192
+
193
+ return {
194
+ FunctionDeclaration(node) {
195
+ if (node.id !== null) {
196
+ checkName(node.id);
197
+ }
198
+ },
199
+ VariableDeclarator(node) {
200
+ const isFunctionInit =
201
+ node.init !== null &&
202
+ node.init !== undefined &&
203
+ (node.init.type === 'ArrowFunctionExpression' || node.init.type === 'FunctionExpression');
204
+
205
+ if (isFunctionInit && node.id.type === 'Identifier') {
206
+ checkName(node.id);
207
+ }
208
+ },
209
+ MethodDefinition(node) {
210
+ if (node.kind === 'method' && !node.computed && node.key.type === 'Identifier') {
211
+ checkName(node.key);
212
+ }
213
+ },
214
+ };
215
+ },
216
+ };
217
+
218
+ export default functionVerb;
package/oxlintrc.json CHANGED
@@ -25,6 +25,8 @@
25
25
  "zgeoff/no-await-args": "error",
26
26
  "zgeoff/no-await-in-condition": "error",
27
27
  "zgeoff/no-await-in-logical": "error",
28
+ "zgeoff/no-single-line-jsdoc": "error",
29
+ "zgeoff/function-verb": "error",
28
30
 
29
31
  // enforced comment style — line comments unless JSDoc or exclamation
30
32
  "@stylistic/multiline-comment-style": [
@@ -142,8 +144,9 @@
142
144
  {
143
145
  "files": ["**/*.js", "**/*.cjs", "**/*.mjs"],
144
146
  // untyped JS is `any` end to end, so the unsafe-* rules flag every
145
- // expression
147
+ // expression and strict-boolean-expressions every conditional
146
148
  "rules": {
149
+ "typescript/strict-boolean-expressions": "off",
147
150
  "typescript/no-unsafe-argument": "off",
148
151
  "typescript/no-unsafe-assignment": "off",
149
152
  "typescript/no-unsafe-call": "off",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zgeoff/oxlint-config",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Shareable oxlint config: strict categories, house-style rules, and a custom JS plugin",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -11,6 +11,7 @@
11
11
  "files": [
12
12
  "oxlintrc.json",
13
13
  "plugin.js",
14
+ "function-verb.js",
14
15
  "index.js"
15
16
  ],
16
17
  "type": "module",
@@ -22,9 +23,16 @@
22
23
  "publishConfig": {
23
24
  "access": "public"
24
25
  },
26
+ "scripts": {
27
+ "typecheck": "tsc"
28
+ },
25
29
  "dependencies": {
26
30
  "@stylistic/eslint-plugin": "5.10.0"
27
31
  },
32
+ "devDependencies": {
33
+ "@types/bun": "1.3.14",
34
+ "typescript": "7.0.1-rc"
35
+ },
28
36
  "peerDependencies": {
29
37
  "oxlint": ">=1.71.0"
30
38
  }
package/plugin.js CHANGED
@@ -1,8 +1,11 @@
1
+ import functionVerb from './function-verb.js';
2
+
1
3
  // Custom lint rules, loaded through oxlint's jsPlugins (ESLint v9 rule API).
2
- // Each rule bans an AST shape that no native oxlint rule can express, using
3
- // esquery selectors.
4
+ // Each rule bans a shape no native oxlint rule can express — esquery selectors
5
+ // for AST shapes, a source-comment scan where the target isn't
6
+ // esquery-selectable.
4
7
 
5
- function banSelectors(type, message, selectors) {
8
+ function buildBanRule(type, message, selectors) {
6
9
  return {
7
10
  meta: { type, messages: { banned: message }, schema: [] },
8
11
  create(context) {
@@ -16,10 +19,86 @@ function banSelectors(type, message, selectors) {
16
19
  };
17
20
  }
18
21
 
22
+ // mirrors eslint-plugin-jsdoc's default singleLineTags: blocks that exist to
23
+ // cast inline (`/** @type {Foo} */ (bar)`) stay single-line by design
24
+ const inlineTagPattern = /^@(?:type|lends)\b/u;
25
+
26
+ function isSingleLineJSDoc(comment) {
27
+ return (
28
+ comment.type === 'Block' &&
29
+ comment.value.startsWith('*') &&
30
+ comment.loc.start.line === comment.loc.end.line
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Builds the multi-line replacement for a single-line JSDoc comment, or null
36
+ * when the comment shares its line with code — expanding those in place would
37
+ * scramble the surrounding statement, so they are reported without a fix.
38
+ */
39
+ function planExpansion(text, comment) {
40
+ if (!isOwnLine(text, comment)) {
41
+ return null;
42
+ }
43
+
44
+ const lineStart = text.lastIndexOf('\n', comment.range[0] - 1) + 1;
45
+ const indent = text.slice(lineStart, comment.range[0]);
46
+ const body = comment.value.slice(1).trim();
47
+ const bodyLine = body === '' ? `${indent} *` : `${indent} * ${body}`;
48
+
49
+ return `/**\n${bodyLine}\n${indent} */`;
50
+ }
51
+
52
+ function isOwnLine(text, comment) {
53
+ const [start, end] = comment.range;
54
+ const lineStart = text.lastIndexOf('\n', start - 1) + 1;
55
+ const lineEnd = text.indexOf('\n', end);
56
+ const sliceEnd = lineEnd === -1 ? text.length : lineEnd;
57
+
58
+ return /^[ \t]*$/u.test(text.slice(lineStart, start)) && text.slice(end, sliceEnd).trim() === '';
59
+ }
60
+
61
+ const noSingleLineJSDoc = {
62
+ meta: {
63
+ type: 'layout',
64
+ fixable: 'whitespace',
65
+ messages: {
66
+ singleLine:
67
+ 'Write JSDoc blocks multi-line: `/**` alone, one `*`-prefixed line per point, `*/` alone.',
68
+ },
69
+ schema: [],
70
+ },
71
+ create(context) {
72
+ return {
73
+ Program() {
74
+ const offenders = context.sourceCode
75
+ .getAllComments()
76
+ .filter(
77
+ (comment) =>
78
+ isSingleLineJSDoc(comment) && !inlineTagPattern.test(comment.value.slice(1).trim()),
79
+ );
80
+
81
+ for (const comment of offenders) {
82
+ const replacement = planExpansion(context.sourceCode.text, comment);
83
+
84
+ context.report({
85
+ loc: comment.loc,
86
+ messageId: 'singleLine',
87
+ fix:
88
+ replacement === null
89
+ ? undefined
90
+ : (fixer) => fixer.replaceTextRange(comment.range, replacement),
91
+ });
92
+ }
93
+ },
94
+ };
95
+ },
96
+ };
97
+
19
98
  const plugin = {
20
99
  meta: { name: 'zgeoff' },
21
100
  rules: {
22
- 'no-top-level-arrow': banSelectors(
101
+ 'no-top-level-arrow': buildBanRule(
23
102
  'problem',
24
103
  'Declare top-level functions with the `function` keyword instead of assigning an arrow function.',
25
104
  [
@@ -29,19 +108,19 @@ const plugin = {
29
108
  "Program > ExpressionStatement > AssignmentExpression[right.type='ArrowFunctionExpression']",
30
109
  ],
31
110
  ),
32
- 'no-nested-function-declaration': banSelectors(
111
+ 'no-nested-function-declaration': buildBanRule(
33
112
  'problem',
34
113
  'Use an arrow function here; reserve the `function` keyword for top-level declarations.',
35
114
  [
36
115
  'FunctionDeclaration:not(Program > FunctionDeclaration):not(Program > ExportNamedDeclaration > FunctionDeclaration):not(Program > ExportDefaultDeclaration > FunctionDeclaration)',
37
116
  ],
38
117
  ),
39
- 'no-bare-named-exports': banSelectors(
118
+ 'no-bare-named-exports': buildBanRule(
40
119
  'problem',
41
120
  'Export declarations inline instead of listing names in `export { ... }`.',
42
121
  ['ExportNamedDeclaration[specifiers.length>0][declaration=null][source=null]'],
43
122
  ),
44
- 'no-inline-param-object-types': banSelectors(
123
+ 'no-inline-param-object-types': buildBanRule(
45
124
  'problem',
46
125
  'Give this parameter a named interface or type alias instead of an inline object-literal type.',
47
126
  [
@@ -49,7 +128,7 @@ const plugin = {
49
128
  'TSMethodSignature > Identifier.params > TSTypeAnnotation > TSTypeLiteral',
50
129
  ],
51
130
  ),
52
- 'no-destructured-params': banSelectors(
131
+ 'no-destructured-params': buildBanRule(
53
132
  'suggestion',
54
133
  'Accept the parameter whole instead of destructuring it in the signature.',
55
134
  [
@@ -57,12 +136,12 @@ const plugin = {
57
136
  ':matches(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression) > AssignmentPattern.params > ObjectPattern',
58
137
  ],
59
138
  ),
60
- 'no-pick-destructuring': banSelectors(
139
+ 'no-pick-destructuring': buildBanRule(
61
140
  'suggestion',
62
141
  'Use direct member access; destructure an object only with a rest element to omit properties.',
63
142
  ['VariableDeclarator > ObjectPattern:not(:has(> RestElement))'],
64
143
  ),
65
- 'no-ternary-args': banSelectors(
144
+ 'no-ternary-args': buildBanRule(
66
145
  'suggestion',
67
146
  'Extract this ternary to a named const instead of passing it as an argument.',
68
147
  [
@@ -70,12 +149,12 @@ const plugin = {
70
149
  'NewExpression > ConditionalExpression.arguments',
71
150
  ],
72
151
  ),
73
- 'no-await-args': banSelectors(
152
+ 'no-await-args': buildBanRule(
74
153
  'suggestion',
75
154
  'Await into a named const instead of awaiting inside an argument list.',
76
155
  ['CallExpression > AwaitExpression.arguments', 'NewExpression > AwaitExpression.arguments'],
77
156
  ),
78
- 'no-await-in-condition': banSelectors(
157
+ 'no-await-in-condition': buildBanRule(
79
158
  'suggestion',
80
159
  'Await into a named const before the statement instead of inside a control-flow condition.',
81
160
  [
@@ -83,11 +162,13 @@ const plugin = {
83
162
  'SwitchStatement > AwaitExpression.discriminant',
84
163
  ],
85
164
  ),
86
- 'no-await-in-logical': banSelectors(
165
+ 'no-await-in-logical': buildBanRule(
87
166
  'suggestion',
88
167
  'Await into a named const instead of chaining it after a && / || / ?? operator.',
89
168
  ['LogicalExpression > AwaitExpression'],
90
169
  ),
170
+ 'no-single-line-jsdoc': noSingleLineJSDoc,
171
+ 'function-verb': functionVerb,
91
172
  },
92
173
  };
93
174