eslint-plugin-chrome-extensions 2.2.0 → 2.4.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 +646 -33
- package/docs/LICENSE.md +1 -1
- package/package.json +47 -36
- package/src/data/apis.js +28 -29
- package/src/data/i18n.js +71 -0
- package/src/data/license.js +27 -0
- package/src/data/manifest.js +14 -13
- package/src/data/text-parser.js +11 -0
- package/src/index.js +53 -1
- package/src/rules/manifest-key-order.js +1 -3
- package/src/rules/manifest-spacing.js +12 -4
- package/src/rules/no-content-style-leak.js +5 -2
- package/src/rules/no-invalid-permissions.js +2 -2
- package/src/rules/no-missing-default-locale.js +91 -0
- package/src/rules/no-missing-i18n-keys.js +72 -0
- package/src/rules/no-missing-permissions.js +16 -3
- package/src/rules/no-missing-web-accessible-resources.js +4 -4
- package/src/rules/no-remote-code.js +5 -6
- package/src/rules/no-unrestricted-host-permissions.js +5 -4
- package/src/rules/no-unused-i18n-keys.js +54 -0
- package/src/rules/no-unused-permissions.js +3 -2
- package/src/rules/require-license-file.js +27 -0
- package/src/rules/require-short-name-when-useful.js +71 -0
- package/src/rules/valid-filenames.js +151 -0
- package/src/rules/valid-license-file.js +59 -0
- package/src/rules/valid-mv3-schema.js +2 -1
- package/src/rules/valid-service-worker.js +3 -2
- package/src/rules/valid-url-fields.js +136 -0
- package/src/rules/valid-version-field.js +4 -2
- package/src/utils.js +12 -13
package/docs/LICENSE.md
CHANGED
package/package.json
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-chrome-extensions",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "ESLint rules for Chrome extensions",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
},
|
|
5
|
+
"keywords": [
|
|
6
|
+
"eslint",
|
|
7
|
+
"eslint-plugin",
|
|
8
|
+
"eslintplugin",
|
|
9
|
+
"chrome-extensions",
|
|
10
|
+
"chromium",
|
|
11
|
+
"security"
|
|
12
|
+
],
|
|
14
13
|
"homepage": "https://codeberg.org/adamlui/eslint-plugin-chrome-extensions/#readme",
|
|
15
14
|
"bugs": {
|
|
16
15
|
"url": "https://codeberg.org/adamlui/eslint-plugin-chrome-extensions/issues",
|
|
17
16
|
"email": "support@adamlui.com"
|
|
18
17
|
},
|
|
19
|
-
"
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://codeberg.org/adamlui/eslint-plugin-chrome-extensions.git"
|
|
21
|
+
},
|
|
20
22
|
"funding": [
|
|
21
23
|
{
|
|
22
24
|
"type": "ko-fi",
|
|
@@ -27,44 +29,46 @@
|
|
|
27
29
|
"url": "https://paypal.me/adamlui"
|
|
28
30
|
}
|
|
29
31
|
],
|
|
30
|
-
"
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"author": {
|
|
34
|
+
"name": "Adam Lui",
|
|
35
|
+
"email": "adam@kudoai.org",
|
|
36
|
+
"url": "https://codeberg.org/adamlui"
|
|
37
|
+
},
|
|
38
|
+
"type": "module",
|
|
39
|
+
"exports": {
|
|
40
|
+
".": {
|
|
41
|
+
"types": "./src/index.d.ts",
|
|
42
|
+
"default": "./src/index.js"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"main": "./src/index.js",
|
|
46
|
+
"types": "./src/index.d.ts",
|
|
31
47
|
"files": [
|
|
32
48
|
"docs/",
|
|
33
49
|
"src/"
|
|
34
50
|
],
|
|
35
|
-
"types": "src/index.d.ts",
|
|
36
51
|
"scripts": {
|
|
52
|
+
"bump:feat": "npm run bump:minor",
|
|
53
|
+
"bump:major": "bash utils/bump.sh major",
|
|
54
|
+
"bump:minor": "bash utils/bump.sh minor",
|
|
55
|
+
"bump:patch": "bash utils/bump.sh patch",
|
|
56
|
+
"demo": "node demo/run.js",
|
|
37
57
|
"lint": "eslint . --cache && yamllint .",
|
|
38
58
|
"lint:all": "eslint . && yamllint .",
|
|
39
59
|
"lint:fix": "eslint . --fix --cache && yamllint .",
|
|
40
60
|
"lint:fix-all": "eslint . --fix && yamllint .",
|
|
41
|
-
"test": "node tests/run.js"
|
|
42
|
-
"demo": "node demo/run.js",
|
|
43
|
-
"bump:patch": "bash utils/bump.sh patch",
|
|
44
|
-
"bump:minor": "bash utils/bump.sh minor",
|
|
45
|
-
"bump:feat": "npm run bump:minor",
|
|
46
|
-
"bump:major": "bash utils/bump.sh major"
|
|
47
|
-
},
|
|
48
|
-
"repository": {
|
|
49
|
-
"type": "git",
|
|
50
|
-
"url": "git+https://codeberg.org/adamlui/eslint-plugin-chrome-extensions.git"
|
|
61
|
+
"test": "node tests/run.js"
|
|
51
62
|
},
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"eslintplugin",
|
|
56
|
-
"chrome-extensions",
|
|
57
|
-
"chromium"
|
|
58
|
-
],
|
|
59
|
-
"peerDependencies": {
|
|
60
|
-
"eslint": ">=9.6.0",
|
|
61
|
-
"@eslint/json": ">=0.1.0"
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"globals": "^17.13.0",
|
|
65
|
+
"spdx-license-ids": "^3.0.24"
|
|
62
66
|
},
|
|
63
67
|
"devDependencies": {
|
|
64
68
|
"@e18e/eslint-plugin": "^0.8.1",
|
|
65
69
|
"@eslint/js": "^10.0.1",
|
|
66
70
|
"@eslint/json": "^2.1.1",
|
|
67
|
-
"@eslint/markdown": "^
|
|
71
|
+
"@eslint/markdown": "^7.5.1",
|
|
68
72
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
69
73
|
"eslint": "^10.12.0",
|
|
70
74
|
"eslint-plugin-constructors": "^1.4.2",
|
|
@@ -73,12 +77,19 @@
|
|
|
73
77
|
"eslint-plugin-n": "^18.4.1",
|
|
74
78
|
"eslint-plugin-no-secrets": "^2.3.3",
|
|
75
79
|
"eslint-plugin-no-unsanitized": "^4.1.5",
|
|
80
|
+
"eslint-plugin-package-json": "^1.10.1",
|
|
76
81
|
"eslint-plugin-promise": "^7.3.0",
|
|
77
82
|
"eslint-plugin-regexp": "^3.3.1",
|
|
78
83
|
"eslint-plugin-security": "^4.2.0",
|
|
79
84
|
"yamllint-js": "^0.2.4"
|
|
80
85
|
},
|
|
81
|
-
"
|
|
82
|
-
"
|
|
86
|
+
"peerDependencies": {
|
|
87
|
+
"@eslint/css": ">=0.5.0",
|
|
88
|
+
"@eslint/json": ">=0.1.0",
|
|
89
|
+
"@html-eslint/eslint-plugin": ">=0.42.0",
|
|
90
|
+
"eslint": ">=9.6.0"
|
|
91
|
+
},
|
|
92
|
+
"engines": {
|
|
93
|
+
"node": ">=18.18.0"
|
|
83
94
|
}
|
|
84
95
|
}
|
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,32 @@ 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 externalMsgListeners = new Set(['chrome.runtime.onMessageExternal', 'chrome.runtime.onConnectExternal'])
|
|
51
|
+
|
|
52
|
+
export const permlessAPIs = new Set([
|
|
51
53
|
// https://developer.chrome.com/docs/extensions/reference/api/management
|
|
52
54
|
'chrome.management.getPermissionWarningsByManifest',
|
|
53
55
|
'chrome.management.getSelf',
|
|
@@ -96,9 +98,6 @@ export const permissionlessAPIs = new Set([
|
|
|
96
98
|
'chrome.tabs.update'
|
|
97
99
|
])
|
|
98
100
|
|
|
99
|
-
export const externalMessagingListeners =
|
|
100
|
-
new Set(['chrome.runtime.onMessageExternal', 'chrome.runtime.onConnectExternal'])
|
|
101
|
-
|
|
102
101
|
export function findDeprecated(api) {
|
|
103
102
|
for (const [deprecated, replacement] of deprecatedAPIs)
|
|
104
103
|
if (api == deprecated || api.startsWith(`${deprecated}.`))
|
|
@@ -107,11 +106,11 @@ export function findDeprecated(api) {
|
|
|
107
106
|
}
|
|
108
107
|
|
|
109
108
|
export function findPermission(api) {
|
|
110
|
-
if (
|
|
109
|
+
if (permlessAPIs.has(api)) return null
|
|
111
110
|
for (const [prefix, permission] of apiPermissionMap)
|
|
112
111
|
if (api == prefix || api.startsWith(`${prefix}.`))
|
|
113
112
|
return permission
|
|
114
113
|
return null
|
|
115
114
|
}
|
|
116
115
|
|
|
117
|
-
export function isExternalMessagingListener(api) { return
|
|
116
|
+
export function isExternalMessagingListener(api) { return externalMsgListeners.has(api) }
|
package/src/data/i18n.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/* eslint-disable security/detect-non-literal-fs-filename -- lint-time project inspection only */
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
export const re = {
|
|
7
|
+
getMsgLiteral: /\b(?:chrome|browser)\.i18n\.getMessage\(\s*['"]([^'"]+)['"]/g,
|
|
8
|
+
getMsgTemplate: /\b(?:chrome|browser)\.i18n\.getMessage\(\s*`([^`]*)`/g,
|
|
9
|
+
msgPlaceholder: /__MSG_([\w@]+)__/g,
|
|
10
|
+
specialChars: /[.*+?^${}()|[\]\\]/g,
|
|
11
|
+
templateExpr: /\$\{[^}]*\}/g
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function collectUsedKeys(root, scanExtensions) {
|
|
15
|
+
const used = new Set(),
|
|
16
|
+
patterns = [],
|
|
17
|
+
exts = new Set(scanExtensions),
|
|
18
|
+
stack = [root]
|
|
19
|
+
while (stack.length) {
|
|
20
|
+
const dir = stack.pop()
|
|
21
|
+
let entries
|
|
22
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }) } catch { continue }
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
if (entry.name.startsWith('.') || entry.name == 'node_modules') continue
|
|
25
|
+
const full = path.join(dir, entry.name)
|
|
26
|
+
if (entry.isDirectory()) { stack.push(full) ; continue }
|
|
27
|
+
const ext = path.extname(entry.name).slice(1).toLowerCase()
|
|
28
|
+
if (!exts.has(ext)) continue
|
|
29
|
+
let text
|
|
30
|
+
try { text = fs.readFileSync(full, 'utf8') } catch { continue }
|
|
31
|
+
for (const match of text.matchAll(re.msgPlaceholder))
|
|
32
|
+
if (!isPredefined(match[1])) used.add(match[1])
|
|
33
|
+
for (const match of text.matchAll(re.getMsgLiteral))
|
|
34
|
+
used.add(match[1])
|
|
35
|
+
for (const match of text.matchAll(re.getMsgTemplate)) {
|
|
36
|
+
const body = match[1]
|
|
37
|
+
if (body.includes('${')) patterns.push(templateToPattern(body))
|
|
38
|
+
else used.add(body)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { used, patterns }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const defaultScanExtensions = [
|
|
46
|
+
'cjs', 'css', 'htm', 'html', 'js', 'json', 'jsx', 'md', 'mjs', 'svg', 'ts', 'tsx', 'txt']
|
|
47
|
+
|
|
48
|
+
export function findExtensionRoot(startPath) {
|
|
49
|
+
let dir = path.dirname(startPath)
|
|
50
|
+
while (dir != path.dirname(dir)) {
|
|
51
|
+
if (fs.existsSync(path.join(dir, 'manifest.json'))) return dir
|
|
52
|
+
dir = path.dirname(dir)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isPredefined(key) { return key.startsWith('@@') }
|
|
57
|
+
|
|
58
|
+
export function readDefaultLocaleMessages(manifest, root) {
|
|
59
|
+
const locale = manifest?.default_locale
|
|
60
|
+
if (typeof locale != 'string') return null
|
|
61
|
+
const filePath = path.join(root, '_locales', locale, 'messages.json')
|
|
62
|
+
let messages
|
|
63
|
+
try { messages = JSON.parse(fs.readFileSync(filePath, 'utf8')) } catch { return null }
|
|
64
|
+
if (!messages || typeof messages != 'object' || Array.isArray(messages)) return null
|
|
65
|
+
return { locale, messages }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function templateToPattern(body) {
|
|
69
|
+
const parts = body.split(re.templateExpr).map(part => part.replace(re.specialChars, '\\$&'))
|
|
70
|
+
return new RegExp(`^${parts.join('.*')}$`) // eslint-disable-line security/detect-non-literal-regexp
|
|
71
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
export const re = {
|
|
5
|
+
licenseFilename: // e.g. LICENSE.md, LICENSE-MIT, MIT-LICENSE.md, COPYING.LESSER, LICENSE-2.0.txt
|
|
6
|
+
/^(?:[\w.-]*-)?(?:LICEN[SC]E|COPYING)(?:[-.][\w.-]+)?$/i, // eslint-disable-line security/detect-unsafe-regex
|
|
7
|
+
spdxTag: /SPDX-License-Identifier:\s*([\w.+-]+)/i
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const skipDirs = new Set(['node_modules'])
|
|
11
|
+
|
|
12
|
+
export function findLicenseFile(root) {
|
|
13
|
+
const queue = [root]
|
|
14
|
+
while (queue.length) {
|
|
15
|
+
const dir = queue.shift(), entries = tryReaddir(dir)
|
|
16
|
+
if (!entries) continue
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
if (entry.name.startsWith('.') || skipDirs.has(entry.name)) continue
|
|
19
|
+
if (entry.isFile() && re.licenseFilename.test(entry.name)) return path.join(dir, entry.name)
|
|
20
|
+
if (entry.isDirectory()) queue.push(path.join(dir, entry.name))
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function tryReaddir(dir) { // eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
27
|
+
try { return fs.readdirSync(dir, { withFileTypes: true }) } catch { return null }}
|
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
|
|
|
@@ -22,19 +36,6 @@ export const manifestKeyOrder = [
|
|
|
22
36
|
'theme_experiment', 'tts_engine', 'update_url', 'version_name', 'web_accessible_resources'
|
|
23
37
|
]
|
|
24
38
|
|
|
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
39
|
export const mv2OnlyKeys = new Set(['browser_action', 'page_action'])
|
|
39
40
|
|
|
40
41
|
export const remoteCodePatterns = [
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { bundles as oldBundles } from './data/deprecations.js'
|
|
2
2
|
|
|
3
|
+
import css from '@eslint/css'
|
|
4
|
+
import html from '@html-eslint/eslint-plugin'
|
|
5
|
+
import json from '@eslint/json'
|
|
6
|
+
|
|
3
7
|
import manifestKeyOrder from './rules/manifest-key-order.js'
|
|
4
8
|
import manifestSpacing from './rules/manifest-spacing.js'
|
|
5
9
|
import noContentStyleLeak from './rules/no-content-style-leak.js'
|
|
@@ -8,18 +12,27 @@ import noDuplicateManifestKeys from './rules/no-duplicate-manifest-keys.js'
|
|
|
8
12
|
import noInsecureFetch from './rules/no-insecure-fetch.js'
|
|
9
13
|
import noInsecureMessaging from './rules/no-insecure-messaging.js'
|
|
10
14
|
import noInvalidPermissions from './rules/no-invalid-permissions.js'
|
|
15
|
+
import noMissingDefaultLocale from './rules/no-missing-default-locale.js'
|
|
16
|
+
import noMissingI18nKeys from './rules/no-missing-i18n-keys.js'
|
|
11
17
|
import noMissingPermissions from './rules/no-missing-permissions.js'
|
|
12
18
|
import noMissingWebAccessibleResources from './rules/no-missing-web-accessible-resources.js'
|
|
13
19
|
import noRemoteCode from './rules/no-remote-code.js'
|
|
14
20
|
import noUnrestrictedHostPermissions from './rules/no-unrestricted-host-permissions.js'
|
|
21
|
+
import noUnusedI18nKeys from './rules/no-unused-i18n-keys.js'
|
|
15
22
|
import noUnusedPermissions from './rules/no-unused-permissions.js'
|
|
23
|
+
import requireLicenseFile from './rules/require-license-file.js'
|
|
16
24
|
import requireMV3 from './rules/require-mv3.js'
|
|
25
|
+
import requireShortNameWhenUseful from './rules/require-short-name-when-useful.js'
|
|
26
|
+
import validFilenames from './rules/valid-filenames.js'
|
|
27
|
+
import validLicenseFile from './rules/valid-license-file.js'
|
|
17
28
|
import validManifestIcons from './rules/valid-manifest-icons.js'
|
|
18
29
|
import validMV3schema from './rules/valid-mv3-schema.js'
|
|
19
30
|
import validServiceWorker from './rules/valid-service-worker.js'
|
|
31
|
+
import validURLfields from './rules/valid-url-fields.js'
|
|
20
32
|
import validVersionField from './rules/valid-version-field.js'
|
|
21
33
|
|
|
22
34
|
import { bundleRules, initBundleDeprecation } from './utils.js'
|
|
35
|
+
import { textParser } from './data/text-parser.js'
|
|
23
36
|
|
|
24
37
|
const plugin = {
|
|
25
38
|
rules: {
|
|
@@ -31,15 +44,23 @@ const plugin = {
|
|
|
31
44
|
'no-insecure-fetch': noInsecureFetch,
|
|
32
45
|
'no-insecure-messaging': noInsecureMessaging,
|
|
33
46
|
'no-invalid-permissions': noInvalidPermissions,
|
|
47
|
+
'no-missing-default-locale': noMissingDefaultLocale,
|
|
48
|
+
'no-missing-i18n-keys': noMissingI18nKeys,
|
|
34
49
|
'no-missing-permissions': noMissingPermissions,
|
|
35
50
|
'no-missing-web-accessible-resources': noMissingWebAccessibleResources,
|
|
36
51
|
'no-remote-code': noRemoteCode,
|
|
37
52
|
'no-unrestricted-host-permissions': noUnrestrictedHostPermissions,
|
|
53
|
+
'no-unused-i18n-keys': noUnusedI18nKeys,
|
|
38
54
|
'no-unused-permissions': noUnusedPermissions,
|
|
39
55
|
'require-mv3': requireMV3,
|
|
56
|
+
'require-license-file': requireLicenseFile,
|
|
57
|
+
'require-short-name-when-useful': requireShortNameWhenUseful,
|
|
58
|
+
'valid-filenames': validFilenames,
|
|
59
|
+
'valid-license-file': validLicenseFile,
|
|
40
60
|
'valid-manifest-icons': validManifestIcons,
|
|
41
61
|
'valid-mv3-schema': validMV3schema,
|
|
42
62
|
'valid-service-worker': validServiceWorker,
|
|
63
|
+
'valid-url-fields': validURLfields,
|
|
43
64
|
'valid-version-field': validVersionField
|
|
44
65
|
}
|
|
45
66
|
}
|
|
@@ -65,8 +86,39 @@ plugin.configs = {
|
|
|
65
86
|
rules: bundleRules(plugin.rules, rule => rule.meta.docs.security, 'chrome-extensions')
|
|
66
87
|
},
|
|
67
88
|
manifest: {
|
|
68
|
-
|
|
89
|
+
language: 'json/json',
|
|
90
|
+
plugins: { 'chrome-extensions': plugin, json },
|
|
69
91
|
rules: bundleRules(plugin.rules, rule => rule.meta.docs.manifest, 'chrome-extensions')
|
|
92
|
+
},
|
|
93
|
+
license: {
|
|
94
|
+
languageOptions: { parser: textParser },
|
|
95
|
+
plugins: { 'chrome-extensions': plugin },
|
|
96
|
+
rules: bundleRules(plugin.rules, rule => rule.meta.docs.license, 'chrome-extensions')
|
|
97
|
+
},
|
|
98
|
+
css: {
|
|
99
|
+
language: 'css/css',
|
|
100
|
+
plugins: { 'chrome-extensions': plugin, css },
|
|
101
|
+
rules: bundleRules(plugin.rules, rule => rule.meta.docs.css, 'chrome-extensions')
|
|
102
|
+
},
|
|
103
|
+
html: {
|
|
104
|
+
language: 'html/html',
|
|
105
|
+
plugins: { 'chrome-extensions': plugin, html },
|
|
106
|
+
rules: bundleRules(plugin.rules, rule => rule.meta.docs.html, 'chrome-extensions')
|
|
107
|
+
},
|
|
108
|
+
json: {
|
|
109
|
+
language: 'json/json',
|
|
110
|
+
plugins: { 'chrome-extensions': plugin, json },
|
|
111
|
+
rules: bundleRules(plugin.rules, rule => rule.meta.docs.json, 'chrome-extensions')
|
|
112
|
+
},
|
|
113
|
+
json5: {
|
|
114
|
+
language: 'json/json5',
|
|
115
|
+
plugins: { 'chrome-extensions': plugin, json },
|
|
116
|
+
rules: bundleRules(plugin.rules, rule => rule.meta.docs.json, 'chrome-extensions')
|
|
117
|
+
},
|
|
118
|
+
jsonc: {
|
|
119
|
+
language: 'json/jsonc',
|
|
120
|
+
plugins: { 'chrome-extensions': plugin, json },
|
|
121
|
+
rules: bundleRules(plugin.rules, rule => rule.meta.docs.json, 'chrome-extensions')
|
|
70
122
|
}
|
|
71
123
|
}
|
|
72
124
|
|
|
@@ -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$/ }
|
|
@@ -15,8 +14,7 @@ function findViolation(members, keys) {
|
|
|
15
14
|
const key = keyOf(member)
|
|
16
15
|
return { member, key, rank: ranks.get(key) }
|
|
17
16
|
}).filter(item => item.rank !== undefined)
|
|
18
|
-
for (let i = 1; i < known.length; i++)
|
|
19
|
-
// eslint-disable-next-line security/detect-object-injection
|
|
17
|
+
for (let i = 1 ; i < known.length ; i++) // eslint-disable-next-line security/detect-object-injection
|
|
20
18
|
if (known[i -1].rank > known[i].rank) return [known[i -1], known[i]]
|
|
21
19
|
}
|
|
22
20
|
|
|
@@ -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
|
|
@@ -131,7 +138,8 @@ export default {
|
|
|
131
138
|
if (prev) {
|
|
132
139
|
const between = text.slice(prev.range[1], token.range[0]),
|
|
133
140
|
inline = !between.includes('\n'),
|
|
134
|
-
isColon = type == 'Colon',
|
|
141
|
+
isColon = type == 'Colon',
|
|
142
|
+
isComma = type == 'Comma'
|
|
135
143
|
if (checkColons && isColon && inline && between != '')
|
|
136
144
|
context.report({
|
|
137
145
|
loc: token.loc.start, messageId: 'spaceBeforeColon',
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readManifest, isContentScriptFile } from '../utils.js'
|
|
2
2
|
|
|
3
|
+
const re = { appendMethod: /^append(?:Child)?$/ }
|
|
4
|
+
|
|
3
5
|
export default {
|
|
4
6
|
meta: {
|
|
5
7
|
type: 'suggestion',
|
|
@@ -41,7 +43,8 @@ export default {
|
|
|
41
43
|
if (isObjectAssign(node) && isCreateStyle(node.arguments[0])) return true
|
|
42
44
|
if (isObjectAssign(node)
|
|
43
45
|
&& node.arguments[0]?.type == 'Identifier'
|
|
44
|
-
&& styleElems.has(node.arguments[0].name)
|
|
46
|
+
&& styleElems.has(node.arguments[0].name)
|
|
47
|
+
) return true
|
|
45
48
|
return false
|
|
46
49
|
}
|
|
47
50
|
return {
|
|
@@ -54,7 +57,7 @@ export default {
|
|
|
54
57
|
CallExpression(node) {
|
|
55
58
|
if (node.callee.type != 'MemberExpression') return
|
|
56
59
|
const method = node.callee.property.name
|
|
57
|
-
if (method
|
|
60
|
+
if (!re.appendMethod.test(method)) return
|
|
58
61
|
if (!isDocumentMember(node.callee.object)) return
|
|
59
62
|
for (const arg of node.arguments)
|
|
60
63
|
if (isStyleTarget(arg))
|
|
@@ -64,10 +64,10 @@ export default {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
const manifest = readManifest(context) ; if (!manifest) return {}
|
|
67
|
-
const
|
|
67
|
+
const allPermissions = [...(manifest.permissions || []), ...(manifest.optional_permissions || [])]
|
|
68
68
|
return {
|
|
69
69
|
'Program:exit'() {
|
|
70
|
-
for (const permission of
|
|
70
|
+
for (const permission of allPermissions) {
|
|
71
71
|
if (allowed.has(permission) || isValidPermission(permission, target)) continue
|
|
72
72
|
context.report({ loc: { line: 1, column: 0 },
|
|
73
73
|
messageId: 'invalidPermission', data: { permission, target }})
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/* eslint-disable security/detect-non-literal-fs-filename -- lint-time project inspection only */
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { findExtensionRoot, isPredefined } from '../data/i18n.js'
|
|
7
|
+
import { readManifest } from '../utils.js'
|
|
8
|
+
|
|
9
|
+
const re = { manifestFile: /manifest\.json$/ }
|
|
10
|
+
|
|
11
|
+
function readMessages(filePath) {
|
|
12
|
+
try {
|
|
13
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
14
|
+
return (data && typeof data == 'object' && !Array.isArray(data)) ? data : null
|
|
15
|
+
} catch { return null }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readLocaleDirs(localesDir) {
|
|
19
|
+
try {
|
|
20
|
+
return fs.readdirSync(localesDir, { withFileTypes: true })
|
|
21
|
+
.filter(entry => entry.isDirectory())
|
|
22
|
+
.map(entry => entry.name)
|
|
23
|
+
} catch { return [] }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default {
|
|
27
|
+
meta: {
|
|
28
|
+
type: 'problem',
|
|
29
|
+
docs: {
|
|
30
|
+
description: 'Ensure `default_locale` is valid and all `_locales` keys exist in it',
|
|
31
|
+
category: 'Possible Errors',
|
|
32
|
+
recommended: false,
|
|
33
|
+
manifest: true
|
|
34
|
+
},
|
|
35
|
+
schema: [{
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
ignoreKeys: {
|
|
39
|
+
type: 'array', items: { type: 'string' },
|
|
40
|
+
description: 'Keys to skip when checking the default locale'
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
additionalProperties: false
|
|
44
|
+
}],
|
|
45
|
+
defaultOptions: [{ ignoreKeys: [] }],
|
|
46
|
+
messages: {
|
|
47
|
+
missingDefaultLocale: '`_locales/` exists but `default_locale` is not declared.',
|
|
48
|
+
unexpectedDefaultLocale: '`default_locale` is declared but no `_locales/` directory exists.',
|
|
49
|
+
missingDefaultLocaleFile: '`_locales/{{locale}}/messages.json` does not exist.',
|
|
50
|
+
missingKeyInDefault: '`{{key}}` exists in `_locales/{{locale}}/messages.json`, '
|
|
51
|
+
+ 'but not in default locale `{{defaultLocale}}`.'
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
create(context) {
|
|
56
|
+
const filename = context.filename
|
|
57
|
+
if (!filename || !re.manifestFile.test(filename)) return {}
|
|
58
|
+
const manifest = readManifest(context) ; if (!manifest) return {}
|
|
59
|
+
const root = findExtensionRoot(filename) ; if (!root) return {}
|
|
60
|
+
const localesDir = path.join(root, '_locales'),
|
|
61
|
+
hasLocales = fs.existsSync(localesDir),
|
|
62
|
+
defaultLocale = manifest.default_locale,
|
|
63
|
+
opts = context.options[0] || {},
|
|
64
|
+
ignored = new Set(opts.ignoreKeys || []),
|
|
65
|
+
loc = { line: 1, column: 1 }
|
|
66
|
+
if (hasLocales && typeof defaultLocale != 'string') {
|
|
67
|
+
context.report({ loc, messageId: 'missingDefaultLocale' })
|
|
68
|
+
return {}
|
|
69
|
+
}
|
|
70
|
+
if (typeof defaultLocale == 'string' && !hasLocales) {
|
|
71
|
+
context.report({ loc, messageId: 'unexpectedDefaultLocale' })
|
|
72
|
+
return {}
|
|
73
|
+
}
|
|
74
|
+
if (typeof defaultLocale != 'string' || !hasLocales) return {}
|
|
75
|
+
const defaultMsgs = readMessages(path.join(localesDir, defaultLocale, 'messages.json'))
|
|
76
|
+
if (!defaultMsgs) {
|
|
77
|
+
context.report({ loc, messageId: 'missingDefaultLocaleFile', data: { locale: defaultLocale }})
|
|
78
|
+
return {}
|
|
79
|
+
}
|
|
80
|
+
for (const locale of readLocaleDirs(localesDir)) {
|
|
81
|
+
if (locale == defaultLocale) continue
|
|
82
|
+
const msgs = readMessages(path.join(localesDir, locale, 'messages.json')) ; if (!msgs) continue
|
|
83
|
+
for (const key of Object.keys(msgs)) {
|
|
84
|
+
if (isPredefined(key) || ignored.has(key)) continue
|
|
85
|
+
if (!(key in defaultMsgs))
|
|
86
|
+
context.report({ loc, messageId: 'missingKeyInDefault', data: { key, locale, defaultLocale }})
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {}
|
|
90
|
+
}
|
|
91
|
+
}
|