eslint-plugin-chrome-extensions 2.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-chrome-extensions",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "ESLint rules for Chrome extensions",
5
5
  "type": "module",
6
6
  "engines": {
@@ -57,14 +57,14 @@
57
57
  "chromium"
58
58
  ],
59
59
  "peerDependencies": {
60
- "eslint": ">=9.6.0",
61
- "@eslint/json": ">=0.1.0"
60
+ "@eslint/json": ">=0.1.0",
61
+ "eslint": ">=9.6.0"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@e18e/eslint-plugin": "^0.8.1",
65
65
  "@eslint/js": "^10.0.1",
66
66
  "@eslint/json": "^2.1.1",
67
- "@eslint/markdown": "^8.0.3",
67
+ "@eslint/markdown": "^7.5.1",
68
68
  "@stylistic/eslint-plugin": "^5.10.0",
69
69
  "eslint": "^10.12.0",
70
70
  "eslint-plugin-constructors": "^1.4.2",
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'],
@@ -47,7 +24,33 @@ export const apiPermissionMap = new Map([
47
24
  ['chrome.webRequest', 'webRequest']
48
25
  ])
49
26
 
50
- export const permissionlessAPIs = new Set([
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
+
50
+ export const externalMessagingListeners =
51
+ new Set(['chrome.runtime.onMessageExternal', 'chrome.runtime.onConnectExternal'])
52
+
53
+ export const permlessAPIs = new Set([
51
54
  // https://developer.chrome.com/docs/extensions/reference/api/management
52
55
  'chrome.management.getPermissionWarningsByManifest',
53
56
  'chrome.management.getSelf',
@@ -96,9 +99,6 @@ export const permissionlessAPIs = new Set([
96
99
  'chrome.tabs.update'
97
100
  ])
98
101
 
99
- export const externalMessagingListeners =
100
- new Set(['chrome.runtime.onMessageExternal', 'chrome.runtime.onConnectExternal'])
101
-
102
102
  export function findDeprecated(api) {
103
103
  for (const [deprecated, replacement] of deprecatedAPIs)
104
104
  if (api == deprecated || api.startsWith(`${deprecated}.`))
@@ -107,7 +107,7 @@ export function findDeprecated(api) {
107
107
  }
108
108
 
109
109
  export function findPermission(api) {
110
- if (permissionlessAPIs.has(api)) return null
110
+ if (permlessAPIs.has(api)) return null
111
111
  for (const [prefix, permission] of apiPermissionMap)
112
112
  if (api == prefix || api.startsWith(`${prefix}.`))
113
113
  return permission
@@ -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
 
@@ -21,20 +35,6 @@ export const manifestKeyOrder = [
21
35
  'protocol_handlers', 'requirements', 'sandbox', 'short_name', 'sidebar_action', 'side_panel', 'storage', 'theme',
22
36
  'theme_experiment', 'tts_engine', 'update_url', 'version_name', 'web_accessible_resources'
23
37
  ]
24
-
25
- export const ffDataCollectionRequired = new Set([
26
- 'none', 'authenticationInfo', 'bookmarksInfo', 'browsingActivity', 'financialAndPaymentInfo', 'healthInfo',
27
- 'locationInfo', 'personalCommunications', 'personallyIdentifyingInfo', 'searchTerms', 'websiteActivity',
28
- 'websiteContent'
29
- ])
30
-
31
- export const ffDataCollectionOptional = new Set([
32
- 'authenticationInfo', 'bookmarksInfo', 'browsingActivity', 'financialAndPaymentInfo', 'healthInfo', 'locationInfo',
33
- 'personalCommunications', 'personallyIdentifyingInfo', 'searchTerms', 'technicalAndInteraction', 'websiteActivity',
34
- 'websiteContent'
35
- ])
36
-
37
- export const ffMV3bgKeys = new Set(['scripts', 'page'])
38
38
  export const mv2OnlyKeys = new Set(['browser_action', 'page_action'])
39
39
 
40
40
  export const remoteCodePatterns = [
@@ -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$/ }
@@ -73,7 +73,7 @@ export default {
73
73
  lineOffsets = [0]
74
74
  for (const line of lines)
75
75
  lineOffsets.push(lineOffsets.at(-1) + line.length +1)
76
- let depth = 0, lastLine = 0
76
+ let depth = 0, lastLine = 0, prevToken = null
77
77
  for (const token of tokens) { // check indentation
78
78
  const startLine = token.loc.start.line
79
79
  if (startLine != lastLine) {
@@ -92,8 +92,15 @@ export default {
92
92
  }
93
93
  lastLine = startLine
94
94
  }
95
- if (brackets.open.has(token.type)) depth += 1
96
- else if (brackets.close.has(token.type)) depth -= 1
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
97
104
  }
98
105
  for (let t = 0 ; t < tokens.length ; t++) { // check inline spacing
99
106
  const token = tokens[t], // eslint-disable-line security/detect-object-injection
@@ -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,7 +1,8 @@
1
- import { getFilename, readManifest } from '../utils.js'
2
1
  import { mv2OnlyKeys, forbiddenCSPdirectives, ffMV3bgKeys, ffDataCollectionRequired, ffDataCollectionOptional }
3
2
  from '../data/manifest.js'
4
3
 
4
+ import { getFilename, readManifest } from '../utils.js'
5
+
5
6
  const re = { manifestFilename: /manifest\.json$/ }
6
7
 
7
8
  export default {
@@ -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: {