@zgeoff/oxlint-config 0.0.4 → 0.0.6

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
@@ -6,6 +6,7 @@ restriction-rule cherry-picks, comment-style enforcement via `@stylistic`, and a
6
6
  function declarations, bare named exports, destructured and inline-typed parameters, ternary and
7
7
  await arguments, awaits hidden inside a control-flow condition or a `&&`/`||`/`??` chain, and
8
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.
9
10
 
10
11
  ## Usage
11
12
 
@@ -34,6 +35,25 @@ import config from '@zgeoff/oxlint-config';
34
35
  The plugin is addressable on its own at `@zgeoff/oxlint-config/plugin` for configs that want the
35
36
  `zgeoff/*` rules without the rest.
36
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
+
37
57
  ## Notes
38
58
 
39
59
  - The config enables the `typescript`, `unicorn`, `oxc`, `import`, and `promise` plugins and loads
@@ -0,0 +1,219 @@
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
+ // a digit also ends the verb: acronyms like 2FA start camelCase segments
109
+ return name.startsWith(verb) && /[A-Z0-9]/u.test(name.charAt(verb.length));
110
+ }
111
+
112
+ function findAllowedVerb(name, verbs) {
113
+ return verbs.find((verb) => isVerbMatch(name, verb, !suffixRequiredVerbs.has(verb))) ?? null;
114
+ }
115
+
116
+ function findBannedVerb(name) {
117
+ for (const [verb, replacement] of bannedVerbs) {
118
+ if (isVerbMatch(name, verb, true)) {
119
+ return { verb, replacement };
120
+ }
121
+ }
122
+
123
+ return null;
124
+ }
125
+
126
+ function planReport(name, verbs, exemptNames) {
127
+ if (exemptNames.has(name) || !/^[a-z]/u.test(name)) {
128
+ return null;
129
+ }
130
+
131
+ if (findAllowedVerb(name, verbs) !== null) {
132
+ return null;
133
+ }
134
+
135
+ const banned = findBannedVerb(name);
136
+
137
+ if (banned === null) {
138
+ return { messageId: 'unknownVerb', data: { name } };
139
+ }
140
+
141
+ if (banned.replacement === null) {
142
+ return { messageId: 'vagueVerb', data: { name, verb: banned.verb } };
143
+ }
144
+
145
+ return {
146
+ messageId: 'bannedVerb',
147
+ data: { name, verb: banned.verb, replacement: banned.replacement },
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Enforces the function-naming taxonomy on function declarations,
153
+ * function-valued variables, and class methods. Object-literal properties are
154
+ * exempt — they overwhelmingly implement externally-defined shapes (rule
155
+ * visitors, route tables) whose names the author doesn't choose — as are
156
+ * names not starting with a lowercase letter (components, classes).
157
+ * Options: `verbs` appends repo-local verbs to the shipped taxonomy;
158
+ * `exemptNames` skips exact names.
159
+ */
160
+ const functionVerb = {
161
+ meta: {
162
+ type: 'suggestion',
163
+ messages: {
164
+ unknownVerb:
165
+ "'{{name}}' does not start with a taxonomy verb — pick one, or extend the taxonomy and the `verbs` option in the same PR.",
166
+ bannedVerb: "'{{name}}' starts with the banned verb '{{verb}}' — use `{{replacement}}`.",
167
+ vagueVerb: "'{{name}}' starts with the banned verb '{{verb}}' — name what the function does.",
168
+ },
169
+ schema: [
170
+ {
171
+ type: 'object',
172
+ properties: {
173
+ verbs: { type: 'array', items: { type: 'string' }, uniqueItems: true },
174
+ exemptNames: { type: 'array', items: { type: 'string' }, uniqueItems: true },
175
+ },
176
+ additionalProperties: false,
177
+ },
178
+ ],
179
+ },
180
+ create(context) {
181
+ const options = context.options[0] ?? {};
182
+ const verbs =
183
+ options.verbs === undefined ? taxonomyVerbs : [...taxonomyVerbs, ...options.verbs];
184
+ const exemptNames = new Set(options.exemptNames);
185
+
186
+ const checkName = (id) => {
187
+ const report = planReport(id.name, verbs, exemptNames);
188
+
189
+ if (report !== null) {
190
+ context.report({ node: id, messageId: report.messageId, data: report.data });
191
+ }
192
+ };
193
+
194
+ return {
195
+ FunctionDeclaration(node) {
196
+ if (node.id !== null) {
197
+ checkName(node.id);
198
+ }
199
+ },
200
+ VariableDeclarator(node) {
201
+ const isFunctionInit =
202
+ node.init !== null &&
203
+ node.init !== undefined &&
204
+ (node.init.type === 'ArrowFunctionExpression' || node.init.type === 'FunctionExpression');
205
+
206
+ if (isFunctionInit && node.id.type === 'Identifier') {
207
+ checkName(node.id);
208
+ }
209
+ },
210
+ MethodDefinition(node) {
211
+ if (node.kind === 'method' && !node.computed && node.key.type === 'Identifier') {
212
+ checkName(node.key);
213
+ }
214
+ },
215
+ };
216
+ },
217
+ };
218
+
219
+ export default functionVerb;
package/oxlintrc.json CHANGED
@@ -26,6 +26,7 @@
26
26
  "zgeoff/no-await-in-condition": "error",
27
27
  "zgeoff/no-await-in-logical": "error",
28
28
  "zgeoff/no-single-line-jsdoc": "error",
29
+ "zgeoff/function-verb": "error",
29
30
 
30
31
  // enforced comment style — line comments unless JSDoc or exclamation
31
32
  "@stylistic/multiline-comment-style": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zgeoff/oxlint-config",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
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",
package/plugin.js CHANGED
@@ -1,9 +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
4
  // Each rule bans a shape no native oxlint rule can express — esquery selectors
3
5
  // for AST shapes, a source-comment scan where the target isn't
4
6
  // esquery-selectable.
5
7
 
6
- function banSelectors(type, message, selectors) {
8
+ function buildBanRule(type, message, selectors) {
7
9
  return {
8
10
  meta: { type, messages: { banned: message }, schema: [] },
9
11
  create(context) {
@@ -96,7 +98,7 @@ const noSingleLineJSDoc = {
96
98
  const plugin = {
97
99
  meta: { name: 'zgeoff' },
98
100
  rules: {
99
- 'no-top-level-arrow': banSelectors(
101
+ 'no-top-level-arrow': buildBanRule(
100
102
  'problem',
101
103
  'Declare top-level functions with the `function` keyword instead of assigning an arrow function.',
102
104
  [
@@ -106,19 +108,19 @@ const plugin = {
106
108
  "Program > ExpressionStatement > AssignmentExpression[right.type='ArrowFunctionExpression']",
107
109
  ],
108
110
  ),
109
- 'no-nested-function-declaration': banSelectors(
111
+ 'no-nested-function-declaration': buildBanRule(
110
112
  'problem',
111
113
  'Use an arrow function here; reserve the `function` keyword for top-level declarations.',
112
114
  [
113
115
  'FunctionDeclaration:not(Program > FunctionDeclaration):not(Program > ExportNamedDeclaration > FunctionDeclaration):not(Program > ExportDefaultDeclaration > FunctionDeclaration)',
114
116
  ],
115
117
  ),
116
- 'no-bare-named-exports': banSelectors(
118
+ 'no-bare-named-exports': buildBanRule(
117
119
  'problem',
118
120
  'Export declarations inline instead of listing names in `export { ... }`.',
119
121
  ['ExportNamedDeclaration[specifiers.length>0][declaration=null][source=null]'],
120
122
  ),
121
- 'no-inline-param-object-types': banSelectors(
123
+ 'no-inline-param-object-types': buildBanRule(
122
124
  'problem',
123
125
  'Give this parameter a named interface or type alias instead of an inline object-literal type.',
124
126
  [
@@ -126,7 +128,7 @@ const plugin = {
126
128
  'TSMethodSignature > Identifier.params > TSTypeAnnotation > TSTypeLiteral',
127
129
  ],
128
130
  ),
129
- 'no-destructured-params': banSelectors(
131
+ 'no-destructured-params': buildBanRule(
130
132
  'suggestion',
131
133
  'Accept the parameter whole instead of destructuring it in the signature.',
132
134
  [
@@ -134,12 +136,12 @@ const plugin = {
134
136
  ':matches(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression) > AssignmentPattern.params > ObjectPattern',
135
137
  ],
136
138
  ),
137
- 'no-pick-destructuring': banSelectors(
139
+ 'no-pick-destructuring': buildBanRule(
138
140
  'suggestion',
139
141
  'Use direct member access; destructure an object only with a rest element to omit properties.',
140
142
  ['VariableDeclarator > ObjectPattern:not(:has(> RestElement))'],
141
143
  ),
142
- 'no-ternary-args': banSelectors(
144
+ 'no-ternary-args': buildBanRule(
143
145
  'suggestion',
144
146
  'Extract this ternary to a named const instead of passing it as an argument.',
145
147
  [
@@ -147,12 +149,12 @@ const plugin = {
147
149
  'NewExpression > ConditionalExpression.arguments',
148
150
  ],
149
151
  ),
150
- 'no-await-args': banSelectors(
152
+ 'no-await-args': buildBanRule(
151
153
  'suggestion',
152
154
  'Await into a named const instead of awaiting inside an argument list.',
153
155
  ['CallExpression > AwaitExpression.arguments', 'NewExpression > AwaitExpression.arguments'],
154
156
  ),
155
- 'no-await-in-condition': banSelectors(
157
+ 'no-await-in-condition': buildBanRule(
156
158
  'suggestion',
157
159
  'Await into a named const before the statement instead of inside a control-flow condition.',
158
160
  [
@@ -160,12 +162,13 @@ const plugin = {
160
162
  'SwitchStatement > AwaitExpression.discriminant',
161
163
  ],
162
164
  ),
163
- 'no-await-in-logical': banSelectors(
165
+ 'no-await-in-logical': buildBanRule(
164
166
  'suggestion',
165
167
  'Await into a named const instead of chaining it after a && / || / ?? operator.',
166
168
  ['LogicalExpression > AwaitExpression'],
167
169
  ),
168
170
  'no-single-line-jsdoc': noSingleLineJSDoc,
171
+ 'function-verb': functionVerb,
169
172
  },
170
173
  };
171
174