eslint-plugin-intlayer 9.3.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/README.md +310 -0
- package/dist/cjs/index.cjs +105 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/rules/enforceAdapterImport.cjs +81 -0
- package/dist/cjs/rules/enforceAdapterImport.cjs.map +1 -0
- package/dist/cjs/rules/noDynamicFieldAccess.cjs +130 -0
- package/dist/cjs/rules/noDynamicFieldAccess.cjs.map +1 -0
- package/dist/cjs/rules/noRawText.cjs +176 -0
- package/dist/cjs/rules/noRawText.cjs.map +1 -0
- package/dist/cjs/rules/staticDictionaryKey.cjs +89 -0
- package/dist/cjs/rules/staticDictionaryKey.cjs.map +1 -0
- package/dist/cjs/utils/ast.cjs +151 -0
- package/dist/cjs/utils/ast.cjs.map +1 -0
- package/dist/cjs/utils/callerMatcher.cjs +130 -0
- package/dist/cjs/utils/callerMatcher.cjs.map +1 -0
- package/dist/cjs/utils/createRule.cjs +26 -0
- package/dist/cjs/utils/createRule.cjs.map +1 -0
- package/dist/cjs/utils/templates.cjs +148 -0
- package/dist/cjs/utils/templates.cjs.map +1 -0
- package/dist/esm/index.mjs +101 -0
- package/dist/esm/index.mjs.map +1 -0
- package/dist/esm/rules/enforceAdapterImport.mjs +80 -0
- package/dist/esm/rules/enforceAdapterImport.mjs.map +1 -0
- package/dist/esm/rules/noDynamicFieldAccess.mjs +129 -0
- package/dist/esm/rules/noDynamicFieldAccess.mjs.map +1 -0
- package/dist/esm/rules/noRawText.mjs +175 -0
- package/dist/esm/rules/noRawText.mjs.map +1 -0
- package/dist/esm/rules/staticDictionaryKey.mjs +88 -0
- package/dist/esm/rules/staticDictionaryKey.mjs.map +1 -0
- package/dist/esm/utils/ast.mjs +143 -0
- package/dist/esm/utils/ast.mjs.map +1 -0
- package/dist/esm/utils/callerMatcher.mjs +126 -0
- package/dist/esm/utils/callerMatcher.mjs.map +1 -0
- package/dist/esm/utils/createRule.mjs +25 -0
- package/dist/esm/utils/createRule.mjs.map +1 -0
- package/dist/esm/utils/templates.mjs +145 -0
- package/dist/esm/utils/templates.mjs.map +1 -0
- package/dist/types/index.d.ts +67 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/rules/enforceAdapterImport.d.ts +6 -0
- package/dist/types/rules/enforceAdapterImport.d.ts.map +1 -0
- package/dist/types/rules/noDynamicFieldAccess.d.ts +7 -0
- package/dist/types/rules/noDynamicFieldAccess.d.ts.map +1 -0
- package/dist/types/rules/noRawText.d.ts +25 -0
- package/dist/types/rules/noRawText.d.ts.map +1 -0
- package/dist/types/rules/staticDictionaryKey.d.ts +7 -0
- package/dist/types/rules/staticDictionaryKey.d.ts.map +1 -0
- package/dist/types/utils/ast.d.ts +105 -0
- package/dist/types/utils/ast.d.ts.map +1 -0
- package/dist/types/utils/callerMatcher.d.ts +58 -0
- package/dist/types/utils/callerMatcher.d.ts.map +1 -0
- package/dist/types/utils/createRule.d.ts +23 -0
- package/dist/types/utils/createRule.d.ts.map +1 -0
- package/dist/types/utils/templates.d.ts +71 -0
- package/dist/types/utils/templates.d.ts.map +1 -0
- package/package.json +105 -0
package/README.md
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
<!-- markdownlint-disable MD024 -->
|
|
2
|
+
|
|
3
|
+
# eslint-plugin-intlayer
|
|
4
|
+
|
|
5
|
+
ESLint rules for [Intlayer](https://intlayer.org) projects.
|
|
6
|
+
|
|
7
|
+
The plugin is deliberately small. Intlayer already reports most i18n mistakes
|
|
8
|
+
through other layers, and a rule that duplicates one of them only produces the
|
|
9
|
+
same squiggle three times:
|
|
10
|
+
|
|
11
|
+
| Mistake | Already reported by |
|
|
12
|
+
| --------------------------------- | ---------------------------------------------------------------- |
|
|
13
|
+
| Unknown dictionary key | TypeScript (`__DictionaryRegistry`) **and** `@intlayer/lsp` |
|
|
14
|
+
| Unknown field path | TypeScript (`__DictionaryRegistry`) |
|
|
15
|
+
| Missing required locale | TypeScript (`StrictModeLocaleMap`) **and** `intlayer test` |
|
|
16
|
+
| Unused dictionary | `intlayer test`, and the VS Code extension's inline decoration |
|
|
17
|
+
|
|
18
|
+
What is left are the two things neither the type system nor the language server
|
|
19
|
+
can see, and that is exactly what this plugin covers:
|
|
20
|
+
|
|
21
|
+
1. **Coverage** — user-facing copy that never made it into a dictionary.
|
|
22
|
+
Types cannot know a string is meant for a human.
|
|
23
|
+
2. **The compiler contract** — call shapes that type-check and run, but that
|
|
24
|
+
the Babel/SWC passes cannot analyse. These silently lose the optimize pass,
|
|
25
|
+
or read a field the purge pass has stripped.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install --save-dev eslint-plugin-intlayer
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Requires ESLint 9 or later (flat config).
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
// eslint.config.js
|
|
39
|
+
import intlayer from 'eslint-plugin-intlayer';
|
|
40
|
+
|
|
41
|
+
export default [intlayer.configs.recommended];
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or configure rules individually:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import intlayer from 'eslint-plugin-intlayer';
|
|
48
|
+
|
|
49
|
+
export default [
|
|
50
|
+
{
|
|
51
|
+
plugins: { intlayer },
|
|
52
|
+
rules: {
|
|
53
|
+
'intlayer/no-raw-text': ['warn', { includeStringLiterals: false }],
|
|
54
|
+
'intlayer/static-dictionary-key': 'error',
|
|
55
|
+
'intlayer/no-dynamic-field-access': 'error',
|
|
56
|
+
'intlayer/enforce-adapter-import': 'warn',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Configs
|
|
63
|
+
|
|
64
|
+
| Config | `no-raw-text` | `static-dictionary-key` | `no-dynamic-field-access` | `enforce-adapter-import` |
|
|
65
|
+
| --------------- | ------------- | ----------------------- | ------------------------- | ------------------------ |
|
|
66
|
+
| `recommended` | warn | error | error | off |
|
|
67
|
+
| `strict` | error (+ non-JSX literals) | error | error | error |
|
|
68
|
+
| `contract-only` | off | error | error | off |
|
|
69
|
+
|
|
70
|
+
`recommended` keeps `no-raw-text` at `warn` on purpose: pointing it at an
|
|
71
|
+
existing codebase surfaces every untranslated string at once, which should not
|
|
72
|
+
block a build on day one.
|
|
73
|
+
|
|
74
|
+
Use `contract-only` when translation coverage is already tracked by
|
|
75
|
+
`intlayer test` in CI and you only want the rules that protect the build output.
|
|
76
|
+
|
|
77
|
+
### Frameworks
|
|
78
|
+
|
|
79
|
+
Every rule works across all of Intlayer's integrations. The rules are
|
|
80
|
+
parser-agnostic; what differs is the parser ESLint needs to read each file type,
|
|
81
|
+
which you configure once:
|
|
82
|
+
|
|
83
|
+
| Framework | Files | Parser | Text coverage | Contract rules |
|
|
84
|
+
| ----------------------------- | ------------------------ | --------------------------------- | ------------- | -------------- |
|
|
85
|
+
| React, Preact, Solid, Lit | `.jsx` `.tsx` | `typescript-eslint` | ✅ | ✅ |
|
|
86
|
+
| Next.js | `.jsx` `.tsx` | `typescript-eslint` | ✅ | ✅ |
|
|
87
|
+
| Vue, Nuxt | `.vue` | `vue-eslint-parser` | ✅ template | ✅ `<script>` |
|
|
88
|
+
| Svelte, SvelteKit | `.svelte` | `svelte-eslint-parser` | ✅ markup | ✅ `<script>` |
|
|
89
|
+
| Angular | `.ts` | `typescript-eslint` | ✅ | ✅ |
|
|
90
|
+
| Angular templates | `.component.html` | `@angular-eslint/template-parser` | ✅ | n/a (no JS) |
|
|
91
|
+
| Astro | `.astro` | `astro-eslint-parser` | ✅ | ✅ frontmatter |
|
|
92
|
+
|
|
93
|
+
`no-raw-text` understands each ecosystem's markup natively — Vue's `VText` /
|
|
94
|
+
`VAttribute` (reached through `defineTemplateBodyVisitor`), Svelte's
|
|
95
|
+
`SvelteText` / `SvelteAttribute`, Angular's `Text` / `TextAttribute`, and JSX's
|
|
96
|
+
`JSXText` / `JSXAttribute` — so a `<code>` block is skipped and a `placeholder`
|
|
97
|
+
is reported the same way in all of them. Vue directives (`:title="expr"`) and
|
|
98
|
+
Svelte interpolations are expressions, not copy, and are never reported.
|
|
99
|
+
|
|
100
|
+
The compiler-contract rules read the `<script>` block, which every one of these
|
|
101
|
+
parsers exposes as an ordinary ESTree program.
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
// eslint.config.js
|
|
105
|
+
import intlayer from 'eslint-plugin-intlayer';
|
|
106
|
+
import tseslint from 'typescript-eslint';
|
|
107
|
+
import vueParser from 'vue-eslint-parser';
|
|
108
|
+
import svelteParser from 'svelte-eslint-parser';
|
|
109
|
+
import angularTemplateParser from '@angular-eslint/template-parser';
|
|
110
|
+
|
|
111
|
+
export default [
|
|
112
|
+
intlayer.configs.recommended,
|
|
113
|
+
|
|
114
|
+
// React / Angular components / plain TS
|
|
115
|
+
{
|
|
116
|
+
files: ['**/*.{ts,tsx,jsx}'],
|
|
117
|
+
languageOptions: { parser: tseslint.parser },
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
// Vue — the TS parser handles <script lang="ts">
|
|
121
|
+
{
|
|
122
|
+
files: ['**/*.vue'],
|
|
123
|
+
languageOptions: {
|
|
124
|
+
parser: vueParser,
|
|
125
|
+
parserOptions: { parser: tseslint.parser },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
// Svelte
|
|
130
|
+
{
|
|
131
|
+
files: ['**/*.svelte'],
|
|
132
|
+
languageOptions: {
|
|
133
|
+
parser: svelteParser,
|
|
134
|
+
parserOptions: { parser: tseslint.parser },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
// Angular templates
|
|
139
|
+
{
|
|
140
|
+
files: ['**/*.component.html'],
|
|
141
|
+
languageOptions: { parser: angularTemplateParser },
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Install only the parsers your project needs — they are peer-optional, and the
|
|
147
|
+
plugin never imports them.
|
|
148
|
+
|
|
149
|
+
**Known limitation.** In Vue and Angular templates, an expression such as
|
|
150
|
+
`{{ content[key] }}` is not checked by `no-dynamic-field-access`: the binding is
|
|
151
|
+
declared in the script block and used in a separate template AST, so the two
|
|
152
|
+
cannot be linked without type information. Dynamic reads written in the script
|
|
153
|
+
block are caught normally. `static-dictionary-key` is unaffected, because a
|
|
154
|
+
dictionary key is always passed in the script.
|
|
155
|
+
|
|
156
|
+
### With oxlint
|
|
157
|
+
|
|
158
|
+
Every rule targets the ESLint v9 API and none needs type information, so they
|
|
159
|
+
load in oxlint as-is:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"jsPlugins": ["eslint-plugin-intlayer"],
|
|
164
|
+
"rules": {
|
|
165
|
+
"intlayer/static-dictionary-key": "error",
|
|
166
|
+
"intlayer/no-dynamic-field-access": "error"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Two caveats: oxlint's JS plugin support is still alpha, and it does not support
|
|
172
|
+
custom parsers — so `.vue`, `.svelte`, `.astro` and Angular templates are not
|
|
173
|
+
linted there. Use oxlint for the JS/TS/JSX files and keep ESLint for the rest.
|
|
174
|
+
|
|
175
|
+
## Rules
|
|
176
|
+
|
|
177
|
+
### no-raw-text
|
|
178
|
+
|
|
179
|
+
Reports user-facing strings that are not declared in a dictionary.
|
|
180
|
+
|
|
181
|
+
The predicate is `shouldExtract` from `@intlayer/config/extract` — the very same
|
|
182
|
+
heuristic `intlayer extract` uses — so what this rule reports is exactly what the
|
|
183
|
+
extractor would rewrite. Brand names, CSS class strings, technical identifiers
|
|
184
|
+
and code-shaped strings are filtered out.
|
|
185
|
+
|
|
186
|
+
```jsx
|
|
187
|
+
// ✗
|
|
188
|
+
<h1>Welcome to our documentation</h1>
|
|
189
|
+
<input placeholder="Enter your email address" />
|
|
190
|
+
|
|
191
|
+
// ✓
|
|
192
|
+
const { title } = useIntlayer('home');
|
|
193
|
+
<h1>{title}</h1>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Content declaration files (`*.content.ts`, and every extension in the project's
|
|
197
|
+
`content.fileExtensions`) are skipped — the copy lives there by definition.
|
|
198
|
+
|
|
199
|
+
The fix is usually not manual: run `npx intlayer extract` on the file.
|
|
200
|
+
|
|
201
|
+
**Options**
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
{
|
|
205
|
+
/** JSX attributes whose value is copy. Default: title, placeholder, alt, aria-label, label. */
|
|
206
|
+
attributes?: string[];
|
|
207
|
+
/** Elements whose children are never copy. Default: code, pre, script, style. */
|
|
208
|
+
ignoreElements?: string[];
|
|
209
|
+
/** Regular-expression sources for text to never report. */
|
|
210
|
+
ignorePatterns?: string[];
|
|
211
|
+
/** Also report string literals outside JSX. Default: false. */
|
|
212
|
+
includeStringLiterals?: boolean;
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### static-dictionary-key
|
|
217
|
+
|
|
218
|
+
Requires the dictionary key of a content read to be a static string literal.
|
|
219
|
+
|
|
220
|
+
`babel-plugin-intlayer-optimize` (and its SWC equivalent) rewrites a call only
|
|
221
|
+
when it can read the key off the argument node. A computed key is left
|
|
222
|
+
untouched — no error, no warning, just a build that quietly stops tree-shaking
|
|
223
|
+
and bundles every dictionary.
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
// ✗ — invisible to the compiler
|
|
227
|
+
useIntlayer(dictionaryKey);
|
|
228
|
+
useIntlayer(`home-${suffix}`);
|
|
229
|
+
getTranslations({ namespace: page });
|
|
230
|
+
|
|
231
|
+
// ✗ — a const still is not a literal at the call site
|
|
232
|
+
const key = 'home';
|
|
233
|
+
useIntlayer(key);
|
|
234
|
+
|
|
235
|
+
// ✓
|
|
236
|
+
useIntlayer('home');
|
|
237
|
+
useIntlayer('home' as const);
|
|
238
|
+
getTranslations({ namespace: 'home' });
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The rule covers every caller in the shared registry — `useIntlayer`,
|
|
242
|
+
`getIntlayer`, and each compat adapter (`useTranslation`, `useTranslations`,
|
|
243
|
+
`formatMessage`, `<FormattedMessage id>`, `<Trans i18nKey>`, …) — because a
|
|
244
|
+
dynamic key breaks usage analysis for all of them.
|
|
245
|
+
|
|
246
|
+
Callers that legitimately take no namespace (next-intl's root-scope
|
|
247
|
+
`useTranslations()`) are not reported.
|
|
248
|
+
|
|
249
|
+
### no-dynamic-field-access
|
|
250
|
+
|
|
251
|
+
Requires the field read from a dictionary to be statically known.
|
|
252
|
+
|
|
253
|
+
The purge pass strips fields it cannot see used. A computed access is a field it
|
|
254
|
+
cannot see, so the read may return `undefined` at runtime — a real bug, not just
|
|
255
|
+
a missed optimization.
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
// ✗
|
|
259
|
+
const content = useIntlayer('home');
|
|
260
|
+
content[fieldName];
|
|
261
|
+
|
|
262
|
+
const t = useTranslations('home');
|
|
263
|
+
t(messageKey);
|
|
264
|
+
|
|
265
|
+
// ✓
|
|
266
|
+
content.title;
|
|
267
|
+
content['title'];
|
|
268
|
+
content.items[0];
|
|
269
|
+
t('hero.title');
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Bindings are resolved through ESLint's scope analysis, so two components in the
|
|
273
|
+
same file that bind the same variable name to different dictionaries are tracked
|
|
274
|
+
independently.
|
|
275
|
+
|
|
276
|
+
### enforce-adapter-import
|
|
277
|
+
|
|
278
|
+
Prefers the `@intlayer/*` compat adapter over the original package specifier.
|
|
279
|
+
|
|
280
|
+
The original specifier only resolves to Intlayer when the bundler alias is
|
|
281
|
+
configured; the adapter always does. Autofixable.
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
// ✗
|
|
285
|
+
import { useTranslation } from 'react-i18next';
|
|
286
|
+
import { getTranslations } from 'next-intl/server';
|
|
287
|
+
|
|
288
|
+
// ✓
|
|
289
|
+
import { useTranslation } from '@intlayer/react-i18next';
|
|
290
|
+
import { getTranslations } from '@intlayer/next-intl/server';
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The mapping is derived from `@intlayer/config/callers`, so a library added to
|
|
294
|
+
the shared registry is covered here automatically.
|
|
295
|
+
|
|
296
|
+
## How it stays in sync
|
|
297
|
+
|
|
298
|
+
The plugin reads the same two shared sources as the compiler and the language
|
|
299
|
+
server, rather than re-encoding what an Intlayer call looks like:
|
|
300
|
+
|
|
301
|
+
- `@intlayer/config/callers` — the caller registry consumed by `@intlayer/babel`,
|
|
302
|
+
`@intlayer/swc`, `@intlayer/lsp` and every compat bundler plugin.
|
|
303
|
+
- `@intlayer/config/extract` — the `shouldExtract` heuristic and the extractable
|
|
304
|
+
attribute list used by `intlayer extract`.
|
|
305
|
+
|
|
306
|
+
Adding a compat library to the registry therefore extends the rules for free.
|
|
307
|
+
|
|
308
|
+
## License
|
|
309
|
+
|
|
310
|
+
Apache-2.0
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: 'Module' } });
|
|
2
|
+
const require_rules_enforceAdapterImport = require('./rules/enforceAdapterImport.cjs');
|
|
3
|
+
const require_rules_noDynamicFieldAccess = require('./rules/noDynamicFieldAccess.cjs');
|
|
4
|
+
const require_rules_noRawText = require('./rules/noRawText.cjs');
|
|
5
|
+
const require_rules_staticDictionaryKey = require('./rules/staticDictionaryKey.cjs');
|
|
6
|
+
let _intlayer_config_lint = require("@intlayer/config/lint");
|
|
7
|
+
|
|
8
|
+
//#region src/index.ts
|
|
9
|
+
const PLUGIN_NAME = "intlayer";
|
|
10
|
+
/**
|
|
11
|
+
* Every rule this plugin ships, keyed by the name used in configuration.
|
|
12
|
+
*
|
|
13
|
+
* The rules split into two families:
|
|
14
|
+
* - **Coverage** (`no-raw-text`) — user-facing copy that never reached a
|
|
15
|
+
* dictionary. Nothing else in the toolchain reports it.
|
|
16
|
+
* - **Compiler contract** (`static-dictionary-key`, `no-dynamic-field-access`)
|
|
17
|
+
* — call shapes that type-check and run, but that the Babel/SWC passes cannot
|
|
18
|
+
* analyse, so they silently lose the optimize pass or read a purged field.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately absent: unknown dictionary keys, unknown field paths and missing
|
|
21
|
+
* required locales. The generated `__DictionaryRegistry` module augmentation
|
|
22
|
+
* already makes all three compile errors, and `@intlayer/lsp` reports the first
|
|
23
|
+
* as an editor diagnostic — a third report would only add noise.
|
|
24
|
+
*/
|
|
25
|
+
const rules = {
|
|
26
|
+
"no-raw-text": require_rules_noRawText.noRawText,
|
|
27
|
+
"static-dictionary-key": require_rules_staticDictionaryKey.staticDictionaryKey,
|
|
28
|
+
"no-dynamic-field-access": require_rules_noDynamicFieldAccess.noDynamicFieldAccess,
|
|
29
|
+
"enforce-adapter-import": require_rules_enforceAdapterImport.enforceAdapterImport
|
|
30
|
+
};
|
|
31
|
+
const meta = {
|
|
32
|
+
name: "eslint-plugin-intlayer",
|
|
33
|
+
version: "9.3.1"
|
|
34
|
+
};
|
|
35
|
+
const plugin = {
|
|
36
|
+
meta,
|
|
37
|
+
rules
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Build a flat config enabling the given rules under the `intlayer/` prefix.
|
|
41
|
+
*
|
|
42
|
+
* @param name - Config name, surfaced by `--print-config` and the config inspector.
|
|
43
|
+
* @param ruleSeverities - Severity per rule name.
|
|
44
|
+
*/
|
|
45
|
+
const createConfig = (name, ruleSeverities) => ({
|
|
46
|
+
name: `intlayer/${name}`,
|
|
47
|
+
plugins: { [PLUGIN_NAME]: plugin },
|
|
48
|
+
rules: Object.fromEntries(Object.entries(ruleSeverities).map(([ruleName, severity]) => [`${PLUGIN_NAME}/${ruleName}`, severity]))
|
|
49
|
+
});
|
|
50
|
+
/**
|
|
51
|
+
* Flat configs, for `eslint.config.js`:
|
|
52
|
+
*
|
|
53
|
+
* ```js
|
|
54
|
+
* import intlayer from 'eslint-plugin-intlayer';
|
|
55
|
+
*
|
|
56
|
+
* export default [intlayer.configs.recommended];
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* - `recommended` — the compiler-contract rules as errors, plus `no-raw-text`
|
|
60
|
+
* as a warning so an existing codebase is not blocked on day one.
|
|
61
|
+
* - `strict` — everything as an error, including string literals outside JSX.
|
|
62
|
+
* - `contract-only` — just the rules that protect the build output; use this
|
|
63
|
+
* when translation coverage is tracked by `intlayer test` instead.
|
|
64
|
+
*
|
|
65
|
+
* `enforce-adapter-import` is opt-in: importing from the original package is
|
|
66
|
+
* perfectly valid once the bundler alias is set up, so a project that has done
|
|
67
|
+
* that should not be nagged. Only `strict` turns it on.
|
|
68
|
+
*/
|
|
69
|
+
const configs = {
|
|
70
|
+
recommended: {
|
|
71
|
+
name: `${PLUGIN_NAME}/recommended`,
|
|
72
|
+
plugins: { [PLUGIN_NAME]: plugin },
|
|
73
|
+
rules: (0, _intlayer_config_lint.getRecommendedLintRules)()
|
|
74
|
+
},
|
|
75
|
+
strict: {
|
|
76
|
+
...createConfig("strict", {
|
|
77
|
+
"no-raw-text": "error",
|
|
78
|
+
"static-dictionary-key": "error",
|
|
79
|
+
"no-dynamic-field-access": "error",
|
|
80
|
+
"enforce-adapter-import": "error"
|
|
81
|
+
}),
|
|
82
|
+
rules: {
|
|
83
|
+
"intlayer/no-raw-text": ["error", { includeStringLiterals: true }],
|
|
84
|
+
"intlayer/static-dictionary-key": "error",
|
|
85
|
+
"intlayer/no-dynamic-field-access": "error",
|
|
86
|
+
"intlayer/enforce-adapter-import": "error"
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"contract-only": createConfig("contract-only", {
|
|
90
|
+
"static-dictionary-key": "error",
|
|
91
|
+
"no-dynamic-field-access": "error"
|
|
92
|
+
})
|
|
93
|
+
};
|
|
94
|
+
const intlayerPlugin = {
|
|
95
|
+
meta,
|
|
96
|
+
rules,
|
|
97
|
+
configs
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
//#endregion
|
|
101
|
+
exports.configs = configs;
|
|
102
|
+
exports.default = intlayerPlugin;
|
|
103
|
+
exports.meta = meta;
|
|
104
|
+
exports.rules = rules;
|
|
105
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["noRawText","staticDictionaryKey","noDynamicFieldAccess","enforceAdapterImport","getRecommendedLintRules"],"sources":["../../src/index.ts"],"sourcesContent":["import { getRecommendedLintRules } from '@intlayer/config/lint';\nimport type { ESLint, Linter } from 'eslint';\nimport { enforceAdapterImport } from './rules/enforceAdapterImport';\nimport { noDynamicFieldAccess } from './rules/noDynamicFieldAccess';\nimport { noRawText } from './rules/noRawText';\nimport { staticDictionaryKey } from './rules/staticDictionaryKey';\n\nconst PLUGIN_NAME = 'intlayer';\n\n/**\n * Every rule this plugin ships, keyed by the name used in configuration.\n *\n * The rules split into two families:\n * - **Coverage** (`no-raw-text`) — user-facing copy that never reached a\n * dictionary. Nothing else in the toolchain reports it.\n * - **Compiler contract** (`static-dictionary-key`, `no-dynamic-field-access`)\n * — call shapes that type-check and run, but that the Babel/SWC passes cannot\n * analyse, so they silently lose the optimize pass or read a purged field.\n *\n * Deliberately absent: unknown dictionary keys, unknown field paths and missing\n * required locales. The generated `__DictionaryRegistry` module augmentation\n * already makes all three compile errors, and `@intlayer/lsp` reports the first\n * as an editor diagnostic — a third report would only add noise.\n */\nexport const rules = {\n 'no-raw-text': noRawText,\n 'static-dictionary-key': staticDictionaryKey,\n 'no-dynamic-field-access': noDynamicFieldAccess,\n 'enforce-adapter-import': enforceAdapterImport,\n} satisfies ESLint.Plugin['rules'];\n\n/** Rule names this plugin exposes. */\nexport type IntlayerRuleName = keyof typeof rules;\n\nconst meta: { name: string; version: string } = {\n name: 'eslint-plugin-intlayer',\n version: '9.3.1',\n};\n\nconst plugin: ESLint.Plugin = { meta, rules };\n\n/** The flat configs this plugin ships. */\nexport type IntlayerConfigName = 'recommended' | 'strict' | 'contract-only';\n\n/**\n * Build a flat config enabling the given rules under the `intlayer/` prefix.\n *\n * @param name - Config name, surfaced by `--print-config` and the config inspector.\n * @param ruleSeverities - Severity per rule name.\n */\nconst createConfig = (\n name: string,\n ruleSeverities: Partial<Record<IntlayerRuleName, Linter.RuleSeverity>>\n): Linter.Config => ({\n name: `intlayer/${name}`,\n plugins: { [PLUGIN_NAME]: plugin },\n rules: Object.fromEntries(\n Object.entries(ruleSeverities).map(([ruleName, severity]) => [\n `${PLUGIN_NAME}/${ruleName}`,\n severity,\n ])\n ),\n});\n\n/**\n * Flat configs, for `eslint.config.js`:\n *\n * ```js\n * import intlayer from 'eslint-plugin-intlayer';\n *\n * export default [intlayer.configs.recommended];\n * ```\n *\n * - `recommended` — the compiler-contract rules as errors, plus `no-raw-text`\n * as a warning so an existing codebase is not blocked on day one.\n * - `strict` — everything as an error, including string literals outside JSX.\n * - `contract-only` — just the rules that protect the build output; use this\n * when translation coverage is tracked by `intlayer test` instead.\n *\n * `enforce-adapter-import` is opt-in: importing from the original package is\n * perfectly valid once the bundler alias is set up, so a project that has done\n * that should not be nagged. Only `strict` turns it on.\n */\nexport const configs: Record<IntlayerConfigName, Linter.Config> = {\n // Built from the shared preset in `@intlayer/config/lint`, which `intlayer\n // init` also writes into `.oxlintrc.json` — oxlint resolves a plugin's rules\n // but not its configs, so the two must not drift.\n recommended: {\n name: `${PLUGIN_NAME}/recommended`,\n plugins: { [PLUGIN_NAME]: plugin },\n rules: getRecommendedLintRules() as Linter.Config['rules'],\n },\n strict: {\n ...createConfig('strict', {\n 'no-raw-text': 'error',\n 'static-dictionary-key': 'error',\n 'no-dynamic-field-access': 'error',\n 'enforce-adapter-import': 'error',\n }),\n rules: {\n 'intlayer/no-raw-text': ['error', { includeStringLiterals: true }],\n 'intlayer/static-dictionary-key': 'error',\n 'intlayer/no-dynamic-field-access': 'error',\n 'intlayer/enforce-adapter-import': 'error',\n },\n },\n 'contract-only': createConfig('contract-only', {\n 'static-dictionary-key': 'error',\n 'no-dynamic-field-access': 'error',\n }),\n};\n\nexport type { NoRawTextOptions } from './rules/noRawText';\nexport { meta };\n\n/** The plugin object, as consumed by `eslint.config.js`. */\nexport type IntlayerPlugin = ESLint.Plugin & {\n meta: { name: string; version: string };\n rules: typeof rules;\n configs: Record<IntlayerConfigName, Linter.Config>;\n};\n\nconst intlayerPlugin: IntlayerPlugin = { meta, rules, configs };\n\nexport default intlayerPlugin;\n"],"mappings":";;;;;;;;AAOA,MAAM,cAAc;;;;;;;;;;;;;;;;AAiBpB,MAAa,QAAQ;CACnB,eAAeA;CACf,yBAAyBC;CACzB,2BAA2BC;CAC3B,0BAA0BC;AAC5B;AAKA,MAAM,OAA0C;CAC9C,MAAM;CACN,SAAS;AACX;AAEA,MAAM,SAAwB;CAAE;CAAM;AAAM;;;;;;;AAW5C,MAAM,gBACJ,MACA,oBACmB;CACnB,MAAM,YAAY;CAClB,SAAS,GAAG,cAAc,OAAO;CACjC,OAAO,OAAO,YACZ,OAAO,QAAQ,cAAc,CAAC,CAAC,KAAK,CAAC,UAAU,cAAc,CAC3D,GAAG,YAAY,GAAG,YAClB,QACF,CAAC,CACH;AACF;;;;;;;;;;;;;;;;;;;;AAqBA,MAAa,UAAqD;CAIhE,aAAa;EACX,MAAM,GAAG,YAAY;EACrB,SAAS,GAAG,cAAc,OAAO;EACjC,WAAOC,+CAAwB;CACjC;CACA,QAAQ;EACN,GAAG,aAAa,UAAU;GACxB,eAAe;GACf,yBAAyB;GACzB,2BAA2B;GAC3B,0BAA0B;EAC5B,CAAC;EACD,OAAO;GACL,wBAAwB,CAAC,SAAS,EAAE,uBAAuB,KAAK,CAAC;GACjE,kCAAkC;GAClC,oCAAoC;GACpC,mCAAmC;EACrC;CACF;CACA,iBAAiB,aAAa,iBAAiB;EAC7C,yBAAyB;EACzB,2BAA2B;CAC7B,CAAC;AACH;AAYA,MAAM,iBAAiC;CAAE;CAAM;CAAO;AAAQ"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
const require_utils_ast = require('../utils/ast.cjs');
|
|
3
|
+
const require_utils_createRule = require('../utils/createRule.cjs');
|
|
4
|
+
let _intlayer_config_callers = require("@intlayer/config/callers");
|
|
5
|
+
|
|
6
|
+
//#region src/rules/enforceAdapterImport.ts
|
|
7
|
+
const ADAPTER_SCOPE = "@intlayer/";
|
|
8
|
+
/**
|
|
9
|
+
* Original library specifier → the `@intlayer/*` adapter that replaces it.
|
|
10
|
+
*
|
|
11
|
+
* Derived from the shared registry rather than hard-coded: every compat
|
|
12
|
+
* descriptor lists both its original specifiers and their adapter equivalents in
|
|
13
|
+
* `importSources`, so a library added to `@intlayer/config/callers` is covered
|
|
14
|
+
* here for free.
|
|
15
|
+
*
|
|
16
|
+
* Sub-paths line up by position within a descriptor — `next-intl/server` and
|
|
17
|
+
* `@intlayer/next-intl/server` are listed together — so the mapping is built by
|
|
18
|
+
* matching each original against the adapter that ends with the same tail.
|
|
19
|
+
*/
|
|
20
|
+
const buildAdapterMap = () => {
|
|
21
|
+
const adapterBySpecifier = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const descriptor of _intlayer_config_callers.ALL_CALLERS) {
|
|
23
|
+
const adapters = descriptor.importSources.filter((source) => source.startsWith(ADAPTER_SCOPE));
|
|
24
|
+
if (adapters.length === 0) continue;
|
|
25
|
+
const originals = descriptor.importSources.filter((source) => !source.startsWith(ADAPTER_SCOPE));
|
|
26
|
+
for (const original of originals) {
|
|
27
|
+
const adapter = adapters.find((candidate) => candidate === `${ADAPTER_SCOPE}${original}`) ?? adapters.find((candidate) => candidate.endsWith(`/${original}`)) ?? adapters[0];
|
|
28
|
+
if (adapter && !adapterBySpecifier.has(original)) adapterBySpecifier.set(original, adapter);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return adapterBySpecifier;
|
|
32
|
+
};
|
|
33
|
+
const ADAPTER_BY_SPECIFIER = buildAdapterMap();
|
|
34
|
+
const enforceAdapterImport = require_utils_createRule.createRule("enforce-adapter-import", {
|
|
35
|
+
meta: {
|
|
36
|
+
type: "suggestion",
|
|
37
|
+
fixable: "code",
|
|
38
|
+
docs: { description: "Import compat-library APIs from their `@intlayer/*` adapter rather than the original package" },
|
|
39
|
+
messages: { useAdapterImport: "Import from `{{adapter}}` instead of `{{original}}`. The original specifier only resolves to Intlayer when the bundler alias is configured; the adapter always does." }
|
|
40
|
+
},
|
|
41
|
+
create: (context) => {
|
|
42
|
+
/** Report and rewrite a module specifier that has an adapter equivalent. */
|
|
43
|
+
const checkSource = (node) => {
|
|
44
|
+
if (!node) return;
|
|
45
|
+
const original = node["value"];
|
|
46
|
+
if (typeof original !== "string") return;
|
|
47
|
+
const adapter = ADAPTER_BY_SPECIFIER.get(original);
|
|
48
|
+
if (!adapter) return;
|
|
49
|
+
context.report({
|
|
50
|
+
node,
|
|
51
|
+
messageId: "useAdapterImport",
|
|
52
|
+
data: {
|
|
53
|
+
adapter,
|
|
54
|
+
original
|
|
55
|
+
},
|
|
56
|
+
fix: (fixer) => {
|
|
57
|
+
const quote = (node["raw"] ?? `'${original}'`).startsWith("\"") ? "\"" : "'";
|
|
58
|
+
return fixer.replaceText(node, `${quote}${adapter}${quote}`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
ImportDeclaration: (node) => {
|
|
64
|
+
checkSource(require_utils_ast.toAstNode(node)["source"]);
|
|
65
|
+
},
|
|
66
|
+
ExportNamedDeclaration: (node) => {
|
|
67
|
+
checkSource(require_utils_ast.toAstNode(node)["source"]);
|
|
68
|
+
},
|
|
69
|
+
ExportAllDeclaration: (node) => {
|
|
70
|
+
checkSource(require_utils_ast.toAstNode(node)["source"]);
|
|
71
|
+
},
|
|
72
|
+
ImportExpression: (node) => {
|
|
73
|
+
checkSource(require_utils_ast.toAstNode(node)["source"]);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
//#endregion
|
|
80
|
+
exports.enforceAdapterImport = enforceAdapterImport;
|
|
81
|
+
//# sourceMappingURL=enforceAdapterImport.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enforceAdapterImport.cjs","names":["ALL_CALLERS","createRule","toAstNode"],"sources":["../../../src/rules/enforceAdapterImport.ts"],"sourcesContent":["import { ALL_CALLERS } from '@intlayer/config/callers';\nimport { type AstNode, toAstNode } from '../utils/ast';\nimport { createRule } from '../utils/createRule';\n\ntype MessageIds = 'useAdapterImport';\n\nconst ADAPTER_SCOPE = '@intlayer/';\n\n/**\n * Original library specifier → the `@intlayer/*` adapter that replaces it.\n *\n * Derived from the shared registry rather than hard-coded: every compat\n * descriptor lists both its original specifiers and their adapter equivalents in\n * `importSources`, so a library added to `@intlayer/config/callers` is covered\n * here for free.\n *\n * Sub-paths line up by position within a descriptor — `next-intl/server` and\n * `@intlayer/next-intl/server` are listed together — so the mapping is built by\n * matching each original against the adapter that ends with the same tail.\n */\nconst buildAdapterMap = (): Map<string, string> => {\n const adapterBySpecifier = new Map<string, string>();\n\n for (const descriptor of ALL_CALLERS) {\n const adapters = descriptor.importSources.filter((source) =>\n source.startsWith(ADAPTER_SCOPE)\n );\n\n if (adapters.length === 0) continue;\n\n const originals = descriptor.importSources.filter(\n (source) => !source.startsWith(ADAPTER_SCOPE)\n );\n\n for (const original of originals) {\n // `next-intl/server` → prefer `@intlayer/next-intl/server` over\n // `@intlayer/next-intl`, so match on the longest shared suffix.\n const adapter =\n adapters.find(\n (candidate) => candidate === `${ADAPTER_SCOPE}${original}`\n ) ??\n adapters.find((candidate) => candidate.endsWith(`/${original}`)) ??\n adapters[0];\n\n if (adapter && !adapterBySpecifier.has(original)) {\n adapterBySpecifier.set(original, adapter);\n }\n }\n }\n\n return adapterBySpecifier;\n};\n\nconst ADAPTER_BY_SPECIFIER = buildAdapterMap();\n\nexport const enforceAdapterImport = createRule<[], MessageIds>(\n 'enforce-adapter-import',\n {\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n 'Import compat-library APIs from their `@intlayer/*` adapter rather than the original package',\n },\n messages: {\n useAdapterImport:\n 'Import from `{{adapter}}` instead of `{{original}}`. The original specifier only resolves to Intlayer when the bundler alias is configured; the adapter always does.',\n },\n },\n\n create: (context) => {\n /** Report and rewrite a module specifier that has an adapter equivalent. */\n const checkSource = (node: AstNode | null | undefined): void => {\n if (!node) return;\n\n const original = node['value'];\n\n if (typeof original !== 'string') return;\n\n const adapter = ADAPTER_BY_SPECIFIER.get(original);\n\n if (!adapter) return;\n\n context.report({\n node: node as never,\n messageId: 'useAdapterImport',\n data: { adapter, original },\n fix: (fixer) => {\n const raw = (node['raw'] as string | undefined) ?? `'${original}'`;\n const quote = raw.startsWith('\"') ? '\"' : \"'\";\n\n return fixer.replaceText(\n node as never,\n `${quote}${adapter}${quote}`\n );\n },\n });\n };\n\n return {\n ImportDeclaration: (node) => {\n checkSource(toAstNode(node)['source'] as AstNode);\n },\n\n ExportNamedDeclaration: (node) => {\n checkSource(toAstNode(node)['source'] as AstNode);\n },\n\n ExportAllDeclaration: (node) => {\n checkSource(toAstNode(node)['source'] as AstNode);\n },\n\n ImportExpression: (node) => {\n checkSource(toAstNode(node)['source'] as AstNode);\n },\n };\n },\n }\n);\n"],"mappings":";;;;;;AAMA,MAAM,gBAAgB;;;;;;;;;;;;;AActB,MAAM,wBAA6C;CACjD,MAAM,qCAAqB,IAAI,IAAoB;CAEnD,KAAK,MAAM,cAAcA,sCAAa;EACpC,MAAM,WAAW,WAAW,cAAc,QAAQ,WAChD,OAAO,WAAW,aAAa,CACjC;EAEA,IAAI,SAAS,WAAW,GAAG;EAE3B,MAAM,YAAY,WAAW,cAAc,QACxC,WAAW,CAAC,OAAO,WAAW,aAAa,CAC9C;EAEA,KAAK,MAAM,YAAY,WAAW;GAGhC,MAAM,UACJ,SAAS,MACN,cAAc,cAAc,GAAG,gBAAgB,UAClD,KACA,SAAS,MAAM,cAAc,UAAU,SAAS,IAAI,UAAU,CAAC,KAC/D,SAAS;GAEX,IAAI,WAAW,CAAC,mBAAmB,IAAI,QAAQ,GAC7C,mBAAmB,IAAI,UAAU,OAAO;EAE5C;CACF;CAEA,OAAO;AACT;AAEA,MAAM,uBAAuB,gBAAgB;AAE7C,MAAa,uBAAuBC,oCAClC,0BACA;CACE,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aACE,+FACJ;EACA,UAAU,EACR,kBACE,uKACJ;CACF;CAEA,SAAS,YAAY;;EAEnB,MAAM,eAAe,SAA2C;GAC9D,IAAI,CAAC,MAAM;GAEX,MAAM,WAAW,KAAK;GAEtB,IAAI,OAAO,aAAa,UAAU;GAElC,MAAM,UAAU,qBAAqB,IAAI,QAAQ;GAEjD,IAAI,CAAC,SAAS;GAEd,QAAQ,OAAO;IACP;IACN,WAAW;IACX,MAAM;KAAE;KAAS;IAAS;IAC1B,MAAM,UAAU;KAEd,MAAM,SADO,KAAK,UAAiC,IAAI,SAAS,GAC/C,CAAC,WAAW,IAAG,IAAI,OAAM;KAE1C,OAAO,MAAM,YACX,MACA,GAAG,QAAQ,UAAU,OACvB;IACF;GACF,CAAC;EACH;EAEA,OAAO;GACL,oBAAoB,SAAS;IAC3B,YAAYC,4BAAU,IAAI,CAAC,CAAC,SAAoB;GAClD;GAEA,yBAAyB,SAAS;IAChC,YAAYA,4BAAU,IAAI,CAAC,CAAC,SAAoB;GAClD;GAEA,uBAAuB,SAAS;IAC9B,YAAYA,4BAAU,IAAI,CAAC,CAAC,SAAoB;GAClD;GAEA,mBAAmB,SAAS;IAC1B,YAAYA,4BAAU,IAAI,CAAC,CAAC,SAAoB;GAClD;EACF;CACF;AACF,CACF"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
const require_utils_ast = require('../utils/ast.cjs');
|
|
3
|
+
const require_utils_createRule = require('../utils/createRule.cjs');
|
|
4
|
+
const require_utils_callerMatcher = require('../utils/callerMatcher.cjs');
|
|
5
|
+
|
|
6
|
+
//#region src/rules/noDynamicFieldAccess.ts
|
|
7
|
+
/**
|
|
8
|
+
* Property name holding the translation function in a `destructured-t` result,
|
|
9
|
+
* e.g. `const { t } = useTranslation('home')`.
|
|
10
|
+
*/
|
|
11
|
+
const TRANSLATOR_PROPERTY_NAME = "t";
|
|
12
|
+
/**
|
|
13
|
+
* True when a computed member access reads a field the compiler can still name:
|
|
14
|
+
* a literal (`content['title']`, `items[0]`) or an uninterpolated template.
|
|
15
|
+
*
|
|
16
|
+
* @param property - The `property` node of a computed MemberExpression.
|
|
17
|
+
*/
|
|
18
|
+
const isStaticComputedProperty = (property) => {
|
|
19
|
+
if (!property) return false;
|
|
20
|
+
if (property.type === "Literal") return true;
|
|
21
|
+
return require_utils_ast.getStaticStringValue(property) !== null;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The variables a declarator binds, paired with the identifier that declared
|
|
25
|
+
* each one — needed to tell `t` apart from `i18n` in a destructuring pattern.
|
|
26
|
+
*
|
|
27
|
+
* @param variables - Result of `sourceCode.getDeclaredVariables(declarator)`.
|
|
28
|
+
* @param identifier - The declaring identifier to look up.
|
|
29
|
+
*/
|
|
30
|
+
const findVariableForIdentifier = (variables, identifier) => variables.find((variable) => variable.identifiers.some((declared) => declared === identifier)) ?? null;
|
|
31
|
+
/**
|
|
32
|
+
* Locate the identifier bound to the `t` property of an object pattern,
|
|
33
|
+
* honouring renames (`const { t: translate } = …`).
|
|
34
|
+
*
|
|
35
|
+
* @param pattern - The declarator's `id`, expected to be an ObjectPattern.
|
|
36
|
+
*/
|
|
37
|
+
const findTranslatorIdentifier = (pattern) => {
|
|
38
|
+
if (pattern.type !== "ObjectPattern") return null;
|
|
39
|
+
const properties = pattern["properties"] ?? [];
|
|
40
|
+
for (const property of properties) {
|
|
41
|
+
if (property.type !== "Property" || property["computed"] === true) continue;
|
|
42
|
+
const key = property["key"];
|
|
43
|
+
if ((key?.type === "Identifier" ? key["name"] : key?.type === "Literal" ? String(key["value"]) : null) !== TRANSLATOR_PROPERTY_NAME) continue;
|
|
44
|
+
const value = property["value"];
|
|
45
|
+
return value?.type === "Identifier" ? value : null;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
};
|
|
49
|
+
const noDynamicFieldAccess = require_utils_createRule.createRule("no-dynamic-field-access", {
|
|
50
|
+
meta: {
|
|
51
|
+
type: "problem",
|
|
52
|
+
docs: { description: "Require the field read from an Intlayer dictionary to be statically known" },
|
|
53
|
+
messages: {
|
|
54
|
+
dynamicField: "Computed access on Intlayer content is not statically analyzable. The purge pass strips fields it cannot see used, so this may read `undefined` at runtime — read the field by name instead.",
|
|
55
|
+
dynamicMessageId: "The message path passed to `{{caller}}` must be a static string. The purge pass strips fields it cannot see used, so this may read `undefined` at runtime."
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
create: (context) => {
|
|
59
|
+
const sourceCode = context.sourceCode;
|
|
60
|
+
let activeDescriptors = require_utils_callerMatcher.getActiveDescriptors(/* @__PURE__ */ new Set());
|
|
61
|
+
/** Report every computed, non-static member access on a bound variable. */
|
|
62
|
+
const checkContentVariable = (variable) => {
|
|
63
|
+
for (const reference of variable.references) {
|
|
64
|
+
const identifier = reference.identifier;
|
|
65
|
+
const parent = identifier.parent;
|
|
66
|
+
if (parent?.type !== "MemberExpression") continue;
|
|
67
|
+
if (parent["object"] !== identifier) continue;
|
|
68
|
+
if (parent["computed"] !== true) continue;
|
|
69
|
+
if (isStaticComputedProperty(parent["property"])) continue;
|
|
70
|
+
context.report({
|
|
71
|
+
node: parent,
|
|
72
|
+
messageId: "dynamicField"
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
/** Report every call of a translation function with a runtime message path. */
|
|
77
|
+
const checkTranslatorVariable = (variable, callerName) => {
|
|
78
|
+
for (const reference of variable.references) {
|
|
79
|
+
const identifier = reference.identifier;
|
|
80
|
+
const parent = identifier.parent;
|
|
81
|
+
if (parent?.type !== "CallExpression") continue;
|
|
82
|
+
if (parent["callee"] !== identifier) continue;
|
|
83
|
+
const [firstArgument] = parent["arguments"] ?? [];
|
|
84
|
+
if (!firstArgument) continue;
|
|
85
|
+
if (require_utils_ast.getStaticStringValue(firstArgument) !== null) continue;
|
|
86
|
+
context.report({
|
|
87
|
+
node: firstArgument,
|
|
88
|
+
messageId: "dynamicMessageId",
|
|
89
|
+
data: { caller: callerName }
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
Program: (node) => {
|
|
95
|
+
activeDescriptors = require_utils_callerMatcher.getActiveDescriptors(require_utils_callerMatcher.collectImportSources(require_utils_ast.toAstNode(node)));
|
|
96
|
+
},
|
|
97
|
+
VariableDeclarator: (node) => {
|
|
98
|
+
const astNode = require_utils_ast.toAstNode(node);
|
|
99
|
+
const init = astNode["init"];
|
|
100
|
+
if (!init) return;
|
|
101
|
+
const match = require_utils_callerMatcher.matchCaller(init, activeDescriptors);
|
|
102
|
+
if (!match) return;
|
|
103
|
+
const { descriptor } = match;
|
|
104
|
+
const shape = descriptor.translationFunction;
|
|
105
|
+
const kind = shape === "content" ? "content" : shape === "return-value" || shape === "destructured-t" ? "translator" : null;
|
|
106
|
+
if (!kind) return;
|
|
107
|
+
const declaredVariables = sourceCode.getDeclaredVariables(node);
|
|
108
|
+
const id = astNode["id"];
|
|
109
|
+
if (kind === "content") {
|
|
110
|
+
for (const variable of declaredVariables) checkContentVariable(variable);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (shape === "return-value") {
|
|
114
|
+
if (id.type !== "Identifier") return;
|
|
115
|
+
const variable = findVariableForIdentifier(declaredVariables, id);
|
|
116
|
+
if (variable) checkTranslatorVariable(variable, descriptor.callerName);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const translatorIdentifier = findTranslatorIdentifier(id);
|
|
120
|
+
if (!translatorIdentifier) return;
|
|
121
|
+
const variable = findVariableForIdentifier(declaredVariables, translatorIdentifier);
|
|
122
|
+
if (variable) checkTranslatorVariable(variable, descriptor.callerName);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
//#endregion
|
|
129
|
+
exports.noDynamicFieldAccess = noDynamicFieldAccess;
|
|
130
|
+
//# sourceMappingURL=noDynamicFieldAccess.cjs.map
|