@zgeoff/oxlint-config 0.0.2 → 0.0.4

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,14 +2,15 @@
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.
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).
8
9
 
9
10
  ## Usage
10
11
 
11
12
  ```sh
12
- bun add -d @zgeoff/oxlint-config oxlint
13
+ bun add -d @zgeoff/oxlint-config oxlint @stylistic/eslint-plugin
13
14
  ```
14
15
 
15
16
  Extend it from `.oxlintrc.json` — `extends` resolves file paths only, so point at the file inside
@@ -36,6 +37,10 @@ The plugin is addressable on its own at `@zgeoff/oxlint-config/plugin` for confi
36
37
  ## Notes
37
38
 
38
39
  - The config enables the `typescript`, `unicorn`, `oxc`, `import`, and `promise` plugins and loads
39
- `@stylistic/eslint-plugin` (a dependency of this package) through `jsPlugins`.
40
+ `@stylistic/eslint-plugin` through `jsPlugins`. oxlint resolves `jsPlugins` specifiers from the
41
+ project root, not from the config file that names them — so `@stylistic/eslint-plugin` must be a
42
+ direct dependency of the consuming project. Hoisting package managers (bun's default linker, npm)
43
+ resolve it transitively anyway, but isolated layouts (pnpm, `bun install --linker isolated`) fail
44
+ with `Cannot find module '@stylistic/eslint-plugin'` unless it's installed directly.
40
45
  - Type-aware rules activate under `oxlint --type-aware` (requires `oxlint-tsgolint`); the config
41
46
  works without it.
package/oxlintrc.json CHANGED
@@ -23,6 +23,9 @@
23
23
  "zgeoff/no-pick-destructuring": "error",
24
24
  "zgeoff/no-ternary-args": "error",
25
25
  "zgeoff/no-await-args": "error",
26
+ "zgeoff/no-await-in-condition": "error",
27
+ "zgeoff/no-await-in-logical": "error",
28
+ "zgeoff/no-single-line-jsdoc": "error",
26
29
 
27
30
  // enforced comment style — line comments unless JSDoc or exclamation
28
31
  "@stylistic/multiline-comment-style": [
@@ -140,8 +143,9 @@
140
143
  {
141
144
  "files": ["**/*.js", "**/*.cjs", "**/*.mjs"],
142
145
  // untyped JS is `any` end to end, so the unsafe-* rules flag every
143
- // expression
146
+ // expression and strict-boolean-expressions every conditional
144
147
  "rules": {
148
+ "typescript/strict-boolean-expressions": "off",
145
149
  "typescript/no-unsafe-argument": "off",
146
150
  "typescript/no-unsafe-assignment": "off",
147
151
  "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.2",
3
+ "version": "0.0.4",
4
4
  "description": "Shareable oxlint config: strict categories, house-style rules, and a custom JS plugin",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,9 +22,16 @@
22
22
  "publishConfig": {
23
23
  "access": "public"
24
24
  },
25
+ "scripts": {
26
+ "typecheck": "tsc"
27
+ },
25
28
  "dependencies": {
26
29
  "@stylistic/eslint-plugin": "5.10.0"
27
30
  },
31
+ "devDependencies": {
32
+ "@types/bun": "1.3.14",
33
+ "typescript": "7.0.1-rc"
34
+ },
28
35
  "peerDependencies": {
29
36
  "oxlint": ">=1.71.0"
30
37
  }
package/plugin.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // 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.
2
+ // Each rule bans a shape no native oxlint rule can express — esquery selectors
3
+ // for AST shapes, a source-comment scan where the target isn't
4
+ // esquery-selectable.
4
5
 
5
6
  function banSelectors(type, message, selectors) {
6
7
  return {
@@ -16,6 +17,82 @@ function banSelectors(type, message, selectors) {
16
17
  };
17
18
  }
18
19
 
20
+ // mirrors eslint-plugin-jsdoc's default singleLineTags: blocks that exist to
21
+ // cast inline (`/** @type {Foo} */ (bar)`) stay single-line by design
22
+ const inlineTagPattern = /^@(?:type|lends)\b/u;
23
+
24
+ function isSingleLineJSDoc(comment) {
25
+ return (
26
+ comment.type === 'Block' &&
27
+ comment.value.startsWith('*') &&
28
+ comment.loc.start.line === comment.loc.end.line
29
+ );
30
+ }
31
+
32
+ /**
33
+ * Builds the multi-line replacement for a single-line JSDoc comment, or null
34
+ * when the comment shares its line with code — expanding those in place would
35
+ * scramble the surrounding statement, so they are reported without a fix.
36
+ */
37
+ function planExpansion(text, comment) {
38
+ if (!isOwnLine(text, comment)) {
39
+ return null;
40
+ }
41
+
42
+ const lineStart = text.lastIndexOf('\n', comment.range[0] - 1) + 1;
43
+ const indent = text.slice(lineStart, comment.range[0]);
44
+ const body = comment.value.slice(1).trim();
45
+ const bodyLine = body === '' ? `${indent} *` : `${indent} * ${body}`;
46
+
47
+ return `/**\n${bodyLine}\n${indent} */`;
48
+ }
49
+
50
+ function isOwnLine(text, comment) {
51
+ const [start, end] = comment.range;
52
+ const lineStart = text.lastIndexOf('\n', start - 1) + 1;
53
+ const lineEnd = text.indexOf('\n', end);
54
+ const sliceEnd = lineEnd === -1 ? text.length : lineEnd;
55
+
56
+ return /^[ \t]*$/u.test(text.slice(lineStart, start)) && text.slice(end, sliceEnd).trim() === '';
57
+ }
58
+
59
+ const noSingleLineJSDoc = {
60
+ meta: {
61
+ type: 'layout',
62
+ fixable: 'whitespace',
63
+ messages: {
64
+ singleLine:
65
+ 'Write JSDoc blocks multi-line: `/**` alone, one `*`-prefixed line per point, `*/` alone.',
66
+ },
67
+ schema: [],
68
+ },
69
+ create(context) {
70
+ return {
71
+ Program() {
72
+ const offenders = context.sourceCode
73
+ .getAllComments()
74
+ .filter(
75
+ (comment) =>
76
+ isSingleLineJSDoc(comment) && !inlineTagPattern.test(comment.value.slice(1).trim()),
77
+ );
78
+
79
+ for (const comment of offenders) {
80
+ const replacement = planExpansion(context.sourceCode.text, comment);
81
+
82
+ context.report({
83
+ loc: comment.loc,
84
+ messageId: 'singleLine',
85
+ fix:
86
+ replacement === null
87
+ ? undefined
88
+ : (fixer) => fixer.replaceTextRange(comment.range, replacement),
89
+ });
90
+ }
91
+ },
92
+ };
93
+ },
94
+ };
95
+
19
96
  const plugin = {
20
97
  meta: { name: 'zgeoff' },
21
98
  rules: {
@@ -75,6 +152,20 @@ const plugin = {
75
152
  'Await into a named const instead of awaiting inside an argument list.',
76
153
  ['CallExpression > AwaitExpression.arguments', 'NewExpression > AwaitExpression.arguments'],
77
154
  ),
155
+ 'no-await-in-condition': banSelectors(
156
+ 'suggestion',
157
+ 'Await into a named const before the statement instead of inside a control-flow condition.',
158
+ [
159
+ ':matches(IfStatement, WhileStatement, DoWhileStatement, ForStatement, ConditionalExpression) > AwaitExpression.test',
160
+ 'SwitchStatement > AwaitExpression.discriminant',
161
+ ],
162
+ ),
163
+ 'no-await-in-logical': banSelectors(
164
+ 'suggestion',
165
+ 'Await into a named const instead of chaining it after a && / || / ?? operator.',
166
+ ['LogicalExpression > AwaitExpression'],
167
+ ),
168
+ 'no-single-line-jsdoc': noSingleLineJSDoc,
78
169
  },
79
170
  };
80
171