eslint-plugin-chrome-extensions 2.3.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +645 -34
- package/docs/LICENSE.md +1 -1
- package/package.json +49 -36
- package/src/data/apis.js +2 -3
- package/src/data/i18n.js +71 -0
- package/src/data/license.js +27 -0
- package/src/data/manifest.js +18 -0
- package/src/data/text-parser.js +11 -0
- package/src/index.js +53 -1
- package/src/rules/manifest-key-order.js +1 -2
- package/src/rules/manifest-spacing.js +2 -1
- package/src/rules/no-content-style-leak.js +5 -2
- package/src/rules/no-invalid-permissions.js +5 -5
- 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 +3 -3
- package/src/rules/no-missing-web-accessible-resources.js +4 -4
- package/src/rules/no-remote-code.js +5 -5
- 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 +6 -7
- 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 +152 -0
- package/src/rules/valid-license-file.js +59 -0
- package/src/rules/valid-mv3-schema.js +3 -3
- package/src/rules/valid-service-worker.js +6 -4
- package/src/rules/valid-url-fields.js +120 -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.1",
|
|
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,58 +29,69 @@
|
|
|
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"
|
|
61
|
+
"test": "node tests/run.js"
|
|
47
62
|
},
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
},
|
|
52
|
-
"keywords": [
|
|
53
|
-
"eslint",
|
|
54
|
-
"eslint-plugin",
|
|
55
|
-
"eslintplugin",
|
|
56
|
-
"chrome-extensions",
|
|
57
|
-
"chromium"
|
|
58
|
-
],
|
|
59
|
-
"peerDependencies": {
|
|
60
|
-
"@eslint/json": ">=0.1.0",
|
|
61
|
-
"eslint": ">=9.6.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",
|
|
69
|
+
"@eslint/css": "^2.0.0",
|
|
65
70
|
"@eslint/js": "^10.0.1",
|
|
66
71
|
"@eslint/json": "^2.1.1",
|
|
67
72
|
"@eslint/markdown": "^7.5.1",
|
|
73
|
+
"@html-eslint/eslint-plugin": "^0.66.1",
|
|
68
74
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
69
75
|
"eslint": "^10.12.0",
|
|
70
|
-
"eslint-plugin-constructors": "^1.4.
|
|
76
|
+
"eslint-plugin-constructors": "^1.4.3",
|
|
71
77
|
"eslint-plugin-eslint-plugin": "^7.6.2",
|
|
72
78
|
"eslint-plugin-import-x": "^4.17.1",
|
|
73
79
|
"eslint-plugin-n": "^18.4.1",
|
|
74
80
|
"eslint-plugin-no-secrets": "^2.3.3",
|
|
75
81
|
"eslint-plugin-no-unsanitized": "^4.1.5",
|
|
82
|
+
"eslint-plugin-package-json": "^1.10.1",
|
|
76
83
|
"eslint-plugin-promise": "^7.3.0",
|
|
77
84
|
"eslint-plugin-regexp": "^3.3.1",
|
|
78
85
|
"eslint-plugin-security": "^4.2.0",
|
|
79
86
|
"yamllint-js": "^0.2.4"
|
|
80
87
|
},
|
|
81
|
-
"
|
|
82
|
-
"
|
|
88
|
+
"peerDependencies": {
|
|
89
|
+
"@eslint/css": ">=0.5.0",
|
|
90
|
+
"@eslint/json": ">=0.1.0",
|
|
91
|
+
"@html-eslint/eslint-plugin": ">=0.42.0",
|
|
92
|
+
"eslint": ">=9.6.0"
|
|
93
|
+
},
|
|
94
|
+
"engines": {
|
|
95
|
+
"node": ">=18.18.0"
|
|
83
96
|
}
|
|
84
97
|
}
|
package/src/data/apis.js
CHANGED
|
@@ -47,8 +47,7 @@ export const deprecatedAPIs = new Map([
|
|
|
47
47
|
['chrome.tabs.removeCSS', 'chrome.scripting.removeCSS']
|
|
48
48
|
])
|
|
49
49
|
|
|
50
|
-
export const
|
|
51
|
-
new Set(['chrome.runtime.onMessageExternal', 'chrome.runtime.onConnectExternal'])
|
|
50
|
+
export const externalMsgListeners = new Set(['chrome.runtime.onMessageExternal', 'chrome.runtime.onConnectExternal'])
|
|
52
51
|
|
|
53
52
|
export const permlessAPIs = new Set([
|
|
54
53
|
// https://developer.chrome.com/docs/extensions/reference/api/management
|
|
@@ -114,4 +113,4 @@ export function findPermission(api) {
|
|
|
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
|
@@ -35,9 +35,27 @@ export const manifestKeyOrder = [
|
|
|
35
35
|
'protocol_handlers', 'requirements', 'sandbox', 'short_name', 'sidebar_action', 'side_panel', 'storage', 'theme',
|
|
36
36
|
'theme_experiment', 'tts_engine', 'update_url', 'version_name', 'web_accessible_resources'
|
|
37
37
|
]
|
|
38
|
+
|
|
39
|
+
export const matchPaths = new Set([
|
|
40
|
+
'content_scripts.matches', 'externally_connectable.matches',
|
|
41
|
+
'host_permissions', 'optional_host_permissions', 'web_accessible_resources.matches'
|
|
42
|
+
])
|
|
43
|
+
|
|
38
44
|
export const mv2OnlyKeys = new Set(['browser_action', 'page_action'])
|
|
39
45
|
|
|
40
46
|
export const remoteCodePatterns = [
|
|
41
47
|
'eval', 'new-function', 'string-timer', 'dynamic-import', 'remote-script-src', 'execute-script-url']
|
|
42
48
|
|
|
43
49
|
export const serviceWorkerKeepaliveMinMS = 30000
|
|
50
|
+
|
|
51
|
+
export const urlPaths = new Set([
|
|
52
|
+
'homepage_url', 'update_url', 'oauth2.scopes',
|
|
53
|
+
'chrome_settings_overrides.homepage',
|
|
54
|
+
'chrome_settings_overrides.search_provider.alternate_urls',
|
|
55
|
+
'chrome_settings_overrides.search_provider.favicon_url',
|
|
56
|
+
'chrome_settings_overrides.search_provider.image_url',
|
|
57
|
+
'chrome_settings_overrides.search_provider.instant_url',
|
|
58
|
+
'chrome_settings_overrides.search_provider.search_url',
|
|
59
|
+
'chrome_settings_overrides.search_provider.suggest_url',
|
|
60
|
+
'chrome_settings_overrides.startup_pages'
|
|
61
|
+
])
|
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
|
|
|
@@ -14,8 +14,7 @@ function findViolation(members, keys) {
|
|
|
14
14
|
const key = keyOf(member)
|
|
15
15
|
return { member, key, rank: ranks.get(key) }
|
|
16
16
|
}).filter(item => item.rank !== undefined)
|
|
17
|
-
for (let i = 1; i < known.length; i++)
|
|
18
|
-
// 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
|
|
19
18
|
if (known[i -1].rank > known[i].rank) return [known[i -1], known[i]]
|
|
20
19
|
}
|
|
21
20
|
|
|
@@ -138,7 +138,8 @@ export default {
|
|
|
138
138
|
if (prev) {
|
|
139
139
|
const between = text.slice(prev.range[1], token.range[0]),
|
|
140
140
|
inline = !between.includes('\n'),
|
|
141
|
-
isColon = type == 'Colon',
|
|
141
|
+
isColon = type == 'Colon',
|
|
142
|
+
isComma = type == 'Comma'
|
|
142
143
|
if (checkColons && isColon && inline && between != '')
|
|
143
144
|
context.report({
|
|
144
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))
|
|
@@ -56,18 +56,18 @@ export default {
|
|
|
56
56
|
const keyName = keyNode.value ?? keyNode.name
|
|
57
57
|
if (keyName != 'permissions' && keyName != 'optional_permissions') return
|
|
58
58
|
const arrNode = node.value ; if (arrNode?.type != 'Array') return
|
|
59
|
-
const
|
|
60
|
-
for (const
|
|
61
|
-
const valueNode =
|
|
59
|
+
const elems = arrNode.elements || arrNode.items || []
|
|
60
|
+
for (const elem of elems) {
|
|
61
|
+
const valueNode = elem.value || elem
|
|
62
62
|
checkPermission(valueNode, valueNode?.value)
|
|
63
63
|
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { getMemberPath, readManifest } from '../utils.js'
|
|
2
|
+
import { findExtensionRoot, isPredefined, readDefaultLocaleMessages, re as i18nRe } from '../data/i18n.js'
|
|
3
|
+
|
|
4
|
+
const re = { file: { js: /\.(cjs|js|jsx|mjs|ts|tsx)$/i, manifest: /manifest\.json$/, text: /\.(css|htm|html)$/i }}
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'problem',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Flag i18n keys referenced in code but missing from `_locales/<default_locale>/messages.json`',
|
|
11
|
+
category: 'Possible Errors',
|
|
12
|
+
recommended: true,
|
|
13
|
+
manifest: true,
|
|
14
|
+
css: true,
|
|
15
|
+
html: true,
|
|
16
|
+
json: true
|
|
17
|
+
},
|
|
18
|
+
schema: [{
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
ignoreKeys: {
|
|
22
|
+
type: 'array', items: { type: 'string' },
|
|
23
|
+
description: 'Keys to skip (e.g. `["myKey"]`)'
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
additionalProperties: false
|
|
27
|
+
}],
|
|
28
|
+
defaultOptions: [{ ignoreKeys: [] }],
|
|
29
|
+
messages: {
|
|
30
|
+
missingKey: '`{{key}}` is not in `_locales/{{locale}}/messages.json`.'
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
create(context) {
|
|
35
|
+
const filename = context.filename
|
|
36
|
+
if (!filename || filename == '<input>') return {}
|
|
37
|
+
const manifest = readManifest(context) ; if (!manifest?.default_locale) return {}
|
|
38
|
+
const root = findExtensionRoot(filename) ; if (!root) return {}
|
|
39
|
+
const i18n = readDefaultLocaleMessages(manifest, root) ; if (!i18n) return {}
|
|
40
|
+
const opts = context.options[0] || {},
|
|
41
|
+
ignored = new Set(opts.ignoreKeys || []),
|
|
42
|
+
isMissing = key => !isPredefined(key) && !ignored.has(key) && !(key in i18n.messages),
|
|
43
|
+
loc = { line: 1, column: 1 }
|
|
44
|
+
if (re.file.js.test(filename))
|
|
45
|
+
return {
|
|
46
|
+
CallExpression(node) {
|
|
47
|
+
if (node.callee.type != 'MemberExpression') return
|
|
48
|
+
const path = getMemberPath(node.callee)
|
|
49
|
+
if (path != 'chrome.i18n.getMessage' && path != 'browser.i18n.getMessage') return
|
|
50
|
+
const arg = node.arguments[0] ; if (!arg) return
|
|
51
|
+
let key
|
|
52
|
+
if (arg.type == 'Literal' && typeof arg.value == 'string') key = arg.value
|
|
53
|
+
else if (arg.type == 'TemplateLiteral' && arg.expressions.length == 0)
|
|
54
|
+
key = arg.quasis[0].value.cooked
|
|
55
|
+
if (key && isMissing(key))
|
|
56
|
+
context.report({ loc, messageId: 'missingKey', data: { key, locale: i18n.locale }})
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!re.file.text.test(filename) && !re.file.manifest.test(filename)) return {}
|
|
60
|
+
const seen = new Set()
|
|
61
|
+
const scanPlaceholders = () => {
|
|
62
|
+
for (const match of context.sourceCode.text.matchAll(i18nRe.msgPlaceholder)) {
|
|
63
|
+
const key = match[1]
|
|
64
|
+
if (seen.has(key)) continue
|
|
65
|
+
seen.add(key)
|
|
66
|
+
if (isMissing(key))
|
|
67
|
+
context.report({ loc, messageId: 'missingKey', data: { key, locale: i18n.locale }})
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { 'Document': scanPlaceholders, 'Program': scanPlaceholders }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -28,14 +28,14 @@ export default {
|
|
|
28
28
|
create(context) {
|
|
29
29
|
const manifest = readManifest(context) ; if (!manifest) return {}
|
|
30
30
|
const declared = new Set([...(manifest.permissions || []), ...(manifest.optional_permissions || [])])
|
|
31
|
-
if (declared.has('activeTab')) declared.add('tabs')
|
|
32
31
|
return {
|
|
33
32
|
CallExpression(node) {
|
|
34
33
|
if (node.callee.type != 'MemberExpression') return
|
|
35
34
|
const api = getMemberPath(node.callee) ; if (!api) return
|
|
36
35
|
const permission = findPermission(api) ; if (!permission || declared.has(permission)) return
|
|
37
|
-
const
|
|
38
|
-
|
|
36
|
+
const isActiveTabQuery = permission == 'tabs' && queriesActiveTab(node)
|
|
37
|
+
if (isActiveTabQuery && declared.has('activeTab')) return
|
|
38
|
+
const messageId = `missing${ isActiveTabQuery ? 'ActiveTab' : 'Permission' }`
|
|
39
39
|
context.report({ node, messageId, data: { api, permission }})
|
|
40
40
|
}
|
|
41
41
|
}
|