eslint-plugin-chrome-extensions 2.4.2 → 2.6.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 +272 -115
- package/package.json +6 -8
- package/src/data/apis.js +166 -115
- package/src/data/deprecations.js +5 -7
- package/src/data/i18n.js +19 -69
- package/src/data/manifest.js +61 -57
- package/src/data/permissions.js +23 -30
- package/src/data/regex.js +10 -0
- package/src/index.js +7 -124
- package/src/rules/manifest-key-order.js +8 -8
- package/src/rules/manifest-spacing.js +5 -5
- package/src/rules/no-content-style-leak.js +6 -4
- package/src/rules/no-deprecated-apis.js +4 -4
- package/src/rules/no-duplicate-manifest-keys.js +3 -4
- package/src/rules/no-insecure-messaging.js +4 -4
- package/src/rules/no-invalid-locales.js +89 -0
- package/src/rules/no-invalid-permissions.js +10 -13
- package/src/rules/no-missing-default-locale.js +44 -21
- package/src/rules/no-missing-i18n-keys.js +24 -25
- package/src/rules/no-missing-permissions.js +7 -6
- package/src/rules/no-missing-web-accessible-resources.js +7 -6
- package/src/rules/no-remote-code.js +12 -12
- package/src/rules/no-sensitive-permissions.js +75 -0
- package/src/rules/no-unrestricted-host-permissions.js +35 -19
- package/src/rules/no-unused-i18n-keys.js +11 -10
- package/src/rules/no-unused-permissions.js +31 -23
- package/src/rules/require-license-file.js +4 -5
- package/src/rules/require-mv3.js +5 -6
- package/src/rules/require-short-name-when-useful.js +4 -3
- package/src/rules/valid-filenames.js +3 -10
- package/src/rules/valid-license-file.js +18 -11
- package/src/rules/valid-manifest-icons.js +35 -6
- package/src/rules/valid-mv3-schema.js +22 -22
- package/src/rules/valid-service-worker.js +34 -13
- package/src/rules/valid-url-fields.js +49 -41
- package/src/rules/valid-version-field.js +5 -9
- package/src/utils/extension.js +52 -33
- package/src/utils/i18n.js +58 -0
- package/src/utils/init.js +107 -0
- package/src/utils/license.js +56 -0
- package/src/utils/node.js +20 -19
- package/src/utils/pkg.js +12 -10
- package/src/utils/rules.js +6 -4
- package/src/utils/text.js +13 -0
- package/src/data/license.js +0 -28
- package/src/data/text-parser.js +0 -11
- package/src/utils/bundle.js +0 -10
package/src/data/permissions.js
CHANGED
|
@@ -1,31 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'webNavigation', 'webRequest', 'webRequestBlocking'
|
|
25
|
-
])
|
|
26
|
-
|
|
27
|
-
export function isValidPermission(name, target = 'mv3') {
|
|
28
|
-
if (validPermissions.has(name)) return true
|
|
29
|
-
if (target == 'mv3') return false
|
|
30
|
-
return re.url.test(name) || name.startsWith('<') || name == '*://*/*'
|
|
1
|
+
export default {
|
|
2
|
+
sensitive: new Set([
|
|
3
|
+
'accessibilityFeatures.modify', 'accessibilityFeatures.read', 'bookmarks', 'browsingData', 'clipboardRead',
|
|
4
|
+
'contentSettings', 'cookies', 'debugger', 'declarativeNetRequest', 'downloads', 'downloads.open',
|
|
5
|
+
'downloads.ui', 'geolocation', 'history', 'management', 'nativeMessaging', 'pageCapture', 'privacy',
|
|
6
|
+
'proxy', 'readingList', 'sessions', 'system.storage', 'tabCapture', 'tabGroups', 'tabs', 'topSites',
|
|
7
|
+
'ttsEngine', 'webAuthenticationProxy', 'webNavigation', 'webRequest', 'webRequestBlocking'
|
|
8
|
+
]),
|
|
9
|
+
valid: new Set([
|
|
10
|
+
'accessibilityFeatures.modify', 'accessibilityFeatures.read', 'activeTab', 'alarms', 'audio', 'background',
|
|
11
|
+
'bookmarks', 'browsingData', 'certificateProvider', 'clipboardRead', 'clipboardWrite', 'contentSettings',
|
|
12
|
+
'contextMenus', 'cookies', 'debugger', 'declarativeContent', 'declarativeNetRequest',
|
|
13
|
+
'declarativeNetRequestFeedback', 'declarativeNetRequestWithHostAccess', 'desktopCapture', 'dns',
|
|
14
|
+
'documentScan', 'downloads', 'downloads.open', 'downloads.ui', 'enterprise.deviceAttributes',
|
|
15
|
+
'enterprise.hardwarePlatform', 'enterprise.networkingAttributes', 'enterprise.platformKeys', 'favicon',
|
|
16
|
+
'fileBrowserHandler', 'fileSystemProvider', 'fontSettings', 'gcm', 'geolocation', 'history', 'identity',
|
|
17
|
+
'identity.email', 'idle', 'loginState', 'management', 'menus', 'nativeMessaging', 'notifications',
|
|
18
|
+
'offscreen', 'pageCapture', 'platformKeys', 'power', 'printerProvider', 'printing', 'printingMetrics',
|
|
19
|
+
'privacy', 'processes', 'proxy', 'publicSuffix', 'readingList', 'runtime', 'scripting', 'search',
|
|
20
|
+
'sessions', 'sidePanel', 'storage', 'system.cpu', 'system.display', 'system.memory', 'system.storage',
|
|
21
|
+
'tabCapture', 'tabGroups', 'tabs', 'topSites', 'tts', 'ttsEngine', 'unlimitedStorage', 'userScripts',
|
|
22
|
+
'vpnProvider', 'wallpaper', 'webAuthenticationProxy', 'webNavigation', 'webRequest', 'webRequestBlocking'
|
|
23
|
+
])
|
|
31
24
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
filename: {
|
|
3
|
+
css: /\.css$/i,
|
|
4
|
+
license: // eslint-disable-next-line security/detect-unsafe-regex
|
|
5
|
+
/^(?:[\w.-]*-)?(?:LICEN[SC]E|COPYING)(?:[-.][\w.-]+)?$/i,
|
|
6
|
+
manifest: /manifest\.json$/,
|
|
7
|
+
script: /\.[cm]?[jt]sx?$/i
|
|
8
|
+
},
|
|
9
|
+
url: /^https?:\/\//
|
|
10
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,129 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import init from './utils/init.js'
|
|
2
|
+
import deprecations from './data/deprecations.js'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
import html from '@html-eslint/eslint-plugin'
|
|
5
|
-
import json from '@eslint/json'
|
|
4
|
+
const plugin = init.plugin()
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import noContentStyleLeak from './rules/no-content-style-leak.js'
|
|
10
|
-
import noDeprecatedAPIs from './rules/no-deprecated-apis.js'
|
|
11
|
-
import noDuplicateManifestKeys from './rules/no-duplicate-manifest-keys.js'
|
|
12
|
-
import noInsecureFetch from './rules/no-insecure-fetch.js'
|
|
13
|
-
import noInsecureMessaging from './rules/no-insecure-messaging.js'
|
|
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'
|
|
17
|
-
import noMissingPermissions from './rules/no-missing-permissions.js'
|
|
18
|
-
import noMissingWebAccessibleResources from './rules/no-missing-web-accessible-resources.js'
|
|
19
|
-
import noRemoteCode from './rules/no-remote-code.js'
|
|
20
|
-
import noUnrestrictedHostPermissions from './rules/no-unrestricted-host-permissions.js'
|
|
21
|
-
import noUnusedI18nKeys from './rules/no-unused-i18n-keys.js'
|
|
22
|
-
import noUnusedPermissions from './rules/no-unused-permissions.js'
|
|
23
|
-
import requireLicenseFile from './rules/require-license-file.js'
|
|
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'
|
|
28
|
-
import validManifestIcons from './rules/valid-manifest-icons.js'
|
|
29
|
-
import validMV3schema from './rules/valid-mv3-schema.js'
|
|
30
|
-
import validServiceWorker from './rules/valid-service-worker.js'
|
|
31
|
-
import validURLfields from './rules/valid-url-fields.js'
|
|
32
|
-
import validVersionField from './rules/valid-version-field.js'
|
|
6
|
+
init.rules(plugin)
|
|
7
|
+
init.configs(plugin)
|
|
33
8
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
import { textParser } from './data/text-parser.js'
|
|
37
|
-
|
|
38
|
-
const plugin = {
|
|
39
|
-
rules: {
|
|
40
|
-
'manifest-key-order': manifestKeyOrder,
|
|
41
|
-
'manifest-spacing': manifestSpacing,
|
|
42
|
-
'no-content-style-leak': noContentStyleLeak,
|
|
43
|
-
'no-deprecated-apis': noDeprecatedAPIs,
|
|
44
|
-
'no-duplicate-manifest-keys': noDuplicateManifestKeys,
|
|
45
|
-
'no-insecure-fetch': noInsecureFetch,
|
|
46
|
-
'no-insecure-messaging': noInsecureMessaging,
|
|
47
|
-
'no-invalid-permissions': noInvalidPermissions,
|
|
48
|
-
'no-missing-default-locale': noMissingDefaultLocale,
|
|
49
|
-
'no-missing-i18n-keys': noMissingI18nKeys,
|
|
50
|
-
'no-missing-permissions': noMissingPermissions,
|
|
51
|
-
'no-missing-web-accessible-resources': noMissingWebAccessibleResources,
|
|
52
|
-
'no-remote-code': noRemoteCode,
|
|
53
|
-
'no-unrestricted-host-permissions': noUnrestrictedHostPermissions,
|
|
54
|
-
'no-unused-i18n-keys': noUnusedI18nKeys,
|
|
55
|
-
'no-unused-permissions': noUnusedPermissions,
|
|
56
|
-
'require-mv3': requireMV3,
|
|
57
|
-
'require-license-file': requireLicenseFile,
|
|
58
|
-
'require-short-name-when-useful': requireShortNameWhenUseful,
|
|
59
|
-
'valid-filenames': validFilenames,
|
|
60
|
-
'valid-license-file': validLicenseFile,
|
|
61
|
-
'valid-manifest-icons': validManifestIcons,
|
|
62
|
-
'valid-mv3-schema': validMV3schema,
|
|
63
|
-
'valid-service-worker': validServiceWorker,
|
|
64
|
-
'valid-url-fields': validURLfields,
|
|
65
|
-
'valid-version-field': validVersionField
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
for (const [ruleId, rule] of Object.entries(plugin.rules))
|
|
70
|
-
rule.meta.docs.url = `https://codeberg.org/adamlui/eslint-plugin-chrome-extensions/#${ruleId}`
|
|
71
|
-
|
|
72
|
-
plugin.configs = {
|
|
73
|
-
recommended: {
|
|
74
|
-
plugins: { 'chrome-extensions': plugin },
|
|
75
|
-
rules: bundleRules(plugin.rules, rule => rule.meta.docs.recommended, 'chrome-extensions')
|
|
76
|
-
},
|
|
77
|
-
strict: {
|
|
78
|
-
plugins: { 'chrome-extensions': plugin },
|
|
79
|
-
rules: bundleRules(plugin.rules, () => true, 'chrome-extensions')
|
|
80
|
-
},
|
|
81
|
-
problem: {
|
|
82
|
-
plugins: { 'chrome-extensions': plugin },
|
|
83
|
-
rules: bundleRules(plugin.rules, rule => rule.meta.type == 'problem', 'chrome-extensions')
|
|
84
|
-
},
|
|
85
|
-
security: {
|
|
86
|
-
plugins: { 'chrome-extensions': plugin },
|
|
87
|
-
rules: bundleRules(plugin.rules, rule => rule.meta.docs.security, 'chrome-extensions')
|
|
88
|
-
},
|
|
89
|
-
manifest: {
|
|
90
|
-
language: 'json/json',
|
|
91
|
-
plugins: { 'chrome-extensions': plugin, json },
|
|
92
|
-
rules: bundleRules(plugin.rules, rule => rule.meta.docs.manifest, 'chrome-extensions')
|
|
93
|
-
},
|
|
94
|
-
license: {
|
|
95
|
-
languageOptions: { parser: textParser },
|
|
96
|
-
plugins: { 'chrome-extensions': plugin },
|
|
97
|
-
rules: bundleRules(plugin.rules, rule => rule.meta.docs.license, 'chrome-extensions')
|
|
98
|
-
},
|
|
99
|
-
css: {
|
|
100
|
-
language: 'css/css',
|
|
101
|
-
plugins: { 'chrome-extensions': plugin, css },
|
|
102
|
-
rules: bundleRules(plugin.rules, rule => rule.meta.docs.css, 'chrome-extensions')
|
|
103
|
-
},
|
|
104
|
-
html: {
|
|
105
|
-
language: 'html/html',
|
|
106
|
-
plugins: { 'chrome-extensions': plugin, html },
|
|
107
|
-
rules: bundleRules(plugin.rules, rule => rule.meta.docs.html, 'chrome-extensions')
|
|
108
|
-
},
|
|
109
|
-
json: {
|
|
110
|
-
language: 'json/json',
|
|
111
|
-
plugins: { 'chrome-extensions': plugin, json },
|
|
112
|
-
rules: bundleRules(plugin.rules, rule => rule.meta.docs.json, 'chrome-extensions')
|
|
113
|
-
},
|
|
114
|
-
json5: {
|
|
115
|
-
language: 'json/json5',
|
|
116
|
-
plugins: { 'chrome-extensions': plugin, json },
|
|
117
|
-
rules: bundleRules(plugin.rules, rule => rule.meta.docs.json, 'chrome-extensions')
|
|
118
|
-
},
|
|
119
|
-
jsonc: {
|
|
120
|
-
language: 'json/jsonc',
|
|
121
|
-
plugins: { 'chrome-extensions': plugin, json },
|
|
122
|
-
rules: bundleRules(plugin.rules, rule => rule.meta.docs.json, 'chrome-extensions')
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
for (const entry of Object.entries(oldBundles))
|
|
127
|
-
initBundleDeprecation(plugin, entry, 'chrome-extensions')
|
|
9
|
+
for (const entry of Object.entries(deprecations.bundles))
|
|
10
|
+
init.bundleDeprecation(plugin, entry, 'chrome-extensions')
|
|
128
11
|
|
|
129
12
|
export { plugin as default, plugin as chromeExtensions }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { file } from '../utils/extension.js'
|
|
1
|
+
import extension from '../utils/extension.js'
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
import manifest from '../data/manifest.js'
|
|
4
|
+
import regex from '../data/regex.js'
|
|
5
5
|
|
|
6
6
|
function keyOf(member) {
|
|
7
7
|
const keyNode = member.name || member.key
|
|
@@ -43,15 +43,15 @@ export default {
|
|
|
43
43
|
},
|
|
44
44
|
|
|
45
45
|
create(context) {
|
|
46
|
-
if (!
|
|
46
|
+
if (!regex.filename.manifest.test(extension.file.getName(context))) return {}
|
|
47
47
|
const custom = context.options[0]?.order ?? [],
|
|
48
|
-
master = custom.length ?
|
|
49
|
-
:
|
|
48
|
+
master = custom.length ? manifest.keys.order.filter(key => !custom.includes(key))
|
|
49
|
+
: manifest.keys.order
|
|
50
50
|
return {
|
|
51
51
|
'Document:exit'(document) {
|
|
52
52
|
const root = document.body ; if (root?.type != 'Object') return
|
|
53
|
-
const members = root.members || root.properties || []
|
|
54
|
-
|
|
53
|
+
const members = root.members || root.properties || [],
|
|
54
|
+
violation = findViolation(members, master) ?? (custom.length && findViolation(members, custom))
|
|
55
55
|
if (!violation) return
|
|
56
56
|
const [first, second] = violation
|
|
57
57
|
context.report({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import extension from '../utils/extension.js'
|
|
2
|
+
import regex from '../data/regex.js'
|
|
2
3
|
|
|
3
|
-
const re = {
|
|
4
|
+
const re = { leadingWhitespace: /^[ \t]+/ },
|
|
4
5
|
brackets = { open: new Set(['LBrace', 'LBracket']), close: new Set(['RBrace', 'RBracket']) }
|
|
5
6
|
|
|
6
7
|
export default {
|
|
@@ -39,8 +40,7 @@ export default {
|
|
|
39
40
|
},
|
|
40
41
|
additionalProperties: false
|
|
41
42
|
}],
|
|
42
|
-
defaultOptions: [{ spaces: 2, checkColons: true, checkCommas: true,
|
|
43
|
-
checkBraces: true, checkBrackets: true }],
|
|
43
|
+
defaultOptions: [{ spaces: 2, checkColons: true, checkCommas: true, checkBraces: true, checkBrackets: true }],
|
|
44
44
|
messages: {
|
|
45
45
|
wrongIndent: 'Expected {{expected}} spaces of indentation (found {{found}}).',
|
|
46
46
|
tabFound: 'Expected spaces for indentation, found tabs.',
|
|
@@ -57,7 +57,7 @@ export default {
|
|
|
57
57
|
},
|
|
58
58
|
|
|
59
59
|
create(context) {
|
|
60
|
-
if (!
|
|
60
|
+
if (!regex.filename.manifest.test(extension.file.getName(context))) return {}
|
|
61
61
|
const opts = context.options[0] || {},
|
|
62
62
|
spaces = opts.spaces ?? 2,
|
|
63
63
|
checkColons = opts.checkColons ?? true,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import extension from '../utils/extension.js'
|
|
2
2
|
|
|
3
3
|
const re = { appendMethod: /^append(?:Child)?$/ }
|
|
4
4
|
|
|
@@ -17,7 +17,8 @@ export default {
|
|
|
17
17
|
},
|
|
18
18
|
|
|
19
19
|
create(context) {
|
|
20
|
-
const manifest =
|
|
20
|
+
const manifest = extension.manifest.read(context)
|
|
21
|
+
if (!manifest || !extension.file.isContentScript(context, manifest)) return {}
|
|
21
22
|
const styleElems = new Set(), docAliases = new Set()
|
|
22
23
|
function isCreateStyle(node) {
|
|
23
24
|
return node?.type == 'CallExpression'
|
|
@@ -57,8 +58,9 @@ export default {
|
|
|
57
58
|
CallExpression(node) {
|
|
58
59
|
if (node.callee.type != 'MemberExpression') return
|
|
59
60
|
const method = node.callee.property.name
|
|
60
|
-
if (!re.appendMethod.test(method)
|
|
61
|
-
|
|
61
|
+
if (!re.appendMethod.test(method)
|
|
62
|
+
|| !isDocumentMember(node.callee.object)
|
|
63
|
+
) return
|
|
62
64
|
for (const arg of node.arguments)
|
|
63
65
|
if (isStyleTarget(arg))
|
|
64
66
|
context.report({ node: arg, messageId: 'styleLeak' })
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import nodeUtils from '../utils/node.js'
|
|
2
|
+
import apis from '../data/apis.js'
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
5
|
meta: {
|
|
@@ -50,8 +50,8 @@ export default {
|
|
|
50
50
|
return {
|
|
51
51
|
MemberExpression(node) {
|
|
52
52
|
if (node.parent.type == 'MemberExpression' && node.parent.object === node) return
|
|
53
|
-
const api = getMemberPath(node) ; if (!api?.startsWith('chrome.') || isAllowed(api)) return
|
|
54
|
-
const found = findDeprecated(api) ; if (!found) return
|
|
53
|
+
const api = nodeUtils.getMemberPath(node) ; if (!api?.startsWith('chrome.') || isAllowed(api)) return
|
|
54
|
+
const found = apis.findDeprecated(api) ; if (!found) return
|
|
55
55
|
context.report({ node, messageId: 'deprecatedAPI',
|
|
56
56
|
data: { api: found.deprecated, replacement: found.replacement, target }})
|
|
57
57
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
const re = { manifestFilename: /manifest\.json$/ }
|
|
1
|
+
import extension from '../utils/extension.js'
|
|
2
|
+
import regex from '../data/regex.js'
|
|
4
3
|
|
|
5
4
|
export default {
|
|
6
5
|
meta: {
|
|
@@ -18,7 +17,7 @@ export default {
|
|
|
18
17
|
},
|
|
19
18
|
|
|
20
19
|
create(context) {
|
|
21
|
-
if (!
|
|
20
|
+
if (!regex.filename.manifest.test(extension.file.getName(context))) return {}
|
|
22
21
|
return {
|
|
23
22
|
Object(node) {
|
|
24
23
|
const members = node.members || node.properties || [],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import nodeUtils from '../utils/node.js'
|
|
2
|
+
import apis from '../data/apis.js'
|
|
3
3
|
|
|
4
4
|
const re = { postmsgValidation: /\b\w+\.(?:origin|source)\b/, senderValidation: /sender\.(?:id|origin)/ }
|
|
5
5
|
|
|
@@ -44,9 +44,9 @@ export default {
|
|
|
44
44
|
return {
|
|
45
45
|
CallExpression(node) {
|
|
46
46
|
if (node.callee.type != 'MemberExpression') return
|
|
47
|
-
const path = getMemberPath(node.callee) ; if (!path) return
|
|
47
|
+
const path = nodeUtils.getMemberPath(node.callee) ; if (!path) return
|
|
48
48
|
const listenerRoot = path.endsWith('.addListener') ? path.slice(0, -'.addListener'.length) : null
|
|
49
|
-
if (listenerRoot &&
|
|
49
|
+
if (listenerRoot && apis.isExternalMsgListener(listenerRoot)) {
|
|
50
50
|
const handler = node.arguments[0] ; if (!handler) return
|
|
51
51
|
const body = context.sourceCode.getText(handler)
|
|
52
52
|
if (!re.senderValidation.test(body))
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/* eslint-disable security/detect-non-literal-fs-filename -- lint-time project inspection only */
|
|
2
|
+
|
|
3
|
+
import extension from '../utils/extension.js'
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import i18nUtils from '../utils/i18n.js'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
|
|
8
|
+
import regex from '../data/regex.js'
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'problem',
|
|
13
|
+
docs: {
|
|
14
|
+
description: 'Disallow invalid Chrome locale codes in `_locales/` folders and `default_locale`',
|
|
15
|
+
category: 'Possible Errors',
|
|
16
|
+
recommended: true,
|
|
17
|
+
manifest: true
|
|
18
|
+
},
|
|
19
|
+
schema: [{
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
allowedLocales: {
|
|
23
|
+
oneOf: [
|
|
24
|
+
{ type: 'string', description: 'Single locale code to allow' },
|
|
25
|
+
{
|
|
26
|
+
type: 'array',
|
|
27
|
+
items: { type: 'string', description: 'Locale code to allow' },
|
|
28
|
+
description: 'List of locale codes to allow'
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
description: 'Additional locale codes to allow (e.g. for Chromium forks)'
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
additionalProperties: false
|
|
35
|
+
}],
|
|
36
|
+
defaultOptions: [{ allowedLocales: [] }],
|
|
37
|
+
messages: {
|
|
38
|
+
invalidLocale: '`{{value}}` is not a Chrome-supported locale code.'
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
create(context) {
|
|
43
|
+
const filename = extension.file.getName(context),
|
|
44
|
+
isManifest = regex.filename.manifest.test(filename),
|
|
45
|
+
isScript = regex.filename.script.test(filename)
|
|
46
|
+
if (!isManifest && !isScript) return {}
|
|
47
|
+
const opts = context.options[0] || {},
|
|
48
|
+
allowed = new Set(Array.isArray(opts.allowedLocales) ? opts.allowedLocales
|
|
49
|
+
: opts.allowedLocales ? [opts.allowedLocales] : []),
|
|
50
|
+
invalidFolders = [],
|
|
51
|
+
root = extension.findRoot(context)
|
|
52
|
+
if (root) {
|
|
53
|
+
const localesDir = path.join(root, '_locales')
|
|
54
|
+
try {
|
|
55
|
+
for (const entry of fs.readdirSync(localesDir, { withFileTypes: true }))
|
|
56
|
+
if (entry.isDirectory() && isInvalid(entry.name)) invalidFolders.push(entry.name)
|
|
57
|
+
invalidFolders.sort()
|
|
58
|
+
} catch {} // eslint-disable-line no-empty
|
|
59
|
+
}
|
|
60
|
+
const loc = { line: 1, column: 1 }
|
|
61
|
+
if (!isManifest) {
|
|
62
|
+
const manifest = extension.manifest.read(context)
|
|
63
|
+
if (manifest) {
|
|
64
|
+
const defaultLocale = manifest.default_locale
|
|
65
|
+
if (isInvalid(defaultLocale))
|
|
66
|
+
context.report({ loc, messageId: 'invalidLocale', data: { value: defaultLocale }})
|
|
67
|
+
}
|
|
68
|
+
for (const name of invalidFolders)
|
|
69
|
+
context.report({ loc, messageId: 'invalidLocale', data: { value: name }})
|
|
70
|
+
return {}
|
|
71
|
+
}
|
|
72
|
+
function isInvalid(locale) {
|
|
73
|
+
return typeof locale == 'string' && !i18nUtils.localeIsValid(locale, allowed) }
|
|
74
|
+
return {
|
|
75
|
+
'*'(node) {
|
|
76
|
+
const keyNode = node.name || node.key ; if (!keyNode) return
|
|
77
|
+
const keyName = keyNode.value ?? keyNode.name ; if (keyName != 'default_locale') return
|
|
78
|
+
const valueNode = node.value ; if (valueNode?.type != 'String') return
|
|
79
|
+
if (isInvalid(valueNode.value))
|
|
80
|
+
context.report({ node: valueNode, messageId: 'invalidLocale',
|
|
81
|
+
data: { value: valueNode.value }})
|
|
82
|
+
},
|
|
83
|
+
'Document:exit'() {
|
|
84
|
+
for (const name of invalidFolders)
|
|
85
|
+
context.report({ loc, messageId: 'invalidLocale', data: { value: name }})
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
const re = { manifestFilename: /manifest\.json$/ }
|
|
1
|
+
import extension from '../utils/extension.js'
|
|
2
|
+
import regex from '../data/regex.js'
|
|
5
3
|
|
|
6
4
|
export default {
|
|
7
5
|
meta: {
|
|
@@ -9,7 +7,7 @@ export default {
|
|
|
9
7
|
docs: {
|
|
10
8
|
description: 'Disallow invalid or unsupported manifest permission names',
|
|
11
9
|
category: 'Possible Errors',
|
|
12
|
-
recommended:
|
|
10
|
+
recommended: true,
|
|
13
11
|
manifest: true
|
|
14
12
|
},
|
|
15
13
|
defaultOptions: [{ target: 'mv3', allowedPermissions: [] }],
|
|
@@ -46,29 +44,28 @@ export default {
|
|
|
46
44
|
: opts.allowedPermissions ? [opts.allowedPermissions] : [])
|
|
47
45
|
function checkPermission(node, permission) {
|
|
48
46
|
if (typeof permission != 'string') return
|
|
49
|
-
if (allowed.has(permission) ||
|
|
47
|
+
if (allowed.has(permission) || extension.manifest.permission.isValid(permission, target)) return
|
|
50
48
|
context.report({ node, messageId: 'invalidPermission', data: { permission, target }})
|
|
51
49
|
}
|
|
52
|
-
if (
|
|
50
|
+
if (regex.filename.manifest.test(extension.file.getName(context))) // lint it directly
|
|
53
51
|
return {
|
|
54
52
|
'*'(node) {
|
|
55
53
|
const keyNode = node.name || node.key ; if (!keyNode) return
|
|
56
|
-
|
|
57
|
-
if (keyName != 'permissions' && keyName != 'optional_permissions') return
|
|
54
|
+
if (!['optional_permissions', 'permissions'].includes(keyNode.value ?? keyNode.name)) return
|
|
58
55
|
const arrNode = node.value ; if (arrNode?.type != 'Array') return
|
|
59
|
-
const
|
|
60
|
-
for (const elem of
|
|
56
|
+
const nodeElems = arrNode.elements || arrNode.items || []
|
|
57
|
+
for (const elem of nodeElems) {
|
|
61
58
|
const valueNode = elem.value || elem
|
|
62
59
|
checkPermission(valueNode, valueNode?.value)
|
|
63
60
|
}
|
|
64
61
|
}
|
|
65
62
|
}
|
|
66
|
-
const manifest =
|
|
63
|
+
const manifest = extension.manifest.read(context) ; if (!manifest) return {}
|
|
67
64
|
const allPermissions = [...(manifest.permissions || []), ...(manifest.optional_permissions || [])]
|
|
68
65
|
return {
|
|
69
66
|
'Program:exit'() {
|
|
70
67
|
for (const permission of allPermissions) {
|
|
71
|
-
if (allowed.has(permission) ||
|
|
68
|
+
if (allowed.has(permission) || extension.manifest.permission.isValid(permission, target)) continue
|
|
72
69
|
context.report({ loc: { line: 1, column: 0 },
|
|
73
70
|
messageId: 'invalidPermission', data: { permission, target }})
|
|
74
71
|
}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
/* eslint-disable security/detect-non-literal-fs-filename -- lint-time project inspection only */
|
|
2
2
|
|
|
3
|
+
import extension from '../utils/extension.js'
|
|
3
4
|
import fs from 'node:fs'
|
|
5
|
+
import i18n from '../utils/i18n.js'
|
|
4
6
|
import path from 'node:path'
|
|
5
7
|
|
|
6
|
-
import
|
|
7
|
-
import { readManifest } from '../utils/extension.js'
|
|
8
|
+
import regex from '../data/regex.js'
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
function readLocaleDirs(localesDir) {
|
|
11
|
+
try {
|
|
12
|
+
return fs.readdirSync(localesDir, { withFileTypes: true })
|
|
13
|
+
.filter(entry => entry.isDirectory())
|
|
14
|
+
.map(entry => entry.name)
|
|
15
|
+
} catch { return [] }
|
|
16
|
+
}
|
|
10
17
|
|
|
11
18
|
function readMessages(filePath) {
|
|
12
19
|
try {
|
|
@@ -15,21 +22,13 @@ function readMessages(filePath) {
|
|
|
15
22
|
} catch { return null }
|
|
16
23
|
}
|
|
17
24
|
|
|
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
25
|
export default {
|
|
27
26
|
meta: {
|
|
28
27
|
type: 'problem',
|
|
29
28
|
docs: {
|
|
30
29
|
description: 'Ensure `default_locale` is valid and all `_locales` keys exist in it',
|
|
31
30
|
category: 'Possible Errors',
|
|
32
|
-
recommended:
|
|
31
|
+
recommended: true,
|
|
33
32
|
manifest: true
|
|
34
33
|
},
|
|
35
34
|
schema: [{
|
|
@@ -37,31 +36,37 @@ export default {
|
|
|
37
36
|
properties: {
|
|
38
37
|
ignoreKeys: {
|
|
39
38
|
type: 'array', items: { type: 'string' },
|
|
40
|
-
description: 'Keys to skip when checking
|
|
39
|
+
description: 'Keys to skip when checking locales'
|
|
40
|
+
},
|
|
41
|
+
checkAllLocales: {
|
|
42
|
+
type: 'boolean',
|
|
43
|
+
description: 'Also flag keys missing from any locale, not just the default'
|
|
41
44
|
}
|
|
42
45
|
},
|
|
43
46
|
additionalProperties: false
|
|
44
47
|
}],
|
|
45
|
-
defaultOptions: [{ ignoreKeys: [] }],
|
|
48
|
+
defaultOptions: [{ ignoreKeys: [], checkAllLocales: false }],
|
|
46
49
|
messages: {
|
|
47
50
|
missingDefaultLocale: '`_locales/` exists but `default_locale` is not declared.',
|
|
48
51
|
unexpectedDefaultLocale: '`default_locale` is declared but no `_locales/` directory exists.',
|
|
49
52
|
missingDefaultLocaleFile: '`_locales/{{locale}}/messages.json` does not exist.',
|
|
50
53
|
missingKeyInDefault: '`{{key}}` exists in `_locales/{{locale}}/messages.json`, '
|
|
51
|
-
+ 'but not in default locale `{{defaultLocale}}`.'
|
|
54
|
+
+ 'but not in default locale `{{defaultLocale}}`.',
|
|
55
|
+
missingKeyInLocale: '`{{key}}` is missing from `_locales/{{locale}}/messages.json`.'
|
|
52
56
|
}
|
|
53
57
|
},
|
|
54
58
|
|
|
55
59
|
create(context) {
|
|
56
|
-
const filename = context
|
|
57
|
-
if (!filename
|
|
58
|
-
const manifest =
|
|
59
|
-
const root =
|
|
60
|
+
const filename = extension.file.getName(context) ; if (!filename) return {}
|
|
61
|
+
if (!regex.filename.manifest.test(filename) && !regex.filename.script.test(filename)) return {}
|
|
62
|
+
const manifest = extension.manifest.read(context) ; if (!manifest) return {}
|
|
63
|
+
const root = extension.findRoot(context) ; if (!root) return {}
|
|
60
64
|
const localesDir = path.join(root, '_locales'),
|
|
61
65
|
hasLocales = fs.existsSync(localesDir),
|
|
62
66
|
defaultLocale = manifest.default_locale,
|
|
63
67
|
opts = context.options[0] || {},
|
|
64
68
|
ignored = new Set(opts.ignoreKeys || []),
|
|
69
|
+
checkAllLocales = opts.checkAllLocales ?? false,
|
|
65
70
|
loc = { line: 1, column: 1 }
|
|
66
71
|
if (hasLocales && typeof defaultLocale != 'string') {
|
|
67
72
|
context.report({ loc, messageId: 'missingDefaultLocale' })
|
|
@@ -77,11 +82,29 @@ export default {
|
|
|
77
82
|
context.report({ loc, messageId: 'missingDefaultLocaleFile', data: { locale: defaultLocale }})
|
|
78
83
|
return {}
|
|
79
84
|
}
|
|
80
|
-
|
|
85
|
+
const localeDirs = readLocaleDirs(localesDir)
|
|
86
|
+
if (checkAllLocales) {
|
|
87
|
+
const allMsgs = new Map([[defaultLocale, defaultMsgs]])
|
|
88
|
+
for (const locale of localeDirs) {
|
|
89
|
+
if (locale == defaultLocale) continue
|
|
90
|
+
const msgs = readMessages(path.join(localesDir, locale, 'messages.json'))
|
|
91
|
+
if (msgs) allMsgs.set(locale, msgs)
|
|
92
|
+
}
|
|
93
|
+
const union = new Set()
|
|
94
|
+
for (const msgs of allMsgs.values())
|
|
95
|
+
for (const key of Object.keys(msgs))
|
|
96
|
+
if (!i18n.isPredefined(key) && !ignored.has(key)) union.add(key)
|
|
97
|
+
for (const [locale, msgs] of allMsgs)
|
|
98
|
+
for (const key of union)
|
|
99
|
+
if (!(key in msgs))
|
|
100
|
+
context.report({ loc, messageId: 'missingKeyInLocale', data: { key, locale }})
|
|
101
|
+
return {}
|
|
102
|
+
}
|
|
103
|
+
for (const locale of localeDirs) {
|
|
81
104
|
if (locale == defaultLocale) continue
|
|
82
105
|
const msgs = readMessages(path.join(localesDir, locale, 'messages.json')) ; if (!msgs) continue
|
|
83
106
|
for (const key of Object.keys(msgs)) {
|
|
84
|
-
if (isPredefined(key) || ignored.has(key)) continue
|
|
107
|
+
if (i18n.isPredefined(key) || ignored.has(key)) continue
|
|
85
108
|
if (!(key in defaultMsgs))
|
|
86
109
|
context.report({ loc, messageId: 'missingKeyInDefault', data: { key, locale, defaultLocale }})
|
|
87
110
|
}
|