@rdjperron/lint 0.2.0 → 0.5.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.
Files changed (55) hide show
  1. package/README.md +210 -93
  2. package/dist/config-CD2iCM6L.mjs +346 -0
  3. package/dist/config-CD2iCM6L.mjs.map +1 -0
  4. package/dist/config.d.mts +121 -14
  5. package/dist/config.mjs +2 -218
  6. package/dist/index.d.mts +2 -2
  7. package/dist/index.mjs +94 -26
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/verify.d.mts +1 -92
  10. package/dist/verify.mjs +125 -257
  11. package/dist/verify.mjs.map +1 -1
  12. package/fixtures/README.md +18 -25
  13. package/fixtures/no-array-filter-map.fixture.ts +1 -1
  14. package/fixtures/no-chained-type-assertions.fixture.ts +1 -1
  15. package/fixtures/no-conditional-empty-object-spread.fixture.ts +1 -1
  16. package/fixtures/no-hardcoded-jsx-text.fixture.tsx +1 -1
  17. package/fixtures/no-known-value-widening.fixture.ts +1 -1
  18. package/fixtures/no-module-mocking.fixture.ts +1 -1
  19. package/fixtures/no-object-parameters.fixture.ts +1 -1
  20. package/fixtures/no-reduce-accumulator-copy.fixture.ts +1 -1
  21. package/fixtures/no-reflect-apply.fixture.ts +1 -1
  22. package/fixtures/no-reflect-get.fixture.ts +1 -1
  23. package/fixtures/no-runtime-typeof.fixture.ts +1 -1
  24. package/fixtures/no-shape-in-symbol-names.fixture.ts +1 -1
  25. package/fixtures/no-unknown-parameters.fixture.ts +1 -1
  26. package/fixtures/no-unknown-returns.fixture.ts +1 -1
  27. package/fixtures/no-unknown-type-aliases.fixture.ts +1 -1
  28. package/fixtures/no-unsafe-dictionary-type.fixture.ts +1 -1
  29. package/fixtures/no-use-effect.fixture.tsx +1 -1
  30. package/fixtures/no-widen-then-assert.fixture.ts +1 -1
  31. package/fixtures/require-safety-comment-for-type-assertion.fixture.ts +1 -1
  32. package/fixtures/static-public-env.fixture.ts +1 -1
  33. package/package.json +28 -20
  34. package/dist/config.mjs.map +0 -1
  35. package/docs/rules/README.md +0 -43
  36. package/docs/rules/no-array-filter-map.md +0 -60
  37. package/docs/rules/no-chained-type-assertions.md +0 -52
  38. package/docs/rules/no-conditional-empty-object-spread.md +0 -50
  39. package/docs/rules/no-hardcoded-jsx-text.md +0 -52
  40. package/docs/rules/no-known-value-widening.md +0 -62
  41. package/docs/rules/no-module-mocking.md +0 -53
  42. package/docs/rules/no-object-parameters.md +0 -55
  43. package/docs/rules/no-reduce-accumulator-copy.md +0 -57
  44. package/docs/rules/no-reflect-apply.md +0 -56
  45. package/docs/rules/no-reflect-get.md +0 -51
  46. package/docs/rules/no-runtime-typeof.md +0 -61
  47. package/docs/rules/no-shape-in-symbol-names.md +0 -53
  48. package/docs/rules/no-unknown-parameters.md +0 -55
  49. package/docs/rules/no-unknown-returns.md +0 -61
  50. package/docs/rules/no-unknown-type-aliases.md +0 -52
  51. package/docs/rules/no-unsafe-dictionary-type.md +0 -56
  52. package/docs/rules/no-use-effect.md +0 -55
  53. package/docs/rules/no-widen-then-assert.md +0 -53
  54. package/docs/rules/require-safety-comment-for-type-assertion.md +0 -59
  55. package/docs/rules/static-public-env.md +0 -55
package/dist/config.mjs CHANGED
@@ -1,218 +1,2 @@
1
- //#region src/config.ts
2
- /**
3
- * The plugin entry. The alias is `rdj`, which is also the prefix in every
4
- * diagnostic and every disable comment, so a consumer that renames it renames
5
- * its own rule keys too.
6
- */
7
- const plugin = {
8
- name: "rdj",
9
- specifier: "@rdjperron/lint"
10
- };
11
- /** Every rule the plugin registers, prefixed. `fixtures()` and the verify bin read this list. */
12
- const RULE_IDS = [
13
- "rdj/no-array-filter-map",
14
- "rdj/no-chained-type-assertions",
15
- "rdj/no-conditional-empty-object-spread",
16
- "rdj/no-hardcoded-jsx-text",
17
- "rdj/no-known-value-widening",
18
- "rdj/no-module-mocking",
19
- "rdj/no-object-parameters",
20
- "rdj/no-reduce-accumulator-copy",
21
- "rdj/no-reflect-apply",
22
- "rdj/no-reflect-get",
23
- "rdj/no-runtime-typeof",
24
- "rdj/no-shape-in-symbol-names",
25
- "rdj/no-unknown-parameters",
26
- "rdj/no-unknown-returns",
27
- "rdj/no-unknown-type-aliases",
28
- "rdj/no-unsafe-dictionary-type",
29
- "rdj/no-use-effect",
30
- "rdj/no-widen-then-assert",
31
- "rdj/require-safety-comment-for-type-assertion",
32
- "rdj/static-public-env"
33
- ];
34
- /**
35
- * The base config, for `lint: { extends: [recommended] }`.
36
- *
37
- * Measured through `extends` on vite-plus 0.3.1 / oxlint 1.81.0 (2026-09-17):
38
- * `jsPlugins`, `rules`, `overrides`, `plugins`, `categories` and `options` are
39
- * inherited by the consumer's config, and the consumer's own entries win.
40
- * `env`, `settings` and `ignorePatterns` are not inherited, which is why this
41
- * object carries none of them and the consumer keeps writing its own.
42
- */
43
- const recommended = {
44
- jsPlugins: [plugin],
45
- rules: {
46
- "rdj/no-array-filter-map": "error",
47
- "rdj/no-chained-type-assertions": "error",
48
- "rdj/no-conditional-empty-object-spread": "error",
49
- "rdj/no-known-value-widening": "error",
50
- "rdj/no-module-mocking": "error",
51
- "rdj/no-object-parameters": "error",
52
- "rdj/no-reduce-accumulator-copy": "error",
53
- "rdj/no-reflect-apply": "error",
54
- "rdj/no-reflect-get": "error",
55
- "rdj/no-runtime-typeof": ["error", { allowInTypeGuards: true }],
56
- "rdj/no-shape-in-symbol-names": "error",
57
- "rdj/no-unknown-parameters": "error",
58
- "rdj/no-unknown-returns": "error",
59
- "rdj/no-unknown-type-aliases": "error",
60
- "rdj/no-unsafe-dictionary-type": "error",
61
- "rdj/no-widen-then-assert": "error",
62
- "rdj/require-safety-comment-for-type-assertion": "error",
63
- "rdj/no-use-effect": "error",
64
- "typescript/no-floating-promises": "error",
65
- "typescript/no-misused-promises": "error",
66
- "typescript/await-thenable": "error",
67
- "typescript/no-unnecessary-type-assertion": "error",
68
- "typescript/no-misused-spread": "error",
69
- "typescript/require-array-sort-compare": "error",
70
- "typescript/no-explicit-any": "warn",
71
- "prefer-const": ["error", { destructuring: "all" }],
72
- "no-unused-vars": ["error", {
73
- argsIgnorePattern: "^_",
74
- varsIgnorePattern: "^_"
75
- }],
76
- "no-underscore-dangle": "off",
77
- eqeqeq: [
78
- "error",
79
- "always",
80
- { null: "ignore" }
81
- ],
82
- "typescript/no-unsafe-type-assertion": "off",
83
- "no-await-in-loop": "off",
84
- "unicorn/consistent-function-scoping": "off",
85
- "promise/always-return": "off"
86
- }
87
- };
88
- /**
89
- * The React file set: browser globals, the hooks rules, and the compiler's own
90
- * judgement. A consumer appends its own entries after these, because an override
91
- * that changes a `react/*` rule must repeat `plugins: ["react"]` or oxlint
92
- * silently drops the entry.
93
- */
94
- function react(files, options = {}) {
95
- const { useEffectAllowedIn } = options;
96
- const rules = {
97
- "react/rules-of-hooks": "error",
98
- "react/exhaustive-deps": "warn",
99
- "react/react-in-jsx-scope": "off",
100
- "react/immutability": "error",
101
- "react/refs": "error",
102
- "react/purity": "error",
103
- "react/only-export-components": "error"
104
- };
105
- const overrides = [{
106
- files,
107
- plugins: ["react"],
108
- env: { browser: true },
109
- rules
110
- }];
111
- if (useEffectAllowedIn !== void 0) {
112
- rules["rdj/no-use-effect"] = ["error", { hint: `The one sanctioned wrapper is ${useEffectAllowedIn}.` }];
113
- overrides.push({
114
- files: [useEffectAllowedIn],
115
- rules: { "rdj/no-use-effect": "off" }
116
- });
117
- }
118
- return overrides;
119
- }
120
- /** Files whose JSX text belongs in a catalogue rather than in the source. */
121
- function i18n(files, options = {}) {
122
- const { pattern } = options;
123
- return [{
124
- files,
125
- rules: { "rdj/no-hardcoded-jsx-text": pattern === void 0 ? "error" : ["error", { pattern }] }
126
- }];
127
- }
128
- /** Files Expo's bundler inlines `process.env` into, where only `process.env.NAME` survives. */
129
- function expo(files) {
130
- return [{
131
- files,
132
- rules: { "rdj/static-public-env": "error" }
133
- }];
134
- }
135
- /** Test files: four rules whose premise a test does not hold. */
136
- function tests(files) {
137
- return [{
138
- files,
139
- rules: {
140
- "typescript/no-explicit-any": "off",
141
- "typescript/no-non-null-assertion": "off",
142
- "rdj/no-runtime-typeof": "off",
143
- "rdj/require-safety-comment-for-type-assertion": "off"
144
- }
145
- }];
146
- }
147
- /** Every rule of the plugin at `error`, so none of them can be missing from the list. */
148
- const FIXTURE_RULES = {
149
- "rdj/no-array-filter-map": "error",
150
- "rdj/no-chained-type-assertions": "error",
151
- "rdj/no-conditional-empty-object-spread": "error",
152
- "rdj/no-hardcoded-jsx-text": "error",
153
- "rdj/no-known-value-widening": "error",
154
- "rdj/no-module-mocking": "error",
155
- "rdj/no-object-parameters": "error",
156
- "rdj/no-reduce-accumulator-copy": "error",
157
- "rdj/no-reflect-apply": "error",
158
- "rdj/no-reflect-get": "error",
159
- "rdj/no-runtime-typeof": "error",
160
- "rdj/no-shape-in-symbol-names": "error",
161
- "rdj/no-unknown-parameters": "error",
162
- "rdj/no-unknown-returns": "error",
163
- "rdj/no-unknown-type-aliases": "error",
164
- "rdj/no-unsafe-dictionary-type": "error",
165
- "rdj/no-use-effect": "error",
166
- "rdj/no-widen-then-assert": "error",
167
- "rdj/require-safety-comment-for-type-assertion": "error",
168
- "rdj/static-public-env": "error"
169
- };
170
- /**
171
- * The view the verify command lints the shipped fixtures through. Without this
172
- * override every rule the consumer leaves off reports nothing there, and "no
173
- * findings" is exactly what a dead rule looks like.
174
- */
175
- function fixtures(view = "scripts/.lint-fixtures/**") {
176
- return [{
177
- files: [view],
178
- rules: { ...FIXTURE_RULES }
179
- }];
180
- }
181
- /** The formatting both consumers settled on. Spread it and add what is yours. */
182
- const fmt = {
183
- printWidth: 100,
184
- singleQuote: false,
185
- semi: true,
186
- tabWidth: 2,
187
- useTabs: false
188
- };
189
- /**
190
- * Paths no linter and no formatter should walk: build output, dependencies, the
191
- * directories AI coding agents scribble in, and the Vitest JSON report. Spread
192
- * it into `ignorePatterns` and append the repo's own.
193
- */
194
- const ignore = [
195
- "node_modules/**",
196
- "**/node_modules/**",
197
- "dist/**",
198
- "**/dist/**",
199
- ...[
200
- ".agents",
201
- ".claude",
202
- ".codex",
203
- ".continue",
204
- ".cursor",
205
- ".gemini",
206
- ".opencode",
207
- ".pi",
208
- ".roo",
209
- ".windsurf"
210
- ].flatMap((dir) => [`${dir}/**`, `**/${dir}/**`]),
211
- "**/test-results.json"
212
- ];
213
- /** The pre-commit entry: `vp check --fix` is defined for JS and TS sources, nothing else. */
214
- const staged = { "*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}": "vp check --fix" };
215
- //#endregion
216
- export { RULE_IDS, expo, fixtures, fmt, i18n, ignore, plugin, react, recommended, staged, tests };
217
-
218
- //# sourceMappingURL=config.mjs.map
1
+ import { a as i18n, c as react, d as staged, f as tests, i as fmt, l as recommended, n as decoders, o as ignore, r as expo, s as plugin, t as RULE_IDS, u as restrictedImports } from "./config-CD2iCM6L.mjs";
2
+ export { RULE_IDS, decoders, expo, fmt, i18n, ignore, plugin, react, recommended, restrictedImports, staged, tests };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region src/index.d.ts
2
- declare const rdjPlugin: import("@oxlint/plugins").Plugin;
2
+ declare const rdjperronPlugin: import("@oxlint/plugins").Plugin;
3
3
  //#endregion
4
- export { rdjPlugin as default };
4
+ export { rdjperronPlugin as default };
5
5
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -454,7 +454,8 @@ function unsafeDirectValue(type, environment, substitutions, resolvingAliases) {
454
454
  if (unwrapped.type === "TSIntersectionType") {
455
455
  const unsafeMembers = unwrapped.types.map((member) => unsafeDirectValue(member, environment, substitutions, resolvingAliases));
456
456
  if (unsafeMembers.includes("any")) return "any";
457
- return unsafeMembers.length > 0 && unsafeMembers.every((member) => member !== null) ? unsafeMembers[0] : null;
457
+ const first = unsafeMembers[0];
458
+ return first !== void 0 && unsafeMembers.every((member) => member !== null) ? first : null;
458
459
  }
459
460
  if (unwrapped.type !== "TSTypeReference") return null;
460
461
  const name = typeReferenceName$2(unwrapped);
@@ -1103,10 +1104,16 @@ const noRuntimeTypeofRule = defineRule({
1103
1104
  meta: {
1104
1105
  type: "problem",
1105
1106
  docs: { description: "Disallow runtime typeof checks; external values must be decoded into meaningful types at their I/O boundary." },
1106
- messages: { runtimeTypeof: "A `typeof` check narrows a representation without establishing its contract. Parse input at its I/O boundary, then branch on the domain value." },
1107
+ messages: {
1108
+ runtimeTypeof: "A `typeof` check narrows a representation without establishing its contract. Parse input at its I/O boundary, then branch on the domain value.",
1109
+ runtimeTypeofWithHint: "A `typeof` check narrows a representation without establishing its contract. Parse input at its I/O boundary, then branch on the domain value. {{hint}}"
1110
+ },
1107
1111
  schema: [{
1108
1112
  type: "object",
1109
- properties: { allowInTypeGuards: { type: "boolean" } },
1113
+ properties: {
1114
+ allowInTypeGuards: { type: "boolean" },
1115
+ hint: { type: "string" }
1116
+ },
1110
1117
  additionalProperties: false
1111
1118
  }],
1112
1119
  defaultOptions: [{ allowInTypeGuards: false }]
@@ -1114,19 +1121,29 @@ const noRuntimeTypeofRule = defineRule({
1114
1121
  createOnce(context) {
1115
1122
  return { UnaryExpression(node) {
1116
1123
  const option = context.options?.[0];
1117
- const allowInTypeGuards = typeof option === "object" && option !== null && !Array.isArray(option) && option.allowInTypeGuards === true;
1118
- if (node.operator === "typeof" && !isExistenceProbe(node) && (!allowInTypeGuards || !isInsideTypeGuard(node))) context.report({
1119
- node,
1120
- messageId: "runtimeTypeof"
1121
- });
1124
+ const configured = typeof option === "object" && option !== null && !Array.isArray(option);
1125
+ const allowInTypeGuards = configured && option.allowInTypeGuards === true;
1126
+ const hint = configured && typeof option.hint === "string" ? option.hint : void 0;
1127
+ if (node.operator === "typeof" && !isExistenceProbe(node) && (!allowInTypeGuards || !isInsideTypeGuard(node))) {
1128
+ if (hint === void 0) context.report({
1129
+ node,
1130
+ messageId: "runtimeTypeof"
1131
+ });
1132
+ else context.report({
1133
+ node,
1134
+ messageId: "runtimeTypeofWithHint",
1135
+ data: { hint }
1136
+ });
1137
+ }
1122
1138
  } };
1123
1139
  }
1124
1140
  });
1125
1141
  //#endregion
1126
1142
  //#region src/anti-slop/rules/no-shape-in-symbol-names.ts
1127
1143
  const FORBIDDEN_SYMBOL_NAME = "shape";
1144
+ const IDENTIFIER_WORD = /[A-Z]+(?![a-z])|[A-Z]?[a-z]+|\d+/gu;
1128
1145
  function containsForbiddenSymbolName(name) {
1129
- return name.toLowerCase().includes(FORBIDDEN_SYMBOL_NAME);
1146
+ return (name.match(IDENTIFIER_WORD) ?? []).some((word) => word.toLowerCase().startsWith(FORBIDDEN_SYMBOL_NAME));
1130
1147
  }
1131
1148
  /** Return whether an identifier names a statically accessed member owned by another value. */
1132
1149
  function isBorrowedMemberName(node) {
@@ -1134,11 +1151,11 @@ function isBorrowedMemberName(node) {
1134
1151
  if (parent === null || parent.type !== "MemberExpression") return false;
1135
1152
  return parent.property === node && parent.computed === false;
1136
1153
  }
1137
- /** Ban the case-insensitive substring "shape" in every JavaScript and TypeScript symbol name. */
1154
+ /** Ban the word "shape" in every JavaScript and TypeScript symbol name. */
1138
1155
  const noForbiddenTermInSymbolNamesRule = defineRule({
1139
1156
  meta: {
1140
1157
  type: "problem",
1141
- docs: { description: "Disallow the case-insensitive substring \"shape\" in JavaScript, TypeScript, private, and JSX symbol names." },
1158
+ docs: { description: "Disallow the word \"shape\" in JavaScript, TypeScript, private, and JSX symbol names." },
1142
1159
  messages: { forbiddenSymbolName: "Rename symbol \"{{name}}\" for its domain role; \"shape\" describes structure rather than ownership." }
1143
1160
  },
1144
1161
  createOnce(context) {
@@ -1168,21 +1185,39 @@ const noUnknownParametersRule = defineRule({
1168
1185
  meta: {
1169
1186
  type: "problem",
1170
1187
  docs: { description: "Disallow explicitly unknown function parameters except `cause` and type-predicate subjects; decode unknown input at its I/O boundary instead." },
1171
- messages: { unknownParameter: "Parameter `{{parameter}}` leaves input unparsed. Accept a named domain type; run the expected schema or parser at the I/O boundary before calling this function." }
1188
+ messages: {
1189
+ unknownParameter: "Parameter `{{parameter}}` leaves input unparsed. Accept a named domain type; run the expected schema or parser at the I/O boundary before calling this function.",
1190
+ unknownParameterWithHint: "Parameter `{{parameter}}` leaves input unparsed. Accept a named domain type; run the expected schema or parser at the I/O boundary before calling this function. {{hint}}"
1191
+ },
1192
+ schema: [{
1193
+ type: "object",
1194
+ properties: { hint: { type: "string" } },
1195
+ additionalProperties: false
1196
+ }]
1172
1197
  },
1173
1198
  createOnce(context) {
1174
1199
  const checkParameters = (node) => {
1200
+ const option = context.options?.[0];
1201
+ const hint = typeof option === "object" && option !== null && !Array.isArray(option) && typeof option.hint === "string" ? option.hint : void 0;
1175
1202
  for (const parameter of node.params) {
1176
1203
  const annotation = functionParameterTypeAnnotation(parameter);
1177
1204
  if (annotation === null || annotation === void 0) continue;
1178
1205
  if (!containsUnknownType(annotation.typeAnnotation)) continue;
1179
1206
  const name = functionParameterBindingName(parameter, context.sourceCode);
1180
1207
  if (name === "cause" || isTypePredicateSubject(node, name)) continue;
1181
- context.report({
1208
+ if (hint === void 0) context.report({
1182
1209
  node: annotation.typeAnnotation,
1183
1210
  messageId: "unknownParameter",
1184
1211
  data: { parameter: name }
1185
1212
  });
1213
+ else context.report({
1214
+ node: annotation.typeAnnotation,
1215
+ messageId: "unknownParameterWithHint",
1216
+ data: {
1217
+ parameter: name,
1218
+ hint
1219
+ }
1220
+ });
1186
1221
  }
1187
1222
  };
1188
1223
  return {
@@ -1720,12 +1755,18 @@ const noHardcodedJsxTextRule = defineRule({
1720
1755
  //#endregion
1721
1756
  //#region src/rules/no-use-effect.ts
1722
1757
  const effectHooks = /* @__PURE__ */ new Set(["useEffect", "useLayoutEffect"]);
1723
- /** The effect hook a call reaches, whether called bare (`useEffect(...)`) or through a member (`React.useEffect(...)`). */
1758
+ /** The name a member expression reads, written `.useEffect` or `["useEffect"]`; null when the key is not a literal name. */
1759
+ function memberPropertyName(node) {
1760
+ const { property } = node;
1761
+ if (!node.computed) return property.type === "Identifier" ? property.name : null;
1762
+ return property.type === "Literal" && typeof property.value === "string" ? property.value : null;
1763
+ }
1764
+ /** The effect hook a call reaches, called bare (`useEffect(...)`) or through a member (`React.useEffect(...)`, `React["useEffect"](...)`). */
1724
1765
  function calledEffectHook(callee) {
1725
1766
  if (callee.type === "Identifier") return effectHooks.has(callee.name) ? callee.name : null;
1726
- if (callee.type !== "MemberExpression" || callee.computed) return null;
1727
- const { property } = callee;
1728
- return property.type === "Identifier" && effectHooks.has(property.name) ? property.name : null;
1767
+ if (callee.type !== "MemberExpression") return null;
1768
+ const name = memberPropertyName(callee);
1769
+ return name !== null && effectHooks.has(name) ? name : null;
1729
1770
  }
1730
1771
  function importedName(specifier) {
1731
1772
  return specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
@@ -1781,17 +1822,43 @@ const noUseEffectRule = defineRule({
1781
1822
  });
1782
1823
  //#endregion
1783
1824
  //#region src/rules/static-public-env.ts
1825
+ /** The name a member expression reads, the way Babel's `toComputedKey` does: `.NAME` and `["NAME"]` only. */
1826
+ function literalKeyName(node) {
1827
+ const { property } = node;
1828
+ if (!node.computed) return property.type === "Identifier" ? property.name : null;
1829
+ return property.type === "Literal" && typeof property.value === "string" ? property.value : null;
1830
+ }
1831
+ /** Whether a member expression is `process.env` or `process["env"]`, the object the inliner matches. */
1784
1832
  function isProcessEnv(node) {
1785
- return node.type === "MemberExpression" && !node.computed && node.object.type === "Identifier" && node.object.name === "process" && node.property.type === "Identifier" && node.property.name === "env";
1833
+ return node.object.type === "Identifier" && node.object.name === "process" && literalKeyName(node) === "env";
1834
+ }
1835
+ /** Whether a pattern takes `process.env` apart, in a declaration or in an assignment. */
1836
+ function isDestructured(node) {
1837
+ const { parent } = node;
1838
+ let target = null;
1839
+ if (parent.type === "VariableDeclarator" && parent.init === node) target = parent.id;
1840
+ if (parent.type === "AssignmentExpression" && parent.right === node) target = parent.left;
1841
+ return target !== null && (target.type === "ObjectPattern" || target.type === "ArrayPattern");
1842
+ }
1843
+ /** What is wrong with a `process.env.KEY` read, or null when Expo replaces that exact shape. */
1844
+ function readProblem(env, read) {
1845
+ const { parent } = read;
1846
+ if (parent.type === "AssignmentExpression" && parent.left === read) return "assigned";
1847
+ if (env.optional || read.optional) return "optionalChain";
1848
+ if (literalKeyName(read) !== null) return null;
1849
+ return read.property.type === "Literal" || read.property.type === "TemplateLiteral" ? "literalKey" : "computed";
1786
1850
  }
1787
1851
  /** Reject every use of `process.env` that Expo's textual inlining cannot see. */
1788
1852
  const staticPublicEnvRule = defineRule({
1789
1853
  meta: {
1790
1854
  type: "problem",
1791
- docs: { description: "Allow only static `process.env.NAME` reads; Expo replaces each one textually at bundle time and everything else is undefined on the device." },
1855
+ docs: { description: "Allow only the `process.env` reads Expo replaces at bundle time; everything else is undefined on the device." },
1792
1856
  messages: {
1793
1857
  destructured: "Expo replaces each `process.env.EXPO_PUBLIC_*` read textually at bundle time, so there is no `process.env` object to destructure on the device. Read each variable as `process.env.NAME`.",
1794
- computed: "Expo replaces each `process.env.EXPO_PUBLIC_*` read textually at bundle time, so a computed key is undefined on the device. Read the variable as `process.env.NAME` with the name spelled out.",
1858
+ computed: "Expo replaces each `process.env.EXPO_PUBLIC_*` read textually at bundle time, so a key it has to evaluate is undefined on the device. Read the variable as `process.env.NAME` with the name spelled out.",
1859
+ literalKey: "Expo reads a computed key only when it is a string literal, so a template or a number leaves the read in the bundle and it is undefined on the device. Read the variable as `process.env.NAME`.",
1860
+ optionalChain: "Expo's inliner never sees an optional chain, so `process.env?.NAME` survives into the bundle and is undefined on the device. Read the variable as `process.env.NAME`.",
1861
+ assigned: "Expo replaces reads, not writes: an assignment to `process.env` stays in the bundle and no inlined read will ever see it. Set the variable in the environment the bundler reads.",
1795
1862
  bareValue: "Expo replaces each `process.env.EXPO_PUBLIC_*` read textually at bundle time, so `process.env` passed around as a value is empty on the device. Read each variable as `process.env.NAME`."
1796
1863
  }
1797
1864
  },
@@ -1800,13 +1867,14 @@ const staticPublicEnvRule = defineRule({
1800
1867
  if (!isProcessEnv(node)) return;
1801
1868
  const { parent } = node;
1802
1869
  if (parent.type === "MemberExpression" && parent.object === node) {
1803
- if (parent.computed) context.report({
1870
+ const problem = readProblem(node, parent);
1871
+ if (problem !== null) context.report({
1804
1872
  node: parent,
1805
- messageId: "computed"
1873
+ messageId: problem
1806
1874
  });
1807
1875
  return;
1808
1876
  }
1809
- if (parent.type === "VariableDeclarator" && parent.id.type === "ObjectPattern") {
1877
+ if (isDestructured(node)) {
1810
1878
  context.report({
1811
1879
  node: parent,
1812
1880
  messageId: "destructured"
@@ -1822,8 +1890,8 @@ const staticPublicEnvRule = defineRule({
1822
1890
  });
1823
1891
  //#endregion
1824
1892
  //#region src/index.ts
1825
- const rdjPlugin = eslintCompatPlugin({
1826
- meta: { name: "rdj" },
1893
+ const rdjperronPlugin = eslintCompatPlugin({
1894
+ meta: { name: "rdjperron" },
1827
1895
  rules: {
1828
1896
  "no-array-filter-map": noArrayFilterMapRule,
1829
1897
  "no-chained-type-assertions": noChainedTypeAssertionsRule,
@@ -1848,6 +1916,6 @@ const rdjPlugin = eslintCompatPlugin({
1848
1916
  }
1849
1917
  });
1850
1918
  //#endregion
1851
- export { rdjPlugin as default };
1919
+ export { rdjperronPlugin as default };
1852
1920
 
1853
1921
  //# sourceMappingURL=index.mjs.map