eslint-plugin-chrome-extensions 2.1.0 → 2.3.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.
package/README.md CHANGED
@@ -41,7 +41,7 @@ import json from '@eslint/json'
41
41
  export default [
42
42
  // other configs
43
43
  {
44
- files: ['extension/**/*.js'],
44
+ files: ['**/extension/**/*.js'],
45
45
  plugins: { 'chrome-extensions': chromeExtensions },
46
46
  rules: {
47
47
  ...chromeExtensions.configs.recommended.rules,
@@ -49,7 +49,7 @@ export default [
49
49
  }
50
50
  },
51
51
  {
52
- files: ['extension/manifest.json'], language: 'json/json',
52
+ files: ['**/manifest.json'], language: 'json/json',
53
53
  plugins: { 'chrome-extensions': chromeExtensions, json },
54
54
  rules: {
55
55
  ...chromeExtensions.configs.manifest.rules,
@@ -73,7 +73,7 @@ Available bundles: `recommended`, `strict` (everything), `problem` (deterministi
73
73
  <h3 id="manifest-key-order">
74
74
  <code>manifest-key-order</code>
75
75
  <a title="Only works w/ 'json/json' config block" href="#usage">
76
- <img height=18 alt="[Manifest only]" src="https://img.shields.io/badge/Manifest_only-32fcee?style=flat-square" align="middle"></a>
76
+ <img height=18 alt="[Manifest only]" src="https://img.shields.io/badge/Manifest_only-999999?style=flat-square" align="middle"></a>
77
77
  <a title="Accepts eslint --fix" target="_blank" href="https://eslint.org/docs/latest/use/command-line-interface#--fix">
78
78
  <img height=18 alt="[Auto-fixable]" src="https://img.shields.io/badge/Auto--fixable-3b5bdb?style=flat-square" align="middle"></a>
79
79
  <a title="Doesn't prevent bugs" href="#manifest-key-order">
@@ -89,7 +89,7 @@ Available bundles: `recommended`, `strict` (everything), `problem` (deterministi
89
89
 
90
90
  <summary>Enforces a preferred order for manifest keys.</summary>
91
91
 
92
- #### Options
92
+ <h4 id="manifest-key-order-options">Options</h4>
93
93
 
94
94
  - `order` (default `[]`) – manifest keys in custom order (e.g. `['name', 'manifest_version']`) to selectively override built-in order with
95
95
 
@@ -129,7 +129,98 @@ If you don't care about the order of manifest keys.
129
129
  #### See also
130
130
 
131
131
  - [`require-mv3`](#require-mv3)
132
+ <a title="Included in recommended config" href="#usage"><img height=14 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
133
+ <a title="Works w/ 'json/json' config block" href="#usage"><img height=14 alt="[Manifest]" src="https://img.shields.io/badge/Manifest-999999?style=flat-square" align="middle"></a>
132
134
  - [`valid-mv3-schema`](#valid-mv3-schema)
135
+ <a title="Included in recommended config" href="#usage"><img height=14 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
136
+ <a title="Works w/ 'json/json' config block" href="#usage"><img height=14 alt="[Manifest]" src="https://img.shields.io/badge/Manifest-999999?style=flat-square" align="middle"></a>
137
+
138
+ <div align="center">
139
+
140
+ [Back to index ↑][epce-rules]
141
+
142
+ </div>
143
+
144
+ ---
145
+
146
+ </details>
147
+
148
+ <h3 id="manifest-spacing">
149
+ <code>manifest-spacing</code>
150
+ <a title="Only works w/ 'json/json' config block" href="#usage">
151
+ <img height=18 alt="[Manifest only]" src="https://img.shields.io/badge/Manifest_only-999999?style=flat-square" align="middle"></a>
152
+ <a title="Accepts eslint --fix" target="_blank" href="https://eslint.org/docs/latest/use/command-line-interface#--fix">
153
+ <img height=18 alt="[Auto-fixable]" src="https://img.shields.io/badge/Auto--fixable-3b5bdb?style=flat-square" align="middle"></a>
154
+ <a title="Doesn't prevent bugs" href="#manifest-spacing">
155
+ <img height=18 alt="[Stylistic]" src="https://img.shields.io/badge/Stylistic-af68ff?style=flat-square" align="middle"></a>
156
+ <a title="Accepts array w/ options" href="#usage">
157
+ <img height=18 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
158
+ <a title="Copy rule URL" target="_blank"
159
+ href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-chrome-extensions%2F%23manifest-spacing">
160
+ <img height=16 width=21 alt="[Copy rule URL]" src="https://api.iconify.design/mdi/content-copy.svg?color=%23cccccc" align="middle"></a>
161
+ </h3>
162
+
163
+ <details>
164
+
165
+ <summary>Enforces consistent indentation and spacing in <code>manifest.json</code>.</summary>
166
+
167
+ <h4 id="manifest-spacing-options">Options</h4>
168
+
169
+ - `spaces` (default `2`) – number of spaces per indent level
170
+ - `checkColons` (default `true`) – enforce 0 spaces before and 1 space after `:` in key/value pairs
171
+ - `checkCommas` (default `true`) – enforce 0 spaces before and 1 space after `,` in inline objects
172
+ - `checkBraces` (default `true`) – enforce 1 space after `{` and before `}` in inline objects
173
+ - `checkBrackets` (default `true`) – enforce 0 spaces after `[` and before `]` in inline arrays
174
+
175
+ #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/gray.svg" align="bottom"> Incorrect code
176
+
177
+ ```diff
178
+ /* eslint chrome-extensions/manifest-spacing: ['error', { spaces: 2 }] */
179
+
180
+ {
181
+ ! "manifest_version": 3,
182
+ ! "version" : "1.0.0",
183
+ ! "permissions": [ "tabs","cookies" ],
184
+ ! "icons": {"16": "icon16.png"},
185
+ "name": "My Extension"
186
+ }
187
+ ```
188
+
189
+ #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/green.svg" align="bottom"> Correct code
190
+
191
+ ```diff
192
+ /* eslint chrome-extensions/manifest-spacing: ['error', { spaces: 2 }] */
193
+
194
+ {
195
+ - "manifest_version": 3,
196
+ + "manifest_version": 3,
197
+ - "version" : "1.0.0",
198
+ + "version": "1.0.0",
199
+ - "permissions": [ "tabs","cookies" ],
200
+ + "permissions": ["tabs", "cookies"],
201
+ - "icons": {"16": "icon16.png"},
202
+ + "icons": { "16": "icon16.png" },
203
+ "name": "My Extension"
204
+ }
205
+ ```
206
+
207
+ #### When not to use it
208
+
209
+ If you don't care about visual consistency in your manifest.
210
+
211
+ #### Introduced in
212
+
213
+ <a target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-chrome-extensions/releases/tag/v2.2.0">v2.2.0</a> (October 12, 2026)
214
+
215
+ #### See also
216
+
217
+ - [`manifest-key-order`](#manifest-key-order)
218
+ <a title="Only works w/ 'json/json' config block" href="#usage"><img height=14 alt="[Manifest only]" src="https://img.shields.io/badge/Manifest_only-999999?style=flat-square" align="middle"></a>
219
+ <a title="Accepts eslint --fix" target="_blank" href="https://eslint.org/docs/latest/use/command-line-interface#--fix"><img height=14 alt="[Auto-fixable]" src="https://img.shields.io/badge/Auto--fixable-3b5bdb?style=flat-square" align="middle"></a>
220
+ <a title="Doesn't prevent bugs" href="#manifest-key-order"><img height=14 alt="[Stylistic]" src="https://img.shields.io/badge/Stylistic-af68ff?style=flat-square" align="middle"></a>
221
+ <a title="Accepts array w/ options" href="#manifest-key-order-options"><img height=14 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
222
+ - [`no-duplicate-manifest-keys`](#no-duplicate-manifest-keys)
223
+ <a title="Only works w/ 'json/json' config block" href="#usage"><img height=14 alt="[Manifest only]" src="https://img.shields.io/badge/Manifest_only-999999?style=flat-square" align="middle"></a>
133
224
 
134
225
  <div align="center">
135
226
 
@@ -180,8 +271,8 @@ If you intentionally interact w/ host page styles.
180
271
 
181
272
  #### See also
182
273
 
183
- - <a target="_blank" href="https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts">Chrome for Developers — Content scripts</a>
184
- - <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM">MDN — Using shadow DOM</a>
274
+ - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.chrome.com&sz=64">&ensp;<a target="_blank" href="https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts">Chrome for Developers — Content scripts</a>
275
+ - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.mozilla.org&sz=64">&ensp;<a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM">MDN — Using shadow DOM</a>
185
276
 
186
277
  <div align="center">
187
278
 
@@ -208,7 +299,7 @@ If you intentionally interact w/ host page styles.
208
299
 
209
300
  <summary>Disallows deprecated <code>chrome</code> API calls.</summary>
210
301
 
211
- #### Options
302
+ <h4 id="no-deprecated-apis-options">Options</h4>
212
303
 
213
304
  - `target` (default `'mv3'`) – Chrome extension manifest version whose deprecations to validate against (`'mv3'`, `'mv2'`)
214
305
  - `allowedAPIs` (default `[]`) – additional deprecated API namespaces to allow, as a single string or array
@@ -246,7 +337,7 @@ If you don't want deprecation validation at all. For individual deprecated APIs
246
337
 
247
338
  #### See also
248
339
 
249
- - <a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/api">Chrome for Developers — API reference</a>
340
+ - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.chrome.com&sz=64">&ensp;<a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/api">Chrome for Developers — API reference</a>
250
341
 
251
342
  <div align="center">
252
343
 
@@ -261,7 +352,7 @@ If you don't want deprecation validation at all. For individual deprecated APIs
261
352
  <h3 id="no-duplicate-manifest-keys">
262
353
  <code>no-duplicate-manifest-keys</code>
263
354
  <a title="Only works w/ 'json/json' config block" href="#usage">
264
- <img height=18 alt="[Manifest only]" src="https://img.shields.io/badge/Manifest_only-32fcee?style=flat-square" align="middle"></a>
355
+ <img height=18 alt="[Manifest only]" src="https://img.shields.io/badge/Manifest_only-999999?style=flat-square" align="middle"></a>
265
356
  <a title="Copy rule URL" target="_blank"
266
357
  href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-chrome-extensions%2F%23no-duplicate-manifest-keys">
267
358
  <img height=16 width=21 alt="[Copy rule URL]" src="https://api.iconify.design/mdi/content-copy.svg?color=%23cccccc" align="middle"></a>
@@ -308,7 +399,12 @@ If you rely on a Chromium fork resolving duplicate keys in a specific order.
308
399
  #### See also
309
400
 
310
401
  - [`manifest-key-order`](#manifest-key-order)
402
+ <a title="Only works w/ 'json/json' config block" href="#usage"><img height=14 alt="[Manifest only]" src="https://img.shields.io/badge/Manifest_only-999999?style=flat-square" align="middle"></a>
403
+ <a title="Accepts eslint --fix" target="_blank" href="https://eslint.org/docs/latest/use/command-line-interface#--fix"><img height=14 alt="[Auto-fixable]" src="https://img.shields.io/badge/Auto--fixable-3b5bdb?style=flat-square" align="middle"></a>
404
+ <a title="Doesn't prevent bugs" href="#manifest-key-order"><img height=14 alt="[Stylistic]" src="https://img.shields.io/badge/Stylistic-af68ff?style=flat-square" align="middle"></a>
405
+ <a title="Accepts array w/ options" href="#manifest-key-order-options"><img height=14 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
311
406
  - [`no-unused-permissions`](#no-unused-permissions)
407
+ <a title="Only works w/ 'json/json' config block" href="#usage"><img height=14 alt="[Manifest only]" src="https://img.shields.io/badge/Manifest_only-999999?style=flat-square" align="middle"></a>
312
408
 
313
409
  <div align="center">
314
410
 
@@ -361,8 +457,7 @@ If you intentionally target HTTP endpoints.
361
457
 
362
458
  #### See also
363
459
 
364
- - <a target="_blank" href="https://developer.chrome.com/docs/extensions/develop/concepts/network-requests">Chrome for Developers — Cross-origin network requests</a>
365
- - [`no-remote-code`](#no-remote-code)
460
+ - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.chrome.com&sz=64">&ensp;<a target="_blank" href="https://developer.chrome.com/docs/extensions/develop/concepts/network-requests">Chrome for Developers — Cross-origin network requests</a>
366
461
 
367
462
  <div align="center">
368
463
 
@@ -391,7 +486,7 @@ If you intentionally target HTTP endpoints.
391
486
 
392
487
  <summary>Requires sender/origin validation + filtered <code>tabs.query()</code> + explicit <code>window.postMessage()</code> target.</summary>
393
488
 
394
- #### Options
489
+ <h4 id="no-insecure-messaging-options">Options</h4>
395
490
 
396
491
  - `disallowWildcardTarget` (default `true`) – flag <code>postMessage()</code> calls that use <code>'*'</code> as <code>targetOrigin</code>
397
492
 
@@ -445,9 +540,9 @@ If you trust every external caller and open tab, or you intentionally broadcast
445
540
 
446
541
  #### See also
447
542
 
448
- - <a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/api/runtime#event-onMessageExternal">Chrome for Developers — `runtime.onMessageExternal`</a>
449
- - <a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/api/tabs#method-query">Chrome for Developers — `tabs.query()`</a>
450
- - <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#security_concerns">MDN — `window.postMessage()` security concerns</a>
543
+ - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.chrome.com&sz=64">&ensp;<a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/api/runtime#event-onMessageExternal">Chrome for Developers — `runtime.onMessageExternal`</a>
544
+ - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.chrome.com&sz=64">&ensp;<a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/api/tabs#method-query">Chrome for Developers — `tabs.query()`</a>
545
+ - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.mozilla.org&sz=64">&ensp;<a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#security_concerns">MDN — `window.postMessage()` security concerns</a>
451
546
 
452
547
  <div align="center">
453
548
 
@@ -464,7 +559,7 @@ If you trust every external caller and open tab, or you intentionally broadcast
464
559
  <a title="Included in recommended config" href="#usage">
465
560
  <img height=18 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
466
561
  <a title="Works w/ 'json/json' config block" href="#usage">
467
- <img height=18 alt="[Manifest]" src="https://img.shields.io/badge/Manifest-32fcee?style=flat-square" align="middle"></a>
562
+ <img height=18 alt="[Manifest]" src="https://img.shields.io/badge/Manifest-999999?style=flat-square" align="middle"></a>
468
563
  <a title="Accepts array w/ options" href="#usage">
469
564
  <img height=18 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
470
565
  <a title="Copy rule URL" target="_blank"
@@ -476,7 +571,7 @@ If you trust every external caller and open tab, or you intentionally broadcast
476
571
 
477
572
  <summary>Disallows invalid or unsupported manifest permission names.</summary>
478
573
 
479
- #### Options
574
+ <h4 id="no-invalid-permissions-options">Options</h4>
480
575
 
481
576
  - `target` (default `'mv3'`) – Chrome manifest version whose permissions to validate against (`'mv3'`, `'mv2'`)
482
577
  - `allowedPermissions` (default `[]`) – additional permission names to allow beyond the built-in list, as a single string or array
@@ -516,7 +611,9 @@ If you don't want permission validation at all. For custom permissions from a Ch
516
611
  #### See also
517
612
 
518
613
  - [`no-missing-permissions`](#no-missing-permissions)
614
+ <a title="Included in recommended config" href="#usage"><img height=14 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
519
615
  - [`no-unused-permissions`](#no-unused-permissions)
616
+ <a title="Only works w/ 'json/json' config block" href="#usage"><img height=14 alt="[Manifest only]" src="https://img.shields.io/badge/Manifest_only-999999?style=flat-square" align="middle"></a>
520
617
 
521
618
  <div align="center">
522
619
 
@@ -579,7 +676,11 @@ If you use APIs through aliases or wrappers that obscure the original call site.
579
676
  #### See also
580
677
 
581
678
  - [`no-invalid-permissions`](#no-invalid-permissions)
679
+ <a title="Included in recommended config" href="#usage"><img height=14 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
680
+ <a title="Works w/ 'json/json' config block" href="#usage"><img height=14 alt="[Manifest]" src="https://img.shields.io/badge/Manifest-999999?style=flat-square" align="middle"></a>
681
+ <a title="Accepts array w/ options" href="#no-invalid-permissions-options"><img height=14 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
582
682
  - [`no-unused-permissions`](#no-unused-permissions)
683
+ <a title="Only works w/ 'json/json' config block" href="#usage"><img height=14 alt="[Manifest only]" src="https://img.shields.io/badge/Manifest_only-999999?style=flat-square" align="middle"></a>
583
684
 
584
685
  <div align="center">
585
686
 
@@ -641,8 +742,8 @@ If your paths are built at runtime.
641
742
 
642
743
  #### See also
643
744
 
644
- - <a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/manifest/web-accessible-resources">Chrome for Developers — `web_accessible_resources`</a>
645
- - <a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/api/runtime#method-getURL">Chrome for Developers — `runtime.getURL()`</a>
745
+ - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.chrome.com&sz=64">&ensp;<a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/manifest/web-accessible-resources">Chrome for Developers — `web_accessible_resources`</a>
746
+ - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.chrome.com&sz=64">&ensp;<a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/api/runtime#method-getURL">Chrome for Developers — `runtime.getURL()`</a>
646
747
 
647
748
  <div align="center">
648
749
 
@@ -671,7 +772,7 @@ If your paths are built at runtime.
671
772
 
672
773
  <summary>Disallows remote or dynamically-evaluated code (blocked by MV3's CSP at runtime).</summary>
673
774
 
674
- #### Options
775
+ <h4 id="no-remote-code-options">Options</h4>
675
776
 
676
777
  - `patterns` (default `['eval', 'new-function', 'string-timer', 'dynamic-import', 'remote-script-src', 'execute-script-url']`) – detection paths to enable, as a single string or array
677
778
  - `checkExecuteScriptClosures` (default `true`) – additionally flag <code>chrome.scripting.executeScript({ func })</code> bodies that capture outer-scope variables
@@ -709,7 +810,11 @@ If you intentionally target forks w/ relaxed CSPs. For individual patterns you s
709
810
  #### See also
710
811
 
711
812
  - [`require-mv3`](#require-mv3)
813
+ <a title="Included in recommended config" href="#usage"><img height=14 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
814
+ <a title="Works w/ 'json/json' config block" href="#usage"><img height=14 alt="[Manifest]" src="https://img.shields.io/badge/Manifest-999999?style=flat-square" align="middle"></a>
712
815
  - [`valid-mv3-schema`](#valid-mv3-schema)
816
+ <a title="Included in recommended config" href="#usage"><img height=14 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
817
+ <a title="Works w/ 'json/json' config block" href="#usage"><img height=14 alt="[Manifest]" src="https://img.shields.io/badge/Manifest-999999?style=flat-square" align="middle"></a>
713
818
 
714
819
  <div align="center">
715
820
 
@@ -724,7 +829,7 @@ If you intentionally target forks w/ relaxed CSPs. For individual patterns you s
724
829
  <h3 id="no-unrestricted-host-permissions">
725
830
  <code>no-unrestricted-host-permissions</code>
726
831
  <a title="Only works w/ 'json/json' config block" href="#usage">
727
- <img height=18 alt="[Manifest only]" src="https://img.shields.io/badge/Manifest_only-32fcee?style=flat-square" align="middle"></a>
832
+ <img height=18 alt="[Manifest only]" src="https://img.shields.io/badge/Manifest_only-999999?style=flat-square" align="middle"></a>
728
833
  <a title="Accepts array w/ options" href="#usage">
729
834
  <img height=18 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
730
835
  <a title="Copy rule URL" target="_blank"
@@ -736,7 +841,7 @@ If you intentionally target forks w/ relaxed CSPs. For individual patterns you s
736
841
 
737
842
  <summary>Disallows broad host permissions that grant access to every site.</summary>
738
843
 
739
- #### Options
844
+ <h4 id="no-unrestricted-host-permissions-options">Options</h4>
740
845
 
741
846
  - `allowedPatterns` (default `[]`) – host patterns to explicitly allow (e.g. `['<all_urls>']`)
742
847
 
@@ -772,8 +877,8 @@ If you legitimately need broad access (e.g. ad blockers or dev tools).
772
877
 
773
878
  #### See also
774
879
 
775
- - <a target="_blank" href="https://developer.chrome.com/docs/extensions/develop/concepts/declare-permissions">Chrome for Developers — Declare permissions</a>
776
- - <a target="_blank" href="https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns">Chrome for Developers — Match patterns</a>
880
+ - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.chrome.com&sz=64">&ensp;<a target="_blank" href="https://developer.chrome.com/docs/extensions/develop/concepts/declare-permissions">Chrome for Developers — Declare permissions</a>
881
+ - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.chrome.com&sz=64">&ensp;<a target="_blank" href="https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns">Chrome for Developers — Match patterns</a>
777
882
 
778
883
  <div align="center">
779
884
 
@@ -788,7 +893,7 @@ If you legitimately need broad access (e.g. ad blockers or dev tools).
788
893
  <h3 id="no-unused-permissions">
789
894
  <code>no-unused-permissions</code>
790
895
  <a title="Only works w/ 'json/json' config block" href="#usage">
791
- <img height=18 alt="[Manifest only]" src="https://img.shields.io/badge/Manifest_only-32fcee?style=flat-square" align="middle"></a>
896
+ <img height=18 alt="[Manifest only]" src="https://img.shields.io/badge/Manifest_only-999999?style=flat-square" align="middle"></a>
792
897
  <a title="Copy rule URL" target="_blank"
793
898
  href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-chrome-extensions%2F%23no-unused-permissions">
794
899
  <img height=16 width=21 alt="[Copy rule URL]" src="https://api.iconify.design/mdi/content-copy.svg?color=%23cccccc" align="middle"></a>
@@ -834,7 +939,11 @@ If you access the granted API through an alias or computed property rather than
834
939
  #### See also
835
940
 
836
941
  - [`no-invalid-permissions`](#no-invalid-permissions)
942
+ <a title="Included in recommended config" href="#usage"><img height=14 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
943
+ <a title="Works w/ 'json/json' config block" href="#usage"><img height=14 alt="[Manifest]" src="https://img.shields.io/badge/Manifest-999999?style=flat-square" align="middle"></a>
944
+ <a title="Accepts array w/ options" href="#no-invalid-permissions-options"><img height=14 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
837
945
  - [`no-missing-permissions`](#no-missing-permissions)
946
+ <a title="Included in recommended config" href="#usage"><img height=14 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
838
947
 
839
948
  <div align="center">
840
949
 
@@ -851,7 +960,7 @@ If you access the granted API through an alias or computed property rather than
851
960
  <a title="Included in recommended config" href="#usage">
852
961
  <img height=18 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
853
962
  <a title="Works w/ 'json/json' config block" href="#usage">
854
- <img height=18 alt="[Manifest]" src="https://img.shields.io/badge/Manifest-32fcee?style=flat-square" align="middle"></a>
963
+ <img height=18 alt="[Manifest]" src="https://img.shields.io/badge/Manifest-999999?style=flat-square" align="middle"></a>
855
964
  <a title="Copy rule URL" target="_blank"
856
965
  href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-chrome-extensions%2F%23require-mv3">
857
966
  <img height=16 width=21 alt="[Copy rule URL]" src="https://api.iconify.design/mdi/content-copy.svg?color=%23cccccc" align="middle"></a>
@@ -893,6 +1002,8 @@ If you intentionally target MV2 for a browser that still supports it (e.g. Firef
893
1002
  #### See also
894
1003
 
895
1004
  - [`valid-mv3-schema`](#valid-mv3-schema)
1005
+ <a title="Included in recommended config" href="#usage"><img height=14 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
1006
+ <a title="Works w/ 'json/json' config block" href="#usage"><img height=14 alt="[Manifest]" src="https://img.shields.io/badge/Manifest-999999?style=flat-square" align="middle"></a>
896
1007
 
897
1008
  <div align="center">
898
1009
 
@@ -907,7 +1018,7 @@ If you intentionally target MV2 for a browser that still supports it (e.g. Firef
907
1018
  <h3 id="valid-manifest-icons">
908
1019
  <code>valid-manifest-icons</code>
909
1020
  <a title="Only works w/ 'json/json' config block" href="#usage">
910
- <img height=18 alt="[Manifest only]" src="https://img.shields.io/badge/Manifest_only-32fcee?style=flat-square" align="middle"></a>
1021
+ <img height=18 alt="[Manifest only]" src="https://img.shields.io/badge/Manifest_only-999999?style=flat-square" align="middle"></a>
911
1022
  <a title="Accepts array w/ options" href="#usage">
912
1023
  <img height=18 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
913
1024
  <a title="Copy rule URL" target="_blank"
@@ -919,7 +1030,7 @@ If you intentionally target MV2 for a browser that still supports it (e.g. Firef
919
1030
 
920
1031
  <summary>Ensures manifest <code>icons</code> exist and use the recommended sizes.</summary>
921
1032
 
922
- #### Options
1033
+ <h4 id="valid-manifest-icons-options">Options</h4>
923
1034
 
924
1035
  - `requireRecommendedSizes` (default `true`) – require all of `16`, `32`, `48`, `128`
925
1036
  - `checkIconExists` (default `true`) – flag icon paths that don't resolve to an existing file
@@ -963,7 +1074,7 @@ If your icons are injected at build time, or you target browsers that support SV
963
1074
 
964
1075
  #### See also
965
1076
 
966
- - <a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/manifest/icons">Chrome for Developers — `icons`</a>
1077
+ - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.chrome.com&sz=64">&ensp;<a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/manifest/icons">Chrome for Developers — `icons`</a>
967
1078
 
968
1079
  <div align="center">
969
1080
 
@@ -980,7 +1091,7 @@ If your icons are injected at build time, or you target browsers that support SV
980
1091
  <a title="Included in recommended config" href="#usage">
981
1092
  <img height=18 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
982
1093
  <a title="Works w/ 'json/json' config block" href="#usage">
983
- <img height=18 alt="[Manifest]" src="https://img.shields.io/badge/Manifest-32fcee?style=flat-square" align="middle"></a>
1094
+ <img height=18 alt="[Manifest]" src="https://img.shields.io/badge/Manifest-999999?style=flat-square" align="middle"></a>
984
1095
  <a title="Copy rule URL" target="_blank"
985
1096
  href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-chrome-extensions%2F%23valid-mv3-schema">
986
1097
  <img height=16 width=21 alt="[Copy rule URL]" src="https://api.iconify.design/mdi/content-copy.svg?color=%23cccccc" align="middle"></a>
@@ -1028,7 +1139,11 @@ If you intentionally target MV2 for a browser that still supports it (e.g. Firef
1028
1139
  #### See also
1029
1140
 
1030
1141
  - [`require-mv3`](#require-mv3)
1142
+ <a title="Included in recommended config" href="#usage"><img height=14 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
1143
+ <a title="Works w/ 'json/json' config block" href="#usage"><img height=14 alt="[Manifest]" src="https://img.shields.io/badge/Manifest-999999?style=flat-square" align="middle"></a>
1031
1144
  - [`no-deprecated-apis`](#no-deprecated-apis)
1145
+ <a title="Included in recommended config" href="#usage"><img height=14 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
1146
+ <a title="Accepts array w/ options" href="#no-deprecated-apis-options"><img height=14 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
1032
1147
 
1033
1148
  <div align="center">
1034
1149
 
@@ -1086,7 +1201,12 @@ If your background context isn't a service worker.
1086
1201
  #### See also
1087
1202
 
1088
1203
  - [`require-mv3`](#require-mv3)
1204
+ <a title="Included in recommended config" href="#usage"><img height=14 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
1205
+ <a title="Works w/ 'json/json' config block" href="#usage"><img height=14 alt="[Manifest]" src="https://img.shields.io/badge/Manifest-999999?style=flat-square" align="middle"></a>
1089
1206
  - [`no-remote-code`](#no-remote-code)
1207
+ <a title="Included in recommended config" href="#usage"><img height=14 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
1208
+ <a title="Included in security config" href="#usage"><img height=14 alt="[Security]" src="https://img.shields.io/badge/Security-e8357a?style=flat-square" align="middle"></a>
1209
+ <a title="Accepts array w/ options" href="#no-remote-code-options"><img height=14 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
1090
1210
 
1091
1211
  <div align="center">
1092
1212
 
@@ -1103,7 +1223,7 @@ If your background context isn't a service worker.
1103
1223
  <a title="Included in recommended config" href="#usage">
1104
1224
  <img height=18 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
1105
1225
  <a title="Works w/ 'json/json' config block" href="#usage">
1106
- <img height=18 alt="[Manifest]" src="https://img.shields.io/badge/Manifest-32fcee?style=flat-square" align="middle"></a>
1226
+ <img height=18 alt="[Manifest]" src="https://img.shields.io/badge/Manifest-999999?style=flat-square" align="middle"></a>
1107
1227
  <a title="Accepts array w/ options" href="#usage">
1108
1228
  <img height=18 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
1109
1229
  <a title="Copy rule URL" target="_blank"
@@ -1115,7 +1235,7 @@ If your background context isn't a service worker.
1115
1235
 
1116
1236
  <summary>Validates the manifest <code>version</code> field.</summary>
1117
1237
 
1118
- #### Options
1238
+ <h4 id="valid-version-field-options">Options</h4>
1119
1239
 
1120
1240
  - `versionType` (default `'chrome'`) – version format to enforce (`'chrome'`, `'semver'`, `'date'`)
1121
1241
  - `allowFutureDate` (default `false`) – allow `version` dates later than the current date
@@ -1156,7 +1276,7 @@ If you don't care about version format consistency, or your version is injected
1156
1276
 
1157
1277
  #### See also
1158
1278
 
1159
- - <a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/manifest/version">Chrome for Developers — `version`</a>
1279
+ - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.chrome.com&sz=64">&ensp;<a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/manifest/version">Chrome for Developers — `version`</a>
1160
1280
 
1161
1281
  <div align="center">
1162
1282
 
package/docs/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 🏛️ MIT License
2
2
 
3
- **Copyright © 2026 [Adam Lui](https://codeberg.org/adamlui).**
3
+ **Copyright © 2026 [Adam Lui](https://codeberg.org/adamlui)**
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-chrome-extensions",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "ESLint rules for Chrome extensions",
5
5
  "type": "module",
6
6
  "engines": {
@@ -34,7 +34,6 @@
34
34
  ],
35
35
  "types": "src/index.d.ts",
36
36
  "scripts": {
37
- "prepare": "husky",
38
37
  "lint": "eslint . --cache && yamllint .",
39
38
  "lint:all": "eslint . && yamllint .",
40
39
  "lint:fix": "eslint . --fix --cache && yamllint .",
@@ -58,17 +57,17 @@
58
57
  "chromium"
59
58
  ],
60
59
  "peerDependencies": {
61
- "eslint": ">=9.6.0",
62
- "@eslint/json": ">=0.1.0"
60
+ "@eslint/json": ">=0.1.0",
61
+ "eslint": ">=9.6.0"
63
62
  },
64
63
  "devDependencies": {
65
64
  "@e18e/eslint-plugin": "^0.8.1",
66
65
  "@eslint/js": "^10.0.1",
67
- "@eslint/json": "^2.1.0",
68
- "@eslint/markdown": "^8.0.3",
66
+ "@eslint/json": "^2.1.1",
67
+ "@eslint/markdown": "^7.5.1",
69
68
  "@stylistic/eslint-plugin": "^5.10.0",
70
69
  "eslint": "^10.12.0",
71
- "eslint-plugin-constructors": "^1.4.1",
70
+ "eslint-plugin-constructors": "^1.4.2",
72
71
  "eslint-plugin-eslint-plugin": "^7.6.2",
73
72
  "eslint-plugin-import-x": "^4.17.1",
74
73
  "eslint-plugin-n": "^18.4.1",
@@ -77,7 +76,6 @@
77
76
  "eslint-plugin-promise": "^7.3.0",
78
77
  "eslint-plugin-regexp": "^3.3.1",
79
78
  "eslint-plugin-security": "^4.2.0",
80
- "husky": "^9.1.7",
81
79
  "yamllint-js": "^0.2.4"
82
80
  },
83
81
  "dependencies": {
package/src/data/apis.js CHANGED
@@ -1,26 +1,3 @@
1
- export const deprecatedAPIs = new Map([
2
- ['chrome.pageAction.show', 'chrome.action.enable'],
3
- ['chrome.pageAction.hide', 'chrome.action.disable'],
4
- ['chrome.pageAction.setIcon', 'chrome.action.setIcon'],
5
- ['chrome.pageAction.setTitle', 'chrome.action.setTitle'],
6
- ['chrome.pageAction.setPopup', 'chrome.action.setPopup'],
7
- ['chrome.pageAction.getTitle', 'chrome.action.getTitle'],
8
- ['chrome.pageAction.getPopup', 'chrome.action.getPopup'],
9
- ['chrome.browserAction', 'chrome.action'],
10
- ['chrome.pageAction', 'chrome.action'],
11
- ['chrome.extension.getBackgroundPage', 'chrome.runtime.getContexts'],
12
- ['chrome.extension.getURL', 'chrome.runtime.getURL'],
13
- ['chrome.extension.sendRequest', 'chrome.runtime.sendMessage'],
14
- ['chrome.extension.onRequest', 'chrome.runtime.onMessage'],
15
- ['chrome.extension.onRequestExternal', 'chrome.runtime.onMessageExternal'],
16
- ['chrome.extension.connect', 'chrome.runtime.connect'],
17
- ['chrome.extension.onConnect', 'chrome.runtime.onConnect'],
18
- ['chrome.extension.onConnectExternal', 'chrome.runtime.onConnectExternal'],
19
- ['chrome.tabs.executeScript', 'chrome.scripting.executeScript'],
20
- ['chrome.tabs.insertCSS', 'chrome.scripting.insertCSS'],
21
- ['chrome.tabs.removeCSS', 'chrome.scripting.removeCSS']
22
- ])
23
-
24
1
  export const apiPermissionMap = new Map([
25
2
  ['chrome.alarms', 'alarms'],
26
3
  ['chrome.bookmarks', 'bookmarks'],
@@ -33,6 +10,7 @@ export const apiPermissionMap = new Map([
33
10
  ['chrome.history', 'history'],
34
11
  ['chrome.idle', 'idle'],
35
12
  ['chrome.management', 'management'],
13
+ ['chrome.menus', 'menus'],
36
14
  ['chrome.notifications', 'notifications'],
37
15
  ['chrome.pageCapture', 'pageCapture'],
38
16
  ['chrome.privacy', 'privacy'],
@@ -41,13 +19,86 @@ export const apiPermissionMap = new Map([
41
19
  ['chrome.sessions', 'sessions'],
42
20
  ['chrome.storage', 'storage'],
43
21
  ['chrome.tabCapture', 'tabCapture'],
22
+ ['chrome.tabs.query', 'tabs'],
44
23
  ['chrome.topSites', 'topSites'],
45
24
  ['chrome.webRequest', 'webRequest']
46
25
  ])
47
26
 
27
+ export const deprecatedAPIs = new Map([
28
+ ['chrome.pageAction.show', 'chrome.action.enable'],
29
+ ['chrome.pageAction.hide', 'chrome.action.disable'],
30
+ ['chrome.pageAction.setIcon', 'chrome.action.setIcon'],
31
+ ['chrome.pageAction.setTitle', 'chrome.action.setTitle'],
32
+ ['chrome.pageAction.setPopup', 'chrome.action.setPopup'],
33
+ ['chrome.pageAction.getTitle', 'chrome.action.getTitle'],
34
+ ['chrome.pageAction.getPopup', 'chrome.action.getPopup'],
35
+ ['chrome.browserAction', 'chrome.action'],
36
+ ['chrome.pageAction', 'chrome.action'],
37
+ ['chrome.extension.getBackgroundPage', 'chrome.runtime.getContexts'],
38
+ ['chrome.extension.getURL', 'chrome.runtime.getURL'],
39
+ ['chrome.extension.sendRequest', 'chrome.runtime.sendMessage'],
40
+ ['chrome.extension.onRequest', 'chrome.runtime.onMessage'],
41
+ ['chrome.extension.onRequestExternal', 'chrome.runtime.onMessageExternal'],
42
+ ['chrome.extension.connect', 'chrome.runtime.connect'],
43
+ ['chrome.extension.onConnect', 'chrome.runtime.onConnect'],
44
+ ['chrome.extension.onConnectExternal', 'chrome.runtime.onConnectExternal'],
45
+ ['chrome.tabs.executeScript', 'chrome.scripting.executeScript'],
46
+ ['chrome.tabs.insertCSS', 'chrome.scripting.insertCSS'],
47
+ ['chrome.tabs.removeCSS', 'chrome.scripting.removeCSS']
48
+ ])
49
+
48
50
  export const externalMessagingListeners =
49
51
  new Set(['chrome.runtime.onMessageExternal', 'chrome.runtime.onConnectExternal'])
50
52
 
53
+ export const permlessAPIs = new Set([
54
+ // https://developer.chrome.com/docs/extensions/reference/api/management
55
+ 'chrome.management.getPermissionWarningsByManifest',
56
+ 'chrome.management.getSelf',
57
+ 'chrome.management.uninstallSelf',
58
+ // https://developer.chrome.com/docs/extensions/reference/api/runtime
59
+ 'chrome.runtime.connect',
60
+ 'chrome.runtime.getManifest',
61
+ 'chrome.runtime.getPlatformInfo',
62
+ 'chrome.runtime.getURL',
63
+ 'chrome.runtime.onBrowserUpdateAvailable', // eslint-disable-line no-secrets/no-secrets
64
+ 'chrome.runtime.onConnect',
65
+ 'chrome.runtime.onConnectExternal',
66
+ 'chrome.runtime.onInstalled',
67
+ 'chrome.runtime.onMessage',
68
+ 'chrome.runtime.onMessageExternal',
69
+ 'chrome.runtime.onStartup',
70
+ 'chrome.runtime.onSuspend',
71
+ 'chrome.runtime.onSuspendCanceled',
72
+ 'chrome.runtime.onUpdateAvailable',
73
+ 'chrome.runtime.openOptionsPage',
74
+ 'chrome.runtime.reload',
75
+ 'chrome.runtime.requestUpdateCheck',
76
+ 'chrome.runtime.restart',
77
+ 'chrome.runtime.restartAfterDelay',
78
+ 'chrome.runtime.sendMessage',
79
+ 'chrome.runtime.setUninstallURL',
80
+ // https://developer.chrome.com/docs/extensions/reference/api/tabs
81
+ 'chrome.tabs.captureVisibleTab',
82
+ 'chrome.tabs.connect',
83
+ 'chrome.tabs.create',
84
+ 'chrome.tabs.detectLanguage',
85
+ 'chrome.tabs.discard',
86
+ 'chrome.tabs.duplicate',
87
+ 'chrome.tabs.executeScript',
88
+ 'chrome.tabs.get',
89
+ 'chrome.tabs.getCurrent',
90
+ 'chrome.tabs.goBack',
91
+ 'chrome.tabs.goForward',
92
+ 'chrome.tabs.highlight',
93
+ 'chrome.tabs.insertCSS',
94
+ 'chrome.tabs.move',
95
+ 'chrome.tabs.reload',
96
+ 'chrome.tabs.remove',
97
+ 'chrome.tabs.removeCSS',
98
+ 'chrome.tabs.sendMessage',
99
+ 'chrome.tabs.update'
100
+ ])
101
+
51
102
  export function findDeprecated(api) {
52
103
  for (const [deprecated, replacement] of deprecatedAPIs)
53
104
  if (api == deprecated || api.startsWith(`${deprecated}.`))
@@ -56,6 +107,7 @@ export function findDeprecated(api) {
56
107
  }
57
108
 
58
109
  export function findPermission(api) {
110
+ if (permlessAPIs.has(api)) return null
59
111
  for (const [prefix, permission] of apiPermissionMap)
60
112
  if (api == prefix || api.startsWith(`${prefix}.`))
61
113
  return permission
@@ -1,10 +1,10 @@
1
1
  // Delete a line to end support
2
2
 
3
- export const bundles = { // ruleId: [bundleName, note]
4
- 'no-invalid-permissions': ['recommended'],
5
- 'require-mv3': ['recommended'],
6
- 'valid-mv3-schema': ['recommended'],
7
- 'valid-version-field': ['recommended']
3
+ export const bundles = {
4
+ 'no-invalid-permissions': ['recommended'], // 2026-10-5 announced future removal
5
+ 'require-mv3': ['recommended'], // 2026-10-5 announced future removal
6
+ 'valid-mv3-schema': ['recommended'], // 2026-10-5 announced future removal
7
+ 'valid-version-field': ['recommended'] // 2026-10-5 announced future removal
8
8
  }
9
9
 
10
10
  export default { bundles }
@@ -3,6 +3,20 @@ import globals from 'globals'
3
3
  const serviceWorkerGlobals = new Set(Object.keys(globals.serviceworker || {}))
4
4
  export const domGlobals = new Set(Object.keys(globals.browser).filter(name => !serviceWorkerGlobals.has(name)))
5
5
 
6
+ export const ffDataCollectionRequired = new Set([
7
+ 'none', 'authenticationInfo', 'bookmarksInfo', 'browsingActivity', 'financialAndPaymentInfo', 'healthInfo',
8
+ 'locationInfo', 'personalCommunications', 'personallyIdentifyingInfo', 'searchTerms', 'websiteActivity',
9
+ 'websiteContent'
10
+ ])
11
+
12
+ export const ffDataCollectionOptional = new Set([
13
+ 'authenticationInfo', 'bookmarksInfo', 'browsingActivity', 'financialAndPaymentInfo', 'healthInfo', 'locationInfo',
14
+ 'personalCommunications', 'personallyIdentifyingInfo', 'searchTerms', 'technicalAndInteraction', 'websiteActivity',
15
+ 'websiteContent'
16
+ ])
17
+
18
+ export const ffMV3bgKeys = new Set(['scripts', 'page'])
19
+
6
20
  export const forbiddenCSPdirectives = [
7
21
  `'unsafe-eval'`, `'unsafe-inline'`, `'unsafe-hashes'`, `'wasm-unsafe-eval'`, 'http://', 'https://']
8
22
 
@@ -11,19 +25,16 @@ export const manifestKeyOrder = [
11
25
  'manifest_version', 'name', 'version',
12
26
  // CWS-required
13
27
  'description', 'icons',
14
- // optional (alphabetical, per docs)
15
- 'action', 'background', 'chrome_settings_overrides', 'chrome_url_overrides',
16
- 'commands', 'content_scripts', 'content_security_policy',
17
- 'cross_origin_embedder_policy', 'cross_origin_opener_policy',
18
- 'declarative_net_request', 'default_locale', 'devtools_page', 'export',
19
- 'externally_connectable', 'homepage_url', 'host_permissions', 'import',
20
- 'incognito', 'key', 'mime_types_handler', 'minimum_chrome_version',
21
- 'oauth2', 'omnibox', 'optional_host_permissions', 'optional_permissions',
22
- 'options_page', 'options_ui', 'permissions', 'requirements', 'sandbox',
23
- 'short_name', 'side_panel', 'storage', 'tts_engine', 'update_url',
24
- 'version_name', 'web_accessible_resources'
28
+ // optional
29
+ 'action', 'author', 'background', 'browser_specific_settings', 'chrome_settings_overrides', 'chrome_url_overrides',
30
+ 'commands', 'content_scripts', 'content_security_policy', 'cross_origin_embedder_policy',
31
+ 'cross_origin_opener_policy', 'dark_theme', 'declarative_net_request', 'default_locale', 'developer',
32
+ 'devtools_page', 'dictionaries', 'export', 'externally_connectable', 'homepage_url', 'host_permissions', 'import',
33
+ 'incognito', 'key', 'mime_types_handler', 'minimum_chrome_version', 'oauth2', 'omnibox',
34
+ 'optional_host_permissions', 'optional_permissions', 'options_page', 'options_ui', 'permissions',
35
+ 'protocol_handlers', 'requirements', 'sandbox', 'short_name', 'sidebar_action', 'side_panel', 'storage', 'theme',
36
+ 'theme_experiment', 'tts_engine', 'update_url', 'version_name', 'web_accessible_resources'
25
37
  ]
26
-
27
38
  export const mv2OnlyKeys = new Set(['browser_action', 'page_action'])
28
39
 
29
40
  export const remoteCodePatterns = [
@@ -16,7 +16,7 @@ export const validPermissions = new Set([
16
16
  'downloads', 'downloads.open', 'downloads.ui', 'enterprise.deviceAttributes', 'enterprise.hardwarePlatform',
17
17
  'enterprise.networkingAttributes', 'enterprise.platformKeys', 'favicon', 'fileBrowserHandler',
18
18
  'fileSystemProvider', 'fontSettings', 'gcm', 'geolocation', 'history', 'identity', 'identity.email', 'idle',
19
- 'loginState', 'management', 'nativeMessaging', 'notifications', 'offscreen', 'pageCapture', 'platformKeys',
19
+ 'loginState', 'management', 'menus', 'nativeMessaging', 'notifications', 'offscreen', 'pageCapture', 'platformKeys',
20
20
  'power', 'printerProvider', 'printing', 'printingMetrics', 'privacy', 'processes', 'proxy', 'publicSuffix',
21
21
  'readingList', 'runtime', 'scripting', 'search', 'sessions', 'sidePanel', 'storage', 'system.cpu',
22
22
  'system.display', 'system.memory', 'system.storage', 'tabCapture', 'tabGroups', 'tabs', 'topSites', 'tts',
package/src/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { bundles as oldBundles } from './data/deprecations.js'
2
2
 
3
3
  import manifestKeyOrder from './rules/manifest-key-order.js'
4
+ import manifestSpacing from './rules/manifest-spacing.js'
4
5
  import noContentStyleLeak from './rules/no-content-style-leak.js'
5
6
  import noDeprecatedAPIs from './rules/no-deprecated-apis.js'
6
7
  import noDuplicateManifestKeys from './rules/no-duplicate-manifest-keys.js'
@@ -18,11 +19,12 @@ import validMV3schema from './rules/valid-mv3-schema.js'
18
19
  import validServiceWorker from './rules/valid-service-worker.js'
19
20
  import validVersionField from './rules/valid-version-field.js'
20
21
 
21
- import { bundleRules, deprecatedFrom, initBundleDeprecation } from './utils.js'
22
+ import { bundleRules, initBundleDeprecation } from './utils.js'
22
23
 
23
24
  const plugin = {
24
25
  rules: {
25
26
  'manifest-key-order': manifestKeyOrder,
27
+ 'manifest-spacing': manifestSpacing,
26
28
  'no-content-style-leak': noContentStyleLeak,
27
29
  'no-deprecated-apis': noDeprecatedAPIs,
28
30
  'no-duplicate-manifest-keys': noDuplicateManifestKeys,
@@ -48,8 +50,7 @@ for (const [ruleId, rule] of Object.entries(plugin.rules))
48
50
  plugin.configs = {
49
51
  recommended: {
50
52
  plugins: { 'chrome-extensions': plugin },
51
- rules: bundleRules(plugin.rules, rule => rule.meta.docs.recommended,
52
- 'chrome-extensions', deprecatedFrom(oldBundles, 'recommended'))
53
+ rules: bundleRules(plugin.rules, rule => rule.meta.docs.recommended, 'chrome-extensions')
53
54
  },
54
55
  strict: {
55
56
  plugins: { 'chrome-extensions': plugin },
@@ -61,13 +62,11 @@ plugin.configs = {
61
62
  },
62
63
  security: {
63
64
  plugins: { 'chrome-extensions': plugin },
64
- rules: bundleRules(plugin.rules, rule => rule.meta.docs.security,
65
- 'chrome-extensions', deprecatedFrom(oldBundles, 'security'))
65
+ rules: bundleRules(plugin.rules, rule => rule.meta.docs.security, 'chrome-extensions')
66
66
  },
67
67
  manifest: {
68
68
  plugins: { 'chrome-extensions': plugin },
69
- rules: bundleRules(plugin.rules, rule => rule.meta.docs.manifest,
70
- 'chrome-extensions', deprecatedFrom(oldBundles, 'manifest'))
69
+ rules: bundleRules(plugin.rules, rule => rule.meta.docs.manifest, 'chrome-extensions')
71
70
  }
72
71
  }
73
72
 
@@ -1,5 +1,4 @@
1
1
  import { manifestKeyOrder } from '../data/manifest.js'
2
-
3
2
  import { getFilename } from '../utils.js'
4
3
 
5
4
  const re = { manifestFilename: /manifest\.json$/ }
@@ -0,0 +1,167 @@
1
+ import { getFilename } from '../utils.js'
2
+
3
+ const re = { manifestFilename: /manifest\.json$/, leadingWhitespace: /^[ \t]+/ },
4
+ brackets = { open: new Set(['LBrace', 'LBracket']), close: new Set(['RBrace', 'RBracket']) }
5
+
6
+ export default {
7
+ meta: {
8
+ type: 'layout',
9
+ docs: {
10
+ description: 'Enforces consistent indentation and spacing in `manifest.json`',
11
+ category: 'Stylistic Issues',
12
+ recommended: false,
13
+ manifest: true
14
+ },
15
+ fixable: 'whitespace',
16
+ schema: [{
17
+ type: 'object',
18
+ properties: {
19
+ spaces: {
20
+ type: 'number',
21
+ description: 'Number of spaces per indent level (default 2)'
22
+ },
23
+ checkColons: {
24
+ type: 'boolean',
25
+ description: 'Enforce no spaces before and 1 space after `:` in key/value pairs (default true)'
26
+ },
27
+ checkCommas: {
28
+ type: 'boolean',
29
+ description: 'Enforce no spaces before and 1 space after `,` in inline objects (default true)'
30
+ },
31
+ checkBraces: {
32
+ type: 'boolean',
33
+ description: 'Enforce 1 space after `{` and before `}` in inline objects (default true)'
34
+ },
35
+ checkBrackets: {
36
+ type: 'boolean',
37
+ description: 'Enforce no spaces after `[` or before `]` in inline arrays (default true)'
38
+ }
39
+ },
40
+ additionalProperties: false
41
+ }],
42
+ defaultOptions: [{ spaces: 2, checkColons: true, checkCommas: true,
43
+ checkBraces: true, checkBrackets: true }],
44
+ messages: {
45
+ wrongIndent: 'Expected {{expected}} spaces of indentation (found {{found}}).',
46
+ tabFound: 'Expected spaces for indentation, found tabs.',
47
+ spaceBeforeColon: 'Expected no spaces before `:`.',
48
+ spaceAfterColon: 'Expected 1 space after `:`.',
49
+ spaceBeforeComma: 'Expected no spaces before `,`.',
50
+ spaceAfterComma: 'Expected 1 space after `,`.',
51
+ spaceAfterBrace: 'Expected 1 space after `{`.',
52
+ spaceBeforeBrace: 'Expected 1 space before `}`.',
53
+ spaceInEmptyBraces: 'Expected no spaces in empty `{}`.',
54
+ spaceAfterBracket: 'Expected no spaces after `[`.',
55
+ spaceBeforeBracket: 'Expected no spaces before `]`.'
56
+ }
57
+ },
58
+
59
+ create(context) {
60
+ if (!re.manifestFilename.test(getFilename(context))) return {}
61
+ const opts = context.options[0] || {},
62
+ spaces = opts.spaces ?? 2,
63
+ checkColons = opts.checkColons ?? true,
64
+ checkCommas = opts.checkCommas ?? true,
65
+ checkBraces = opts.checkBraces ?? true,
66
+ checkBrackets = opts.checkBrackets ?? true
67
+ return {
68
+ 'Document'(document) {
69
+ const sourceCode = context.sourceCode,
70
+ text = sourceCode.getText(),
71
+ lines = text.split('\n'),
72
+ tokens = document.tokens || [],
73
+ lineOffsets = [0]
74
+ for (const line of lines)
75
+ lineOffsets.push(lineOffsets.at(-1) + line.length +1)
76
+ let depth = 0, lastLine = 0, prevToken = null
77
+ for (const token of tokens) { // check indentation
78
+ const startLine = token.loc.start.line
79
+ if (startLine != lastLine) {
80
+ const indent = lines[startLine -1].match(re.leadingWhitespace)?.[0] || '',
81
+ effectiveDepth = brackets.close.has(token.type) ? Math.max(0, depth -1) : depth,
82
+ expected = ' '.repeat(effectiveDepth * spaces)
83
+ if (indent != expected) {
84
+ const lineStart = lineOffsets[startLine -1],
85
+ isTab = indent.includes('\t')
86
+ context.report({
87
+ loc: { line: startLine, column: 1 }, messageId: isTab ? 'tabFound' : 'wrongIndent',
88
+ data: { expected: effectiveDepth * spaces, found: indent.length },
89
+ fix(fixer) {
90
+ return fixer.replaceTextRange([lineStart, lineStart + indent.length], expected) }
91
+ })
92
+ }
93
+ lastLine = startLine
94
+ }
95
+ const isOpen = brackets.open.has(token.type),
96
+ isClose = brackets.close.has(token.type),
97
+ wasOpen = prevToken && brackets.open.has(prevToken.type),
98
+ wasClose = prevToken && brackets.close.has(prevToken.type),
99
+ isSameLine = prevToken && prevToken.loc.end.line == token.loc.start.line,
100
+ isContiguous = prevToken && prevToken.range[1] == token.range[0]
101
+ if (isOpen && !(wasOpen && isSameLine)) depth += 1
102
+ else if (isClose && !(wasClose && isContiguous)) depth -= 1
103
+ prevToken = token
104
+ }
105
+ for (let t = 0 ; t < tokens.length ; t++) { // check inline spacing
106
+ const token = tokens[t], // eslint-disable-line security/detect-object-injection
107
+ type = token.type,
108
+ prev = tokens[t -1], next = tokens[t +1]
109
+ if (next) {
110
+ const between = text.slice(token.range[1], next.range[0]),
111
+ inline = !between.includes('\n'),
112
+ isColon = type == 'Colon', isComma = type == 'Comma'
113
+ if (checkColons && isColon && inline && between != ' ')
114
+ context.report({
115
+ loc: token.loc.end, messageId: 'spaceAfterColon',
116
+ fix(fixer) { return fixer.replaceTextRange([token.range[1], next.range[0]], ' ') }
117
+ })
118
+ if (checkCommas && isComma && inline && between != ' ')
119
+ context.report({
120
+ loc: token.loc.end, messageId: 'spaceAfterComma',
121
+ fix(fixer) { return fixer.replaceTextRange([token.range[1], next.range[0]], ' ') }
122
+ })
123
+ if (checkBraces && type == 'LBrace' && inline) {
124
+ const empty = next.type == 'RBrace', want = empty ? '' : ' '
125
+ if (between != want)
126
+ context.report({
127
+ loc: token.loc.end,
128
+ messageId: empty ? 'spaceInEmptyBraces' : 'spaceAfterBrace',
129
+ fix(fixer) { return fixer.replaceTextRange([token.range[1], next.range[0]], want) }
130
+ })
131
+ }
132
+ if (checkBrackets && type == 'LBracket' && inline && between != '')
133
+ context.report({
134
+ loc: token.loc.end, messageId: 'spaceAfterBracket',
135
+ fix(fixer) { return fixer.replaceTextRange([token.range[1], next.range[0]], '') }
136
+ })
137
+ }
138
+ if (prev) {
139
+ const between = text.slice(prev.range[1], token.range[0]),
140
+ inline = !between.includes('\n'),
141
+ isColon = type == 'Colon', isComma = type == 'Comma'
142
+ if (checkColons && isColon && inline && between != '')
143
+ context.report({
144
+ loc: token.loc.start, messageId: 'spaceBeforeColon',
145
+ fix(fixer) { return fixer.replaceTextRange([prev.range[1], token.range[0]], '') }
146
+ })
147
+ if (checkCommas && isComma && inline && between != '')
148
+ context.report({
149
+ loc: token.loc.start, messageId: 'spaceBeforeComma',
150
+ fix(fixer) { return fixer.replaceTextRange([prev.range[1], token.range[0]], '') }
151
+ })
152
+ if (checkBraces && type == 'RBrace' && prev.type != 'LBrace' && inline && between != ' ')
153
+ context.report({
154
+ loc: token.loc.start, messageId: 'spaceBeforeBrace',
155
+ fix(fixer) { return fixer.replaceTextRange([prev.range[1], token.range[0]], ' ') }
156
+ })
157
+ if (checkBrackets && type == 'RBracket' && prev.type != 'LBracket' && inline && between != '')
158
+ context.report({
159
+ loc: token.loc.start, messageId: 'spaceBeforeBracket',
160
+ fix(fixer) { return fixer.replaceTextRange([prev.range[1], token.range[0]], '') }
161
+ })
162
+ }
163
+ }
164
+ }
165
+ }
166
+ }
167
+ }
@@ -1,5 +1,14 @@
1
1
  import { findPermission } from '../data/apis.js'
2
- import { readManifest, getMemberPath } from '../utils.js'
2
+ import { getMemberPath, readManifest } from '../utils.js'
3
+
4
+ function queriesActiveTab(node) {
5
+ const arg = node.arguments[0] ; if (arg?.type != 'ObjectExpression') return false
6
+ return arg.properties.some(prop => {
7
+ if (prop.type != 'Property') return false
8
+ const key = prop.key?.name ?? prop.key?.value
9
+ return key == 'active' && prop.value?.type == 'Literal' && prop.value.value === true
10
+ })
11
+ }
3
12
 
4
13
  export default {
5
14
  meta: {
@@ -11,19 +20,23 @@ export default {
11
20
  },
12
21
  schema: [],
13
22
  messages: {
14
- missingPermission: '`{{api}}` requires the "{{permission}}" manifest permission.'
23
+ missingPermission: '`{{api}}` requires the "{{permission}}" manifest permission.',
24
+ missingActiveTab: '`{{api}}` requires the "activeTab" manifest permission.'
15
25
  }
16
26
  },
17
27
 
18
28
  create(context) {
19
29
  const manifest = readManifest(context) ; if (!manifest) return {}
20
30
  const declared = new Set([...(manifest.permissions || []), ...(manifest.optional_permissions || [])])
31
+ if (declared.has('activeTab')) declared.add('tabs')
21
32
  return {
22
33
  CallExpression(node) {
23
34
  if (node.callee.type != 'MemberExpression') return
24
35
  const api = getMemberPath(node.callee) ; if (!api) return
25
36
  const permission = findPermission(api) ; if (!permission || declared.has(permission)) return
26
- context.report({ node, messageId: 'missingPermission', data: { api, permission }})
37
+ const messageId = permission == 'tabs' && queriesActiveTab(node)
38
+ ? 'missingActiveTab' : 'missingPermission'
39
+ context.report({ node, messageId, data: { api, permission }})
27
40
  }
28
41
  }
29
42
  }
@@ -1,5 +1,4 @@
1
1
  import { remoteCodePatterns } from '../data/manifest.js'
2
-
3
2
  import { getMemberPath } from '../utils.js'
4
3
 
5
4
  const re = { url: /^https?:\/\// }
@@ -1,6 +1,8 @@
1
+ import { apiPermissionMap, findPermission } from '../data/apis.js'
2
+
1
3
  import fs from 'node:fs'
2
4
  import path from 'node:path'
3
- import { apiPermissionMap, findPermission } from '../data/apis.js'
5
+
4
6
  import { findExtensionRoot, getFilename, readManifest } from '../utils.js'
5
7
 
6
8
  const re = { manifestFilename: /manifest\.json$/ }
@@ -1,4 +1,5 @@
1
- import { mv2OnlyKeys, forbiddenCSPdirectives } from '../data/manifest.js'
1
+ import { mv2OnlyKeys, forbiddenCSPdirectives, ffMV3bgKeys, ffDataCollectionRequired, ffDataCollectionOptional }
2
+ from '../data/manifest.js'
2
3
 
3
4
  import { getFilename, readManifest } from '../utils.js'
4
5
 
@@ -15,6 +16,7 @@ export default {
15
16
  },
16
17
  schema: [],
17
18
  messages: {
19
+ invalidDataCollectionValue: '`{{value}}` is not a valid data collection permission value for {{kind}}.',
18
20
  mv2Key: '`{{key}}` is not allowed in MV3. {{reason}}.',
19
21
  unsafeCSP: 'MV3 CSP must not include `{{directive}}`.',
20
22
  wrongCSPType: '`content_security_policy` must be an object in MV3, not a string.'
@@ -23,11 +25,11 @@ export default {
23
25
 
24
26
  create(context) {
25
27
  if (re.manifestFilename.test(getFilename(context))) { // lint it directly
26
- const ancestors = [], pendingReports = []
27
- let isMV3 = false
28
- const keyOf = n => {
29
- const k = n?.name || n?.key
30
- return k?.value ?? k?.name
28
+ const ancestors = [], pendingReports = [], deferredMV2 = []
29
+ let isMV3 = false, isFF = false
30
+ function keyOf(node) {
31
+ const keyNode = node?.name || node?.key
32
+ return keyNode?.value ?? keyNode?.name
31
33
  }
32
34
  return {
33
35
  '*'(node) {
@@ -36,18 +38,23 @@ export default {
36
38
  const isTopLevel = ancestors.length == 2
37
39
  && ancestors[0].type == 'Document'
38
40
  && ancestors[1].type == 'Object'
39
- const inBackground = ancestors.some(a => keyOf(a) == 'background')
40
- const inCSP = ancestors.some(a => keyOf(a) == 'content_security_policy')
41
+ const inBackground = ancestors.some(ancestor => keyOf(ancestor) == 'background')
42
+ const inCSP = ancestors.some(ancestor => keyOf(ancestor) == 'content_security_policy')
43
+ const inDataCollection = ancestors.some(ancestor =>
44
+ keyOf(ancestor) == 'data_collection_permissions')
41
45
  if (keyName == 'manifest_version' && node.value?.value == 3) isMV3 = true
46
+ if (isTopLevel && keyName == 'browser_specific_settings') isFF = true
42
47
  if (isTopLevel && mv2OnlyKeys.has(keyName))
43
48
  pendingReports.push({ node, messageId: 'mv2Key',
44
49
  data: { key: keyName, reason: 'MV2-only key' }})
45
- if (inBackground && keyName == 'scripts')
46
- pendingReports.push({ node, messageId: 'mv2Key',
47
- data: { key: 'background.scripts', reason: 'use background.service_worker' }})
50
+ if (inBackground && ffMV3bgKeys.has(keyName))
51
+ deferredMV2.push({ node, messageId: 'mv2Key',
52
+ data: { key: `background.${keyName}`,
53
+ reason: 'Use background.service_worker instead' }})
48
54
  if (inBackground && keyName == 'persistent')
49
55
  pendingReports.push({ node, messageId: 'mv2Key',
50
- data: { key: 'background.persistent', reason: 'MV3 service workers are never persistent' }})
56
+ data: { key: 'background.persistent',
57
+ reason: 'MV3 service workers are never persistent' }})
51
58
  if (isTopLevel && keyName == 'content_security_policy' && node.value?.type == 'String')
52
59
  pendingReports.push({ node: node.value, messageId: 'wrongCSPType' })
53
60
  if (inCSP && keyName == 'extension_pages' && typeof node.value?.value == 'string')
@@ -55,6 +62,17 @@ export default {
55
62
  if (node.value.value.includes(directive))
56
63
  pendingReports.push({ node: node.value, messageId: 'unsafeCSP',
57
64
  data: { directive }})
65
+ if (inDataCollection && (keyName == 'required' || keyName == 'optional')
66
+ && Array.isArray(node.value?.elements)
67
+ ) for (const el of node.value.elements) {
68
+ const value = typeof el.value == 'string' ? el.value : el.value?.value
69
+ if (typeof value == 'string') {
70
+ const set = keyName == 'required' ? ffDataCollectionRequired : ffDataCollectionOptional
71
+ if (!set.has(value))
72
+ pendingReports.push({ node: el, messageId: 'invalidDataCollectionValue',
73
+ data: { value, kind: keyName }})
74
+ }
75
+ }
58
76
  }
59
77
  ancestors.push(node)
60
78
  },
@@ -62,20 +80,23 @@ export default {
62
80
  ancestors.pop()
63
81
  if (node.type == 'Document') {
64
82
  if (!isMV3) return
65
- for (const r of pendingReports) context.report(r)
83
+ if (!isFF) pendingReports.push(...deferredMV2)
84
+ for (const report of pendingReports) context.report(report)
66
85
  }
67
86
  }
68
87
  }
69
88
  }
70
89
  const manifest = readManifest(context) ; if (!manifest || manifest.manifest_version != 3) return {}
90
+ const isFF = manifest.browser_specific_settings != null
71
91
  return {
72
- Program(node) {
92
+ Program(node) { /* eslint-disable security/detect-object-injection */
73
93
  for (const key of Object.keys(manifest))
74
94
  if (mv2OnlyKeys.has(key))
75
95
  context.report({ node, messageId: 'mv2Key', data: { key, reason: 'MV2-only key' }})
76
- if (manifest.background?.scripts)
77
- context.report({ node, messageId: 'mv2Key',
78
- data: { key: 'background.scripts', reason: 'use background.service_worker' }})
96
+ for (const key of ffMV3bgKeys)
97
+ if (manifest.background?.[key] && !isFF)
98
+ context.report({ node, messageId: 'mv2Key',
99
+ data: { key: `background.${key}`, reason: 'Use background.service_worker instead' }})
79
100
  if (manifest.background?.persistent != null)
80
101
  context.report({ node, messageId: 'mv2Key',
81
102
  data: { key: 'background.persistent', reason: 'MV3 service workers are never persistent' }})
@@ -87,7 +108,16 @@ export default {
87
108
  for (const directive of forbiddenCSPdirectives)
88
109
  if (cspPages.includes(directive))
89
110
  context.report({ node, messageId: 'unsafeCSP', data: { directive }})
90
- }
111
+ if (isFF) { // validate data collection permissions
112
+ const dcp = manifest.browser_specific_settings?.gecko?.data_collection_permissions
113
+ for (const [kind, set] of [['required', ffDataCollectionRequired], ['optional', ffDataCollectionOptional]])
114
+ if (Array.isArray(dcp?.[kind]))
115
+ for (const value of dcp[kind])
116
+ if (!set.has(value))
117
+ context.report({ node, messageId: 'invalidDataCollectionValue',
118
+ data: { value, kind }})
119
+ }
120
+ } /* eslint-enable security/detect-object-injection */
91
121
  }
92
122
  }
93
123
  }
@@ -1,5 +1,5 @@
1
1
  import { domGlobals, serviceWorkerKeepaliveMinMS } from '../data/manifest.js'
2
- import { readManifest, isServiceWorkerFile, isInsideNestedFunction } from '../utils.js'
2
+ import { isServiceWorkerFile, isInsideNestedFunction, readManifest } from '../utils.js'
3
3
 
4
4
  export default {
5
5
  meta: {
@@ -30,13 +30,18 @@ export default {
30
30
  && node.arguments[1]?.type == 'Literal'
31
31
  && typeof node.arguments[1].value == 'number'
32
32
  && node.arguments[1].value < serviceWorkerKeepaliveMinMS
33
+ && !isInsideNestedFunction(node)
33
34
  ) context.report({ node, messageId: 'keepaliveTimer', data: { fn: node.callee.name }})
34
35
  },
35
36
  Identifier(node) {
36
37
  if (node.parent?.type == 'MemberExpression' && node.parent.property === node) return
37
38
  if (node.parent?.type == 'Property' && node.parent.key === node && !node.parent.computed) return
38
- if (domGlobals.has(node.name))
39
- context.report({ node, messageId: 'domInSW', data: { global: node.name }})
39
+ if (!domGlobals.has(node.name)) return
40
+ for (let scope = context.sourceCode.getScope(node) ;
41
+ scope && scope.type != 'global' ;
42
+ scope = scope.upper
43
+ ) if (scope.set.has(node.name)) return // shadowed by a local binding
44
+ context.report({ node, messageId: 'domInSW', data: { global: node.name }})
40
45
  }
41
46
  }
42
47
  }
package/src/utils.js CHANGED
@@ -4,24 +4,21 @@ import path from 'node:path'
4
4
  const re = { chromeAPI: /\bchrome(?:\.\w+)+/g }, // eslint-disable-line security/detect-unsafe-regex
5
5
  manifestCache = new Map()
6
6
 
7
- export function bundleRules(rules, predicate, pluginName, removals = []) {
7
+ export function bundleRules(rules, predicate, pluginName) {
8
8
  return Object.fromEntries(Object.entries(rules)
9
- .filter(([ruleId, rule]) => predicate(rule) || removals.includes(ruleId))
9
+ .filter(([, rule]) => predicate(rule))
10
10
  .map(([ruleId]) => [`${pluginName}/${ruleId}`, 'error']))
11
11
  }
12
12
 
13
- export function deprecatedFrom(bundles, bundleName) {
14
- const ruleIds = []
15
- for (const [ruleId, [bundle]] of Object.entries(bundles))
16
- if (bundle === bundleName) ruleIds.push(ruleId)
17
- return ruleIds
18
- }
19
-
20
- export function initBundleDeprecation(plugin, [ruleId, [bundleName, note]], pluginName) {
21
- if (!process.env.DEBUG_MODE) return
22
- if (plugin.rules[ruleId]) // eslint-disable-line security/detect-object-injection
23
- console.warn(`[${pluginName}] '${ruleId}' will be removed from '${
24
- bundleName}' in a future major release${note ? ` (${note})` : ''}`)
13
+ export function initBundleDeprecation(plugin, [ruleId, bundleNames], pluginName) {
14
+ for (const bundleName of bundleNames) {
15
+ /* eslint-disable security/detect-object-injection */
16
+ if (plugin.configs[bundleName])
17
+ plugin.configs[bundleName].rules[`${pluginName}/${ruleId}`] = 'error'
18
+ /* eslint-enable security/detect-object-injection */
19
+ if (process.env.DEBUG_MODE)
20
+ console.warn(`[${pluginName}] '${ruleId}' will be removed from '${bundleName}' in a future major release`)
21
+ }
25
22
  }
26
23
 
27
24
  export function findExtensionRoot(context) {