@zgeoff/oxlint-config 0.0.5 → 0.1.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
@@ -4,9 +4,9 @@ Shareable [oxlint](https://oxc.rs/docs/guide/usage/linter.html) config: every ca
4
4
  restriction-rule cherry-picks, comment-style enforcement via `@stylistic`, and a bundled JS plugin
5
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, 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.
7
+ await arguments, awaits hidden inside a control-flow condition or a `&&`/`||`/`??` chain, every
8
+ JSDoc block, and any run of more than three line comments. `zgeoff/function-verb` enforces the
9
+ function-naming taxonomy from the shared agent guidelines.
10
10
 
11
11
  ## Usage
12
12
 
@@ -54,6 +54,23 @@ Options extend the shipped set per repo:
54
54
  }
55
55
  ```
56
56
 
57
+ ## `zgeoff/no-jsdoc` and `zgeoff/max-consecutive-line-comments`
58
+
59
+ A comment holds only what neither the code, a type, a test name, nor the subsystem doc can hold: the
60
+ reason a line does the non-obvious thing. `no-jsdoc` reports every `/** … */` block (inline
61
+ `@type`/`@lends` casts exempt). `max-consecutive-line-comments` reports a run of own-line `//`
62
+ comments longer than `max` (default 3); tool directives (`oxlint-`, `eslint-`, `@ts-`, …) neither
63
+ count nor join the prose around them. Neither rule fixes: a deleted comment is a decision, not a
64
+ whitespace change.
65
+
66
+ ```jsonc
67
+ {
68
+ "rules": {
69
+ "zgeoff/max-consecutive-line-comments": ["error", { "max": 3 }],
70
+ },
71
+ }
72
+ ```
73
+
57
74
  ## Notes
58
75
 
59
76
  - The config enables the `typescript`, `unicorn`, `oxc`, `import`, and `promise` plugins and loads
package/function-verb.js CHANGED
@@ -105,7 +105,8 @@ function isVerbMatch(name, verb, bareAllowed) {
105
105
  return bareAllowed;
106
106
  }
107
107
 
108
- return name.startsWith(verb) && /[A-Z]/u.test(name.charAt(verb.length));
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));
109
110
  }
110
111
 
111
112
  function findAllowedVerb(name, verbs) {
@@ -147,15 +148,6 @@ function planReport(name, verbs, exemptNames) {
147
148
  };
148
149
  }
149
150
 
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
151
  const functionVerb = {
160
152
  meta: {
161
153
  type: 'suggestion',
@@ -0,0 +1,76 @@
1
+ // tool directives are not prose: they neither count toward a run nor join
2
+ // the prose on either side of them into one
3
+ const directivePattern =
4
+ /^\s*(?:oxlint-|eslint-|@ts-|prettier-|oxfmt-|biome-ignore|#region|#endregion|\/\s*<)/u;
5
+
6
+ function isOwnLine(text, comment) {
7
+ const lineStart = text.lastIndexOf('\n', comment.range[0] - 1) + 1;
8
+
9
+ return /^[ \t]*$/u.test(text.slice(lineStart, comment.range[0]));
10
+ }
11
+
12
+ function isProseLine(text, comment) {
13
+ return (
14
+ comment.type === 'Line' && !directivePattern.test(comment.value) && isOwnLine(text, comment)
15
+ );
16
+ }
17
+
18
+ // a run is prose line comments on consecutive lines; whatever else occupies
19
+ // a line between two of them (code, a blank, a directive, a block comment)
20
+ // breaks adjacency and so ends the run
21
+ function collectRuns(text, comments) {
22
+ const runs = [];
23
+
24
+ for (const comment of comments.filter((candidate) => isProseLine(text, candidate))) {
25
+ const run = runs.at(-1);
26
+
27
+ if (run !== undefined && comment.loc.start.line === run.at(-1).loc.end.line + 1) {
28
+ run.push(comment);
29
+ } else {
30
+ runs.push([comment]);
31
+ }
32
+ }
33
+
34
+ return runs;
35
+ }
36
+
37
+ const maxConsecutiveLineComments = {
38
+ meta: {
39
+ type: 'suggestion',
40
+ messages: {
41
+ tooLong:
42
+ 'This comment run is {{count}} lines; the limit is {{max}}. Cut it to the fact the code cannot show, or move it into a test name or the subsystem doc.',
43
+ },
44
+ schema: [
45
+ {
46
+ type: 'object',
47
+ properties: {
48
+ max: { type: 'integer', minimum: 1 },
49
+ },
50
+ additionalProperties: false,
51
+ },
52
+ ],
53
+ },
54
+ create(context) {
55
+ const max = context.options[0]?.max ?? 3;
56
+
57
+ return {
58
+ Program() {
59
+ const text = context.sourceCode.text;
60
+ const runs = collectRuns(text, context.sourceCode.getAllComments());
61
+
62
+ for (const run of runs) {
63
+ if (run.length > max) {
64
+ context.report({
65
+ loc: { start: run[0].loc.start, end: run.at(-1).loc.end },
66
+ messageId: 'tooLong',
67
+ data: { count: String(run.length), max: String(max) },
68
+ });
69
+ }
70
+ }
71
+ },
72
+ };
73
+ },
74
+ };
75
+
76
+ export default maxConsecutiveLineComments;
package/no-jsdoc.js ADDED
@@ -0,0 +1,40 @@
1
+ // mirrors eslint-plugin-jsdoc's default singleLineTags: a block that exists to
2
+ // cast inline (`/** @type {Foo} */ (bar)`) is a type annotation, not prose
3
+ const inlineTagPattern = /^@(?:type|lends)\b/u;
4
+
5
+ function isJSDoc(comment) {
6
+ return comment.type === 'Block' && comment.value.startsWith('*');
7
+ }
8
+
9
+ function isInlineCast(comment) {
10
+ return (
11
+ comment.loc.start.line === comment.loc.end.line &&
12
+ inlineTagPattern.test(comment.value.slice(1).trim())
13
+ );
14
+ }
15
+
16
+ const noJSDoc = {
17
+ meta: {
18
+ type: 'suggestion',
19
+ messages: {
20
+ jsdoc:
21
+ 'Delete this JSDoc block. A rule a caller must respect is a test whose name states it or a sentence in the subsystem doc; the reason a line does the non-obvious thing is a `//` at that line.',
22
+ },
23
+ schema: [],
24
+ },
25
+ create(context) {
26
+ return {
27
+ Program() {
28
+ const offenders = context.sourceCode
29
+ .getAllComments()
30
+ .filter((comment) => isJSDoc(comment) && !isInlineCast(comment));
31
+
32
+ for (const comment of offenders) {
33
+ context.report({ loc: comment.loc, messageId: 'jsdoc' });
34
+ }
35
+ },
36
+ };
37
+ },
38
+ };
39
+
40
+ export default noJSDoc;
package/oxlintrc.json CHANGED
@@ -25,7 +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",
28
+ "zgeoff/no-jsdoc": "error",
29
+ "zgeoff/max-consecutive-line-comments": ["error", { "max": 3 }],
29
30
  "zgeoff/function-verb": "error",
30
31
 
31
32
  // enforced comment style — line comments unless JSDoc or exclamation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zgeoff/oxlint-config",
3
- "version": "0.0.5",
3
+ "version": "0.1.0",
4
4
  "description": "Shareable oxlint config: strict categories, house-style rules, and a custom JS plugin",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -12,6 +12,8 @@
12
12
  "oxlintrc.json",
13
13
  "plugin.js",
14
14
  "function-verb.js",
15
+ "max-consecutive-line-comments.js",
16
+ "no-jsdoc.js",
15
17
  "index.js"
16
18
  ],
17
19
  "type": "module",
package/plugin.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import functionVerb from './function-verb.js';
2
+ import maxConsecutiveLineComments from './max-consecutive-line-comments.js';
3
+ import noJSDoc from './no-jsdoc.js';
2
4
 
3
- // Custom lint rules, loaded through oxlint's jsPlugins (ESLint v9 rule API).
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.
5
+ // custom rules loaded through oxlint's jsPlugins (ESLint v9 rule API): each
6
+ // bans a shape no native oxlint rule can express, as an esquery selector or a
7
+ // source-comment scan
7
8
 
8
9
  function buildBanRule(type, message, selectors) {
9
10
  return {
@@ -19,82 +20,6 @@ function buildBanRule(type, message, selectors) {
19
20
  };
20
21
  }
21
22
 
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
-
98
23
  const plugin = {
99
24
  meta: { name: 'zgeoff' },
100
25
  rules: {
@@ -167,7 +92,8 @@ const plugin = {
167
92
  'Await into a named const instead of chaining it after a && / || / ?? operator.',
168
93
  ['LogicalExpression > AwaitExpression'],
169
94
  ),
170
- 'no-single-line-jsdoc': noSingleLineJSDoc,
95
+ 'no-jsdoc': noJSDoc,
96
+ 'max-consecutive-line-comments': maxConsecutiveLineComments,
171
97
  'function-verb': functionVerb,
172
98
  },
173
99
  };