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 +4 -4
- package/src/data/apis.js +28 -28
- package/src/data/manifest.js +14 -14
- package/src/rules/manifest-key-order.js +0 -1
- package/src/rules/manifest-spacing.js +10 -3
- package/src/rules/no-missing-permissions.js +16 -3
- package/src/rules/no-remote-code.js +0 -1
- package/src/rules/no-unused-permissions.js +3 -1
- package/src/rules/valid-mv3-schema.js +2 -1
- package/src/rules/valid-service-worker.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-chrome-extensions",
|
|
3
|
-
"version": "2.
|
|
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": ">=
|
|
61
|
-
"
|
|
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": "^
|
|
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
|
|
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 (
|
|
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
|
package/src/data/manifest.js
CHANGED
|
@@ -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 = [
|
|
@@ -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
|
-
|
|
96
|
-
|
|
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 {
|
|
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
|
-
|
|
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,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
|
-
|
|
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 {
|
|
2
|
+
import { isServiceWorkerFile, isInsideNestedFunction, readManifest } from '../utils.js'
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
5
|
meta: {
|