@jameswyse/oxc-config 0.5.0 → 0.6.1
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/REFERENCE.md +2 -2
- package/dist/rules/no-ambient-clock.js +15 -0
- package/dist/rules/no-env-access.js +16 -3
- package/dist/shared/index.js +23 -14
- package/package.json +2 -2
package/REFERENCE.md
CHANGED
|
@@ -72,10 +72,10 @@ Native rules: `prefer-const`, `no-param-reassign` and `oxc/no-accumulating-sprea
|
|
|
72
72
|
|
|
73
73
|
These rules run only once the project names its owners through the [project boundary options](docs/configuration.md#enforce-project-boundaries).
|
|
74
74
|
|
|
75
|
-
- `wyse/no-env-access` (`env`): reading `process.env` or `import.meta.env` outside the owners, config files and scripts. That includes
|
|
75
|
+
- `wyse/no-env-access` (`env`): reading `process.env` or `import.meta.env` outside the owners, config files and scripts. That includes `globalThis.process.env`, `global.process.env`, `process["env"]` and `Reflect.get(process, "env")`, including with `"env" as const`, importing `env` from `node:process` or `process`, reading `env` from those modules' default, namespace, literal dynamic import or `require`, including through a loader made with `createRequire` from `node:module` or `module` or a `const` copy of a loader, and re-exporting `env`, the default or the whole module from them with `export { ... } from` or `export * from`. Destructuring `env` from any of these counts in a declaration, a parameter default such as `({ env } = process) => env`, or an assignment. Destructuring `env` through a namespace's `default`, such as `({ default: { env } } = p)`, or into a rest element, such as `({ ...rest } = process)`, isn't followed. Other `node:process` imports and re-exports, such as `argv`, type-only re-exports, a `require` function the file defines itself, a loader variable that's reassigned, and a `process` property on `window` or `self` pass. It doesn't check that the owner validates what it reads.
|
|
76
76
|
- `wyse/import-boundaries` (`boundaries`): a value import, re-export, literal dynamic import, or `require` call, including a loader made with `createRequire` from `node:module` or `module`, whether stored, copied to another `const` or called directly as `createRequire(import.meta.url)("…")`, that matches a `deny` glob. A loader variable that's reassigned doesn't count. Type-only imports and `import("…")` types pass unless the boundary sets `types: true`. It doesn't check computed imports.
|
|
77
77
|
- `wyse/server-action-guard` (`serverActionGuards`): a `"use server"` action that doesn't start by awaiting a guard, and indirect or default exports from server-action modules. Non-exported helpers and test files pass.
|
|
78
|
-
- `wyse/no-ambient-clock` (`clock`): `Date.now()`, `Date()` and argument-less `new Date()`, including through `globalThis` or `window`. It also reports argument-less `new TZDate()` and `new TZDateMini()` and one-argument `TZDate.tz(zone)` from `@date-fns/tz`, `@date-fns/tz/date` or `@date-fns/tz/date/mini`, and the date-fns helpers that read the current time: `constructNow`, the `startOf` and `endOf` helpers for today, tomorrow and yesterday, `isToday`, `isTomorrow`, `isYesterday`, `isPast`, `isFuture`, the `isThis*` helpers, `formatDistanceToNow`, `formatDistanceToNowStrict`, `isMatch`, and `isMatch` and `isMatchWithOptions` from `date-fns/fp`. A helper reports where it's called, where it's passed or stored uncalled, such as `dates.filter(isPast)` or `{ check: isToday }`, and where it's exported by local name, such as `export { isPast }`. A `date-fns/fp` helper reports once at the end of its call chain, so the partial application in `values.filter(isMatch("yyyy"))` reports too. A `const` alias reports where it's used
|
|
78
|
+
- `wyse/no-ambient-clock` (`clock`): `Date.now()`, `Date()` and argument-less `new Date()`, including through `globalThis` or `window`. It also reports argument-less `new TZDate()` and `new TZDateMini()` and one-argument `TZDate.tz(zone)` from `@date-fns/tz`, `@date-fns/tz/date` or `@date-fns/tz/date/mini`, and the date-fns helpers that read the current time: `constructNow`, the `startOf` and `endOf` helpers for today, tomorrow and yesterday, `isToday`, `isTomorrow`, `isYesterday`, `isPast`, `isFuture`, the `isThis*` helpers, `formatDistanceToNow`, `formatDistanceToNowStrict`, `isMatch`, and `isMatch` and `isMatchWithOptions` from `date-fns/fp`. A helper reports where it's called, where it's passed or stored uncalled, such as `dates.filter(isPast)` or `{ check: isToday }`, and where it's exported by local name, such as `export { isPast }`. A `date-fns/fp` helper reports once at the end of its call chain, so the partial application in `values.filter(isMatch("yyyy"))` reports too. A `const` alias reports where it's used, and an exported one where it's declared, including a name destructured from a namespace, such as `export const { isPast } = dates`. Helpers count whether they're imported by name, through a namespace, from a `date-fns/<name>` subpath, by literal dynamic import or by `require`. Re-exporting one of these helpers with `export { ... } from`, or a module that contains one, such as `date-fns` or `date-fns/fp`, with `export * from`, also reports, because calls through the re-export no longer resolve to date-fns. Helpers that compare with a given date, such as `isSameDay`, type positions such as `typeof isPast`, type-only imports and exports, and re-exports of `TZDate`, `TZDateMini`, other date-fns helpers or types pass. Parameter defaults, such as `check = isPast`, uncalled `Date.now`, provider functions that only return the read, including through `.call`, `.apply` or `.bind`, write-only uses of a helper's name on a module object, such as `d.isPast = check`, `[d.isPast] = values`, `for (d.isPast of values)`, `delete d.isPast` and `delete d?.isPast`, and test files pass. Parentheses, `as`, `satisfies` and `!` don't change the result, so `(Date.now as typeof Date.now)()` reports and `{ now: () => startOfToday() as Date }` passes.
|
|
79
79
|
- `wyse/no-ambient-random` (`random`): `Math.random()`, `crypto.randomUUID()`, and `randomUUID` or `randomInt` from `node:crypto`. The same exceptions and wrapper handling apply. `randomBytes` and `getRandomValues` pass because they generate secrets.
|
|
80
80
|
|
|
81
81
|
## Tests
|
|
@@ -109,6 +109,16 @@ function isFollowedAlias(node) {
|
|
|
109
109
|
declaration.kind === "const" &&
|
|
110
110
|
declaration.parent.type !== "ExportNamedDeclaration");
|
|
111
111
|
}
|
|
112
|
+
function destructuredExports(sourceCode, node) {
|
|
113
|
+
const declaration = node.declaration;
|
|
114
|
+
if (declaration?.type !== "VariableDeclaration") {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
return declaration.declarations
|
|
118
|
+
.filter((declarator) => declarator.id.type === "ObjectPattern")
|
|
119
|
+
.flatMap((declarator) => sourceCode.getDeclaredVariables(declarator))
|
|
120
|
+
.flatMap((variable) => variable.identifiers);
|
|
121
|
+
}
|
|
112
122
|
function isTimeZoneDate(sourceCode, node) {
|
|
113
123
|
const binding = importedBinding(sourceCode, node);
|
|
114
124
|
return binding !== null && timeZoneModules.has(binding.source) && timeZoneDates.has(binding.name);
|
|
@@ -172,6 +182,11 @@ export const noAmbientClock = defineRule({
|
|
|
172
182
|
}
|
|
173
183
|
},
|
|
174
184
|
ExportNamedDeclaration(node) {
|
|
185
|
+
for (const binding of destructuredExports(sourceCode, node)) {
|
|
186
|
+
if (isClockHelper(sourceCode, binding)) {
|
|
187
|
+
context.report({ node: binding, messageId: "clock" });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
175
190
|
if (!node.source || node.exportKind === "type") {
|
|
176
191
|
return;
|
|
177
192
|
}
|
|
@@ -20,6 +20,11 @@ function isEnvironmentSource(sourceCode, node) {
|
|
|
20
20
|
isImportMeta(node) ||
|
|
21
21
|
isProcessModule(sourceCode, node));
|
|
22
22
|
}
|
|
23
|
+
function destructuresEnvironment(sourceCode, pattern, value) {
|
|
24
|
+
return (pattern.type === "ObjectPattern" &&
|
|
25
|
+
isEnvironmentSource(sourceCode, value) &&
|
|
26
|
+
pattern.properties.some((property) => property.type === "Property" && staticPropertyName(property) === "env"));
|
|
27
|
+
}
|
|
23
28
|
function isReflectGet(sourceCode, node) {
|
|
24
29
|
const callee = unwrapExpression(node.callee);
|
|
25
30
|
const [target, key] = node.arguments;
|
|
@@ -48,9 +53,17 @@ export const noEnvAccess = defineRule({
|
|
|
48
53
|
}
|
|
49
54
|
},
|
|
50
55
|
VariableDeclarator(node) {
|
|
51
|
-
if (node.id.
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
if (destructuresEnvironment(sourceCode, node.id, node.init)) {
|
|
57
|
+
context.report({ node, messageId: "env" });
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
AssignmentPattern(node) {
|
|
61
|
+
if (destructuresEnvironment(sourceCode, node.left, node.right)) {
|
|
62
|
+
context.report({ node, messageId: "env" });
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
AssignmentExpression(node) {
|
|
66
|
+
if (destructuresEnvironment(sourceCode, node.left, node.right)) {
|
|
54
67
|
context.report({ node, messageId: "env" });
|
|
55
68
|
}
|
|
56
69
|
},
|
package/dist/shared/index.js
CHANGED
|
@@ -133,13 +133,6 @@ function memberBinding(owner, name) {
|
|
|
133
133
|
? { source: owner.source, name }
|
|
134
134
|
: null;
|
|
135
135
|
}
|
|
136
|
-
function bindsName(property, name) {
|
|
137
|
-
if (property.type !== "Property") {
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
const target = property.value.type === "AssignmentPattern" ? property.value.left : property.value;
|
|
141
|
-
return target.type === "Identifier" && target.name === name;
|
|
142
|
-
}
|
|
143
136
|
function constAlias(definition) {
|
|
144
137
|
const declaration = definition.parent;
|
|
145
138
|
const declarator = definition.node;
|
|
@@ -151,19 +144,35 @@ function constAlias(definition) {
|
|
|
151
144
|
? { init: declarator.init, pattern: declarator.id, name: definition.name.name }
|
|
152
145
|
: null;
|
|
153
146
|
}
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
147
|
+
function destructuredPath(pattern, name) {
|
|
148
|
+
if (pattern.type !== "ObjectPattern") {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
for (const property of pattern.properties) {
|
|
152
|
+
const key = property.type === "Property" ? staticPropertyName(property) : null;
|
|
153
|
+
if (property.type !== "Property" || key === null) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const target = property.value.type === "AssignmentPattern" ? property.value.left : property.value;
|
|
157
|
+
if (target.type === "Identifier" && target.name === name) {
|
|
158
|
+
return [key];
|
|
159
|
+
}
|
|
160
|
+
const path = destructuredPath(target, name);
|
|
161
|
+
if (path !== null) {
|
|
162
|
+
return [key, ...path];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
159
166
|
}
|
|
160
167
|
function aliasTarget(alias, resolve, member) {
|
|
161
168
|
const initial = resolve(alias.init);
|
|
162
169
|
if (alias.pattern.type === "Identifier") {
|
|
163
170
|
return initial;
|
|
164
171
|
}
|
|
165
|
-
const
|
|
166
|
-
return
|
|
172
|
+
const path = destructuredPath(alias.pattern, alias.name);
|
|
173
|
+
return path === null
|
|
174
|
+
? null
|
|
175
|
+
: path.reduce((owner, name) => (owner === null ? null : member(owner, name)), initial);
|
|
167
176
|
}
|
|
168
177
|
function cached(cache, variable, resolve) {
|
|
169
178
|
if (!cache.has(variable)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jameswyse/oxc-config",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Opinionated Oxlint and Oxfmt configs with custom rules for TypeScript and JavaScript projects",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"config",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@e18e/eslint-plugin": "0.8.1",
|
|
42
|
-
"@oxlint/plugins": "1.
|
|
42
|
+
"@oxlint/plugins": ">=1.86.0 <2",
|
|
43
43
|
"eslint-plugin-react-you-might-not-need-an-effect": "1.0.2"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|