@zgeoff/oxlint-config 0.2.8 → 0.2.10
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 +22 -8
- package/import-zones.js +38 -1
- package/no-abbreviated-names.js +159 -0
- package/oxlintrc.json +30 -2
- package/package.json +2 -1
- package/plugin.js +2 -0
- package/resolve-import-target.js +8 -1
package/README.md
CHANGED
|
@@ -9,7 +9,8 @@ await arguments, awaits hidden inside a control-flow condition or a `&&`/`||`/`?
|
|
|
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
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,
|
|
12
|
+
`zgeoff/no-restricted-words` keeps the words in its `words` option out of a path's text,
|
|
13
|
+
`zgeoff/no-abbreviated-names` reports each abbreviated value name it lists with its full word, and
|
|
13
14
|
`zgeoff/no-file-snapshot` bans file snapshots.
|
|
14
15
|
|
|
15
16
|
## Usage
|
|
@@ -106,6 +107,15 @@ the paths that may name the words:
|
|
|
106
107
|
}
|
|
107
108
|
```
|
|
108
109
|
|
|
110
|
+
## `zgeoff/no-abbreviated-names`
|
|
111
|
+
|
|
112
|
+
The `names` option maps each abbreviation to the text that replaces it, and the rule reports a
|
|
113
|
+
variable, parameter, catch parameter, destructured name, import, property, getter or setter that has
|
|
114
|
+
one of those names. A name matches whole, so `ms` reports `ms` but not `timeoutMs`. A destructured
|
|
115
|
+
key that an alias renames belongs to the object being read, and the rule reports only the alias. The
|
|
116
|
+
shipped config lists the abbreviations that the code-style skill bans and turns on `id-length`
|
|
117
|
+
beside the rule, so a one-letter name fails too, other than `x`, `y`, `z`, `$` and `_`.
|
|
118
|
+
|
|
109
119
|
## `zgeoff/no-file-snapshot`
|
|
110
120
|
|
|
111
121
|
Snapshots are inline only, so the golden value sits in the test that pins it. The rule reports every
|
|
@@ -122,17 +132,21 @@ the boundaries between them. A file belongs to every zone its path matches, so z
|
|
|
122
132
|
Each entry has a `name`, which starts its report, an optional `reason`, which ends it, and optional
|
|
123
133
|
`from` zones and `ignore` globs that limit which importing files it judges.
|
|
124
134
|
|
|
125
|
-
- `edges` judge an import that resolves to a file: relative,
|
|
126
|
-
through its `exports` under `conditions` plus
|
|
127
|
-
|
|
128
|
-
target in
|
|
129
|
-
must match), and `allowWithin: "src/*"` exempts an
|
|
135
|
+
- `edges` judge an import that resolves to a file: relative, with any bundler query such as `?url`
|
|
136
|
+
dropped, or the package's own name resolved through its `exports` under `conditions` plus
|
|
137
|
+
`default`. A target in a `to` zone fails unless it is also in an `allow` zone; with `allow` alone,
|
|
138
|
+
a target in none of the `allow` zones fails, a target in no zone included. `allowTypeOnly` exempts
|
|
139
|
+
type-only imports (`true`, or globs the target must match), and `allowWithin: "src/*"` exempts an
|
|
140
|
+
import inside the importer's own `src/<name>/`.
|
|
130
141
|
- `modules` restrict a package or builtin (`node:` prefix optional, subpaths included) to the
|
|
131
|
-
`allow` zones; `allowTypeOnly: true` exempts type-only imports.
|
|
142
|
+
`allow` zones; `allowTypeOnly: true` exempts type-only imports. A name that ends in `*` restricts
|
|
143
|
+
every specifier that starts with the text before it, such as `@scope/*`, and `node:*` restricts
|
|
144
|
+
every builtin.
|
|
132
145
|
- `globals` restrict a call to a global, bare or through `globalThis`, `self` or `window`.
|
|
133
146
|
- `unzoned` reports a file in a `from` zone that belongs to none of `zones`.
|
|
134
147
|
- `unresolved` reports a relative or self-referencing import that resolves to no file, and a dynamic
|
|
135
|
-
`import()` or `require()` whose specifier is computed.
|
|
148
|
+
`import()`, `require()` or `import.meta.require()` whose specifier is computed. Bun's
|
|
149
|
+
`import.meta.require` used as a value counts as computed too.
|
|
136
150
|
|
|
137
151
|
```jsonc
|
|
138
152
|
{
|
package/import-zones.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isBuiltin } from 'node:module';
|
|
1
2
|
import { relative, sep } from 'node:path';
|
|
2
3
|
import findRepoRoot from './find-repo-root.js';
|
|
3
4
|
import importZonesSchema from './import-zones-schema.js';
|
|
@@ -114,12 +115,25 @@ function isRestrictedModule(entry, specifier) {
|
|
|
114
115
|
const name = toModuleName(specifier);
|
|
115
116
|
|
|
116
117
|
return entry.modules.some((module) => {
|
|
118
|
+
if (module.endsWith('*')) {
|
|
119
|
+
return isPrefixMatch(module.slice(0, -1), specifier);
|
|
120
|
+
}
|
|
121
|
+
|
|
117
122
|
const restricted = toModuleName(module);
|
|
118
123
|
|
|
119
124
|
return name === restricted || name.startsWith(`${restricted}/`);
|
|
120
125
|
});
|
|
121
126
|
}
|
|
122
127
|
|
|
128
|
+
// `node:*` names every builtin, which a specifier may write without the prefix
|
|
129
|
+
function isPrefixMatch(prefix, specifier) {
|
|
130
|
+
if (specifier.startsWith(prefix)) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return prefix === 'node:' && isBuiltin(specifier);
|
|
135
|
+
}
|
|
136
|
+
|
|
123
137
|
function toModuleName(specifier) {
|
|
124
138
|
return specifier.replace(/^node:/u, '');
|
|
125
139
|
}
|
|
@@ -235,6 +249,18 @@ function isTypeOnlyExport(node) {
|
|
|
235
249
|
);
|
|
236
250
|
}
|
|
237
251
|
|
|
252
|
+
function isImportMetaRequire(node) {
|
|
253
|
+
return (
|
|
254
|
+
node.type === 'MemberExpression' &&
|
|
255
|
+
!node.computed &&
|
|
256
|
+
node.object.type === 'MetaProperty' &&
|
|
257
|
+
node.object.meta.name === 'import' &&
|
|
258
|
+
node.object.property.name === 'meta' &&
|
|
259
|
+
node.property.type === 'Identifier' &&
|
|
260
|
+
node.property.name === 'require'
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
238
264
|
const importZones = {
|
|
239
265
|
meta: {
|
|
240
266
|
type: 'problem',
|
|
@@ -284,10 +310,21 @@ const importZones = {
|
|
|
284
310
|
CallExpression(node) {
|
|
285
311
|
checkGlobalCall(file, node);
|
|
286
312
|
|
|
287
|
-
if (
|
|
313
|
+
if (
|
|
314
|
+
(node.callee.type === 'Identifier' && node.callee.name === 'require') ||
|
|
315
|
+
isImportMetaRequire(node.callee)
|
|
316
|
+
) {
|
|
288
317
|
checkImport(file, { node, source: node.arguments[0], typeOnly: false });
|
|
289
318
|
}
|
|
290
319
|
},
|
|
320
|
+
|
|
321
|
+
// Bun's import.meta.require used as a value, such as `const load =
|
|
322
|
+
// import.meta.require`, loads modules no specifier names
|
|
323
|
+
MemberExpression(node) {
|
|
324
|
+
if (isImportMetaRequire(node) && node.parent?.callee !== node) {
|
|
325
|
+
emitUnresolved(file, node, { messageId: 'computed', data: {} });
|
|
326
|
+
}
|
|
327
|
+
},
|
|
291
328
|
};
|
|
292
329
|
},
|
|
293
330
|
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// each `names` key is a short name and its value the text that replaces it;
|
|
2
|
+
// the rule matches a whole name, so `timeoutMs` passes when `ms` is listed
|
|
3
|
+
const noAbbreviatedNames = {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'suggestion',
|
|
6
|
+
messages: {
|
|
7
|
+
abbreviated:
|
|
8
|
+
'Rename `{{name}}` to {{replacement}}: name a value for what it holds, in full words.',
|
|
9
|
+
},
|
|
10
|
+
schema: [
|
|
11
|
+
{
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
names: { type: 'object', additionalProperties: { type: 'string', minLength: 1 } },
|
|
15
|
+
},
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
const names = new Map(Object.entries(context.options[0]?.names ?? {}));
|
|
22
|
+
|
|
23
|
+
if (names.size === 0) {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const checkName = (node) => {
|
|
28
|
+
const isName = node?.type === 'Identifier' || node?.type === 'PrivateIdentifier';
|
|
29
|
+
|
|
30
|
+
if (!isName || !names.has(node.name)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const prefix = node.type === 'PrivateIdentifier' ? '#' : '';
|
|
35
|
+
|
|
36
|
+
context.report({
|
|
37
|
+
node,
|
|
38
|
+
messageId: 'abbreviated',
|
|
39
|
+
data: { name: `${prefix}${node.name}`, replacement: names.get(node.name) },
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const checkPattern = (node) => {
|
|
44
|
+
for (const identifier of collectPatternIdentifiers(node)) {
|
|
45
|
+
checkName(identifier);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const checkKey = (node) => {
|
|
50
|
+
if (!node.computed) {
|
|
51
|
+
checkName(node.key);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const checkParams = (node) => {
|
|
56
|
+
for (const param of node.params) {
|
|
57
|
+
checkPattern(param);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
VariableDeclarator(node) {
|
|
63
|
+
checkPattern(node.id);
|
|
64
|
+
},
|
|
65
|
+
FunctionDeclaration: checkParams,
|
|
66
|
+
FunctionExpression: checkParams,
|
|
67
|
+
ArrowFunctionExpression: checkParams,
|
|
68
|
+
|
|
69
|
+
// a signature without a body names its parameters as an implementation does
|
|
70
|
+
TSDeclareFunction: checkParams,
|
|
71
|
+
TSEmptyBodyFunctionExpression: checkParams,
|
|
72
|
+
TSFunctionType: checkParams,
|
|
73
|
+
TSConstructorType: checkParams,
|
|
74
|
+
TSMethodSignature: checkParams,
|
|
75
|
+
TSCallSignatureDeclaration: checkParams,
|
|
76
|
+
TSConstructSignatureDeclaration: checkParams,
|
|
77
|
+
CatchClause(node) {
|
|
78
|
+
checkPattern(node.param);
|
|
79
|
+
},
|
|
80
|
+
'ObjectExpression > Property': checkKey,
|
|
81
|
+
PropertyDefinition: checkKey,
|
|
82
|
+
AccessorProperty: checkKey,
|
|
83
|
+
TSAbstractPropertyDefinition: checkKey,
|
|
84
|
+
TSAbstractAccessorProperty: checkKey,
|
|
85
|
+
TSPropertySignature: checkKey,
|
|
86
|
+
|
|
87
|
+
// a getter or setter is read and written as a property, so its key is a
|
|
88
|
+
// value name
|
|
89
|
+
"MethodDefinition[kind='get']": checkKey,
|
|
90
|
+
"MethodDefinition[kind='set']": checkKey,
|
|
91
|
+
"TSAbstractMethodDefinition[kind='get']": checkKey,
|
|
92
|
+
"TSAbstractMethodDefinition[kind='set']": checkKey,
|
|
93
|
+
"TSMethodSignature[kind='get']": checkKey,
|
|
94
|
+
"TSMethodSignature[kind='set']": checkKey,
|
|
95
|
+
|
|
96
|
+
// an import's local binding is a value name in this module, whatever the
|
|
97
|
+
// exporting module calls it; a type-only import names a type, not a value
|
|
98
|
+
'ImportSpecifier, ImportDefaultSpecifier, ImportNamespaceSpecifier'(node) {
|
|
99
|
+
if (!isTypeOnlyImport(node)) {
|
|
100
|
+
checkName(node.local);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
TSImportEqualsDeclaration(node) {
|
|
104
|
+
if (node.importKind !== 'type') {
|
|
105
|
+
checkName(node.id);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
'AssignmentExpression > MemberExpression.left'(node) {
|
|
109
|
+
if (!node.computed) {
|
|
110
|
+
checkName(node.property);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
function isTypeOnlyImport(specifier) {
|
|
118
|
+
return specifier.importKind === 'type' || specifier.parent.importKind === 'type';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// the names a binding pattern declares: a destructured key's alias, not the
|
|
122
|
+
// key, since the key belongs to the object being read
|
|
123
|
+
function collectPatternIdentifiers(node) {
|
|
124
|
+
if (node === null || node === undefined) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (node.type === 'Identifier') {
|
|
129
|
+
return [node];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (node.type === 'ObjectPattern') {
|
|
133
|
+
return node.properties.flatMap((property) => {
|
|
134
|
+
const binding = property.type === 'RestElement' ? property : property.value;
|
|
135
|
+
|
|
136
|
+
return collectPatternIdentifiers(binding);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (node.type === 'ArrayPattern') {
|
|
141
|
+
return node.elements.flatMap((element) => collectPatternIdentifiers(element));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (node.type === 'AssignmentPattern') {
|
|
145
|
+
return collectPatternIdentifiers(node.left);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (node.type === 'RestElement') {
|
|
149
|
+
return collectPatternIdentifiers(node.argument);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (node.type === 'TSParameterProperty') {
|
|
153
|
+
return collectPatternIdentifiers(node.parameter);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export default noAbbreviatedNames;
|
package/oxlintrc.json
CHANGED
|
@@ -33,6 +33,35 @@
|
|
|
33
33
|
"zgeoff/test-beside-module": "error",
|
|
34
34
|
"zgeoff/no-file-snapshot": "error",
|
|
35
35
|
|
|
36
|
+
// name a value for what it holds, in full words: one letter only for a
|
|
37
|
+
// coordinate, zod's `z`, Bun's shell `$` or an unread `_` placeholder, and
|
|
38
|
+
// each listed abbreviation reported with its full word; both rules read
|
|
39
|
+
// every binding (destructured aliases and imports included) and every
|
|
40
|
+
// property key, but a type parameter is not a value, so generics stay
|
|
41
|
+
// unchecked
|
|
42
|
+
"id-length": [
|
|
43
|
+
"error",
|
|
44
|
+
{ "min": 2, "exceptions": ["x", "y", "z", "$", "_"], "checkGeneric": false }
|
|
45
|
+
],
|
|
46
|
+
// oxlintrc.test.ts holds the same names with their full words, and fails
|
|
47
|
+
// when an entry changes here without it
|
|
48
|
+
"zgeoff/no-abbreviated-names": [
|
|
49
|
+
"error",
|
|
50
|
+
{
|
|
51
|
+
"names": {
|
|
52
|
+
"mgr": "`manager`",
|
|
53
|
+
"dir": "`directory`",
|
|
54
|
+
"tmp": "`temporary`",
|
|
55
|
+
"proc": "`process`",
|
|
56
|
+
"src": "`source`",
|
|
57
|
+
"dest": "`destination`",
|
|
58
|
+
"prev": "`previous`",
|
|
59
|
+
"msg": "`message`",
|
|
60
|
+
"ms": "a name for the value with `Ms` as its unit suffix, such as `timeoutMs`"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
|
|
36
65
|
// enforced comment style — line comments unless JSDoc or exclamation
|
|
37
66
|
"@stylistic/multiline-comment-style": [
|
|
38
67
|
"error",
|
|
@@ -114,14 +143,13 @@
|
|
|
114
143
|
// category-enabled rules that contradict this config's style: inline named
|
|
115
144
|
// export declarations are the convention (group-exports demands the exact
|
|
116
145
|
// form no-bare-named-exports bans), keys group by meaning not alphabet,
|
|
117
|
-
// comments stay lowercase,
|
|
146
|
+
// comments stay lowercase, and literal numbers are fine
|
|
118
147
|
"import/prefer-default-export": "off",
|
|
119
148
|
"import/no-named-export": "off",
|
|
120
149
|
"import/exports-last": "off",
|
|
121
150
|
"import/group-exports": "off",
|
|
122
151
|
"sort-keys": "off",
|
|
123
152
|
"capitalized-comments": "off",
|
|
124
|
-
"id-length": "off",
|
|
125
153
|
"no-magic-numbers": "off",
|
|
126
154
|
// prefer-ternary (only-single-line, above) wants ternaries where they fit
|
|
127
155
|
// on one line; no-ternary would ban the same code
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zgeoff/oxlint-config",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "Shareable oxlint config: strict categories, house-style rules, and a custom JS plugin",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"plugin.js",
|
|
14
14
|
"function-verb.js",
|
|
15
15
|
"no-restricted-words.js",
|
|
16
|
+
"no-abbreviated-names.js",
|
|
16
17
|
"max-consecutive-line-comments.js",
|
|
17
18
|
"no-jsdoc.js",
|
|
18
19
|
"no-comment-rule-override.js",
|
package/plugin.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
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
|
+
import noAbbreviatedNames from './no-abbreviated-names.js';
|
|
4
5
|
import noCommentRuleOverride from './no-comment-rule-override.js';
|
|
5
6
|
import noFileSnapshot from './no-file-snapshot.js';
|
|
6
7
|
import noJSDoc from './no-jsdoc.js';
|
|
@@ -107,6 +108,7 @@ const plugin = {
|
|
|
107
108
|
'no-comment-rule-override': noCommentRuleOverride,
|
|
108
109
|
'function-verb': functionVerb,
|
|
109
110
|
'no-restricted-words': noRestrictedWords,
|
|
111
|
+
'no-abbreviated-names': noAbbreviatedNames,
|
|
110
112
|
'test-beside-module': testBesideModule,
|
|
111
113
|
'no-file-snapshot': noFileSnapshot,
|
|
112
114
|
'import-zones': importZones,
|
package/resolve-import-target.js
CHANGED
|
@@ -9,7 +9,7 @@ const sourceExtensions = { '.js': ['.ts', '.tsx'], '.mjs': ['.mts'], '.cjs': ['.
|
|
|
9
9
|
// own name resolves through its `exports`, and `#` names through its `imports`.
|
|
10
10
|
export default function resolveImportTarget(filename, specifier, settings) {
|
|
11
11
|
if (isPathSpecifier(specifier)) {
|
|
12
|
-
return toTarget(findImportedFile(filename, specifier, settings.extensions));
|
|
12
|
+
return toTarget(findImportedFile(filename, toFileSpecifier(specifier), settings.extensions));
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const manifest = findPackageManifest(dirname(filename), settings.root);
|
|
@@ -40,6 +40,13 @@ function resolveManifestTarget(manifest, specifier, conditions) {
|
|
|
40
40
|
return { kind: 'module', specifier };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// a bundler query such as Vite's `./styles.css?url` names the file before it
|
|
44
|
+
function toFileSpecifier(specifier) {
|
|
45
|
+
const query = specifier.indexOf('?');
|
|
46
|
+
|
|
47
|
+
return query === -1 ? specifier : specifier.slice(0, query);
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
function toTarget(path) {
|
|
44
51
|
return path === null ? { kind: 'missing' } : { kind: 'file', path };
|
|
45
52
|
}
|