@zgeoff/oxlint-config 0.2.6 → 0.2.8

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
@@ -8,7 +8,9 @@ await arguments, awaits hidden inside a control-flow condition or a `&&`/`||`/`?
8
8
  `switch` statement, every JSDoc block, any run of more than three line comments, and a repo config
9
9
  that loosens either comment rule. `zgeoff/function-verb` enforces the function-naming taxonomy from
10
10
  the shared agent guidelines, `zgeoff/test-beside-module` keeps each test file beside the module it
11
- tests, and `zgeoff/import-zones` fails an import that crosses a module boundary the repo declares.
11
+ tests, `zgeoff/import-zones` fails an import that crosses a module boundary the repo declares,
12
+ `zgeoff/no-restricted-words` keeps the words in its `words` option out of a path's text, and
13
+ `zgeoff/no-file-snapshot` bans file snapshots.
12
14
 
13
15
  ## Usage
14
16
 
@@ -85,6 +87,34 @@ first statement, so put the comment above it:
85
87
  A second test file for a module (`<name>.host.test.ts`) needs its own `<name>.host.<ext>`, so the
86
88
  rule reports it beside only `<name>.ts`.
87
89
 
90
+ ## `zgeoff/no-restricted-words`
91
+
92
+ Off unless a repo configures it. The `words` option lists the words to keep out of the text of a
93
+ file: string literals, template text and comments. A word matches whole and without case, so `imp`
94
+ reports `'IMP daemon'` and `'imp-e2e'` but not `'impl'` or `'imp_name'`. Identifiers are code, not
95
+ text, and the rule never reads them. Scope the rule to a path with `overrides`, and turn it off for
96
+ the paths that may name the words:
97
+
98
+ ```jsonc
99
+ {
100
+ "rules": {
101
+ "zgeoff/no-restricted-words": ["error", { "words": ["imp", "impd"] }],
102
+ },
103
+ "overrides": [
104
+ { "files": ["packages/execution/**"], "rules": { "zgeoff/no-restricted-words": "off" } },
105
+ ],
106
+ }
107
+ ```
108
+
109
+ ## `zgeoff/no-file-snapshot`
110
+
111
+ Snapshots are inline only, so the golden value sits in the test that pins it. The rule reports every
112
+ `toMatchSnapshot` and `toThrowErrorMatchingSnapshot`, which write a `.snap` file;
113
+ `toMatchInlineSnapshot` and `toThrowErrorMatchingInlineSnapshot` stay allowed. It also reports a
114
+ linted file that has a `__snapshots__/<file name>.snap` beside it, at the first statement, so a
115
+ file-level disable comment covers it. oxlint never visits a `.snap` file, so a snapshot file whose
116
+ test file is gone is not reported.
117
+
88
118
  ## `zgeoff/import-zones`
89
119
 
90
120
  Off unless a repo configures it. The options name zones (path globs, relative to the repo root) and
@@ -0,0 +1,58 @@
1
+ import { statSync } from 'node:fs';
2
+ import { basename, dirname, join } from 'node:path';
3
+
4
+ // the matchers that write `__snapshots__/<test file>.snap`; their inline
5
+ // counterparts keep the golden value in the test itself
6
+ const fileMatchers = ['toMatchSnapshot', 'toThrowErrorMatchingSnapshot'];
7
+
8
+ const noFileSnapshot = {
9
+ meta: {
10
+ type: 'problem',
11
+ messages: {
12
+ matcher:
13
+ 'Replace `{{matcher}}` with its inline form: snapshots are inline only, so the golden value sits in the test.',
14
+ file: 'Delete `__snapshots__/{{name}}` and pin its values inline: snapshots are inline only, so the golden value sits in the test.',
15
+ },
16
+ schema: [],
17
+ },
18
+ create(context) {
19
+ const entries = fileMatchers
20
+ .flatMap((matcher) => [
21
+ `MemberExpression[computed=false] > Identifier.property[name='${matcher}']`,
22
+ `MemberExpression[computed=true] > Literal.property[value='${matcher}']`,
23
+ ])
24
+ .map((selector) => [
25
+ selector,
26
+ (node) =>
27
+ context.report({ node, messageId: 'matcher', data: { matcher: getMatcherName(node) } }),
28
+ ]);
29
+
30
+ return {
31
+ ...Object.fromEntries(entries),
32
+ Program() {
33
+ const filename = context.physicalFilename;
34
+ const name = `${basename(filename)}.snap`;
35
+
36
+ if (!isFile(join(dirname(filename), '__snapshots__', name))) {
37
+ return;
38
+ }
39
+
40
+ // anchor on the first statement, not offset 0, so a disable comment
41
+ // at the top of the file sits before the report and covers it
42
+ const start = context.sourceCode.ast.body[0]?.loc.start ?? { line: 1, column: 0 };
43
+
44
+ context.report({ loc: { start, end: start }, messageId: 'file', data: { name } });
45
+ },
46
+ };
47
+ },
48
+ };
49
+
50
+ function getMatcherName(node) {
51
+ return node.type === 'Identifier' ? node.name : node.value;
52
+ }
53
+
54
+ function isFile(path) {
55
+ return statSync(path, { throwIfNoEntry: false })?.isFile() ?? false;
56
+ }
57
+
58
+ export default noFileSnapshot;
@@ -0,0 +1,61 @@
1
+ const noRestrictedWords = {
2
+ meta: {
3
+ type: 'problem',
4
+ messages: {
5
+ restricted:
6
+ 'Remove `{{word}}` from this {{place}}: the repo config restricts the word in this path.',
7
+ },
8
+ schema: [
9
+ {
10
+ type: 'object',
11
+ properties: {
12
+ words: { type: 'array', items: { type: 'string', minLength: 1 }, uniqueItems: true },
13
+ },
14
+ additionalProperties: false,
15
+ },
16
+ ],
17
+ },
18
+ create(context) {
19
+ const words = context.options[0]?.words ?? [];
20
+
21
+ if (words.length === 0) {
22
+ return {};
23
+ }
24
+
25
+ const pattern = buildWordPattern(words);
26
+
27
+ const checkText = (node, text, place) => {
28
+ for (const match of text.matchAll(pattern)) {
29
+ context.report({ node, messageId: 'restricted', data: { word: match[0], place } });
30
+ }
31
+ };
32
+
33
+ return {
34
+ Literal(node) {
35
+ if (typeof node.value === 'string') {
36
+ checkText(node, node.value, 'string');
37
+ }
38
+ },
39
+ TemplateElement(node) {
40
+ checkText(node, node.value.cooked ?? node.value.raw, 'template');
41
+ },
42
+ Program() {
43
+ for (const comment of context.sourceCode.getAllComments()) {
44
+ checkText(comment, comment.value, 'comment');
45
+ }
46
+ },
47
+ };
48
+ },
49
+ };
50
+
51
+ // a letter, digit or underscore on either side makes the match part of a
52
+ // longer word (`impl`, `simple`), so only those characters bound it
53
+ function buildWordPattern(words) {
54
+ const alternatives = words.map((word) =>
55
+ word.replaceAll(/[$()*+.?[\\\]^{|}]/gu, String.raw`\$&`),
56
+ );
57
+
58
+ return new RegExp(`(?<![\\p{L}\\p{N}_])(?:${alternatives.join('|')})(?![\\p{L}\\p{N}_])`, 'giu');
59
+ }
60
+
61
+ export default noRestrictedWords;
package/oxlintrc.json CHANGED
@@ -31,6 +31,7 @@
31
31
  "zgeoff/no-comment-rule-override": "error",
32
32
  "zgeoff/function-verb": "error",
33
33
  "zgeoff/test-beside-module": "error",
34
+ "zgeoff/no-file-snapshot": "error",
34
35
 
35
36
  // enforced comment style — line comments unless JSDoc or exclamation
36
37
  "@stylistic/multiline-comment-style": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zgeoff/oxlint-config",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Shareable oxlint config: strict categories, house-style rules, and a custom JS plugin",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -12,10 +12,12 @@
12
12
  "oxlintrc.json",
13
13
  "plugin.js",
14
14
  "function-verb.js",
15
+ "no-restricted-words.js",
15
16
  "max-consecutive-line-comments.js",
16
17
  "no-jsdoc.js",
17
18
  "no-comment-rule-override.js",
18
19
  "test-beside-module.js",
20
+ "no-file-snapshot.js",
19
21
  "import-zones.js",
20
22
  "import-zones-schema.js",
21
23
  "find-repo-root.js",
@@ -40,8 +42,8 @@
40
42
  "@stylistic/eslint-plugin": "5.10.0"
41
43
  },
42
44
  "devDependencies": {
43
- "@types/bun": "1.3.14",
44
- "typescript": "7.0.1-rc"
45
+ "@types/bun": "1.4.2",
46
+ "typescript": "7.0.2"
45
47
  },
46
48
  "peerDependencies": {
47
49
  "oxlint": ">=1.78.0"
package/plugin.js CHANGED
@@ -2,7 +2,9 @@ import functionVerb from './function-verb.js';
2
2
  import importZones from './import-zones.js';
3
3
  import maxConsecutiveLineComments from './max-consecutive-line-comments.js';
4
4
  import noCommentRuleOverride from './no-comment-rule-override.js';
5
+ import noFileSnapshot from './no-file-snapshot.js';
5
6
  import noJSDoc from './no-jsdoc.js';
7
+ import noRestrictedWords from './no-restricted-words.js';
6
8
  import testBesideModule from './test-beside-module.js';
7
9
 
8
10
  // custom rules loaded through oxlint's jsPlugins (ESLint v9 rule API): each
@@ -104,7 +106,9 @@ const plugin = {
104
106
  'max-consecutive-line-comments': maxConsecutiveLineComments,
105
107
  'no-comment-rule-override': noCommentRuleOverride,
106
108
  'function-verb': functionVerb,
109
+ 'no-restricted-words': noRestrictedWords,
107
110
  'test-beside-module': testBesideModule,
111
+ 'no-file-snapshot': noFileSnapshot,
108
112
  'import-zones': importZones,
109
113
  },
110
114
  };