eslint-plugin-chrome-extensions 2.0.0 → 2.2.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 +510 -41
- package/docs/LICENSE.md +1 -1
- package/package.json +3 -5
- package/src/data/apis.js +52 -0
- package/src/data/deprecations.js +10 -0
- package/src/data/manifest.js +22 -11
- package/src/data/permissions.js +1 -1
- package/src/index.js +22 -1
- package/src/rules/manifest-key-order.js +3 -2
- package/src/rules/manifest-spacing.js +160 -0
- package/src/rules/no-duplicate-manifest-keys.js +36 -0
- package/src/rules/no-insecure-fetch.js +52 -0
- package/src/rules/no-insecure-messaging.js +44 -7
- package/src/rules/no-invalid-permissions.js +2 -2
- package/src/rules/no-missing-web-accessible-resources.js +51 -0
- package/src/rules/no-remote-code.js +11 -9
- package/src/rules/no-unrestricted-host-permissions.js +54 -0
- package/src/rules/no-unused-permissions.js +1 -1
- package/src/rules/require-mv3.js +2 -2
- package/src/rules/valid-manifest-icons.js +85 -0
- package/src/rules/valid-mv3-schema.js +50 -20
- package/src/rules/valid-service-worker.js +8 -3
- package/src/rules/valid-version-field.js +3 -3
- package/src/utils.js +11 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getMemberPath, isContentScriptFile, readManifest } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
const re = { leadingSlash: /^\//, regexSpecial: /[.+?^${}()|[\]\\]/g }
|
|
4
|
+
|
|
5
|
+
function collectWarPatterns(manifest) {
|
|
6
|
+
const war = manifest.web_accessible_resources
|
|
7
|
+
if (!Array.isArray(war)) return []
|
|
8
|
+
const patterns = []
|
|
9
|
+
for (const entry of war) {
|
|
10
|
+
if (typeof entry == 'string') patterns.push(entry) // MV2 flat
|
|
11
|
+
else if (Array.isArray(entry?.resources))
|
|
12
|
+
for (const res of entry.resources) patterns.push(res) // MV3 object
|
|
13
|
+
}
|
|
14
|
+
return patterns
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function patternToRegex(pattern) {
|
|
18
|
+
const escaped = pattern.replace(re.regexSpecial, '\\$&').replace(/\*/g, '.*')
|
|
19
|
+
return new RegExp(`^${escaped}$`) // eslint-disable-line security/detect-non-literal-regexp
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default {
|
|
23
|
+
meta: {
|
|
24
|
+
type: 'problem',
|
|
25
|
+
docs: {
|
|
26
|
+
description: 'Require content-script getURL() paths to be listed in `web_accessible_resources`',
|
|
27
|
+
category: 'Possible Errors',
|
|
28
|
+
recommended: true
|
|
29
|
+
},
|
|
30
|
+
schema: [],
|
|
31
|
+
messages: {
|
|
32
|
+
missingWar: '`{{path}}` is not in `web_accessible_resources`.'
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
create(context) {
|
|
37
|
+
const manifest = readManifest(context) ; if (!manifest) return {}
|
|
38
|
+
if (!isContentScriptFile(context, manifest)) return {}
|
|
39
|
+
const regexes = collectWarPatterns(manifest).map(patternToRegex)
|
|
40
|
+
return {
|
|
41
|
+
CallExpression(node) {
|
|
42
|
+
if (node.callee.type != 'MemberExpression') return
|
|
43
|
+
if (getMemberPath(node.callee) != 'chrome.runtime.getURL') return
|
|
44
|
+
const arg = node.arguments[0] ; if (arg?.type != 'Literal' || typeof arg.value != 'string') return
|
|
45
|
+
const resourcePath = arg.value.replace(re.leadingSlash, '')
|
|
46
|
+
for (const regex of regexes) if (regex.test(resourcePath)) return
|
|
47
|
+
context.report({ node: arg, messageId: 'missingWar', data: { path: arg.value }})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { remoteCodePatterns } from '../data/manifest.js'
|
|
2
|
+
|
|
2
3
|
import { getMemberPath } from '../utils.js'
|
|
3
4
|
|
|
4
5
|
const re = { url: /^https?:\/\// }
|
|
@@ -7,9 +8,10 @@ export default {
|
|
|
7
8
|
meta: {
|
|
8
9
|
type: 'problem',
|
|
9
10
|
docs: {
|
|
10
|
-
description: 'Disallow remote or dynamically-evaluated code, per
|
|
11
|
+
description: 'Disallow remote or dynamically-evaluated code, per MV3\'s CSP',
|
|
11
12
|
category: 'Possible Errors',
|
|
12
|
-
recommended: true
|
|
13
|
+
recommended: true,
|
|
14
|
+
security: true
|
|
13
15
|
},
|
|
14
16
|
defaultOptions: [{
|
|
15
17
|
patterns: ['eval', 'new-function', 'string-timer',
|
|
@@ -26,19 +28,19 @@ export default {
|
|
|
26
28
|
},
|
|
27
29
|
checkExecuteScriptClosures: {
|
|
28
30
|
type: 'boolean',
|
|
29
|
-
description: 'Flag chrome.scripting.executeScript calls whose func captures outer scope'
|
|
31
|
+
description: 'Flag `chrome.scripting.executeScript` calls whose func captures outer scope'
|
|
30
32
|
}
|
|
31
33
|
},
|
|
32
34
|
additionalProperties: false
|
|
33
35
|
}],
|
|
34
36
|
messages: {
|
|
35
|
-
eval: '`eval()` is forbidden under MV3\'s CSP
|
|
36
|
-
newFunction: '`new Function()` is forbidden under MV3\'s CSP
|
|
37
|
-
stringTimer: 'String-argument `{{fn}}` is forbidden under MV3\'s CSP
|
|
38
|
-
dynamicImport: 'Remote dynamic `import()` is forbidden under MV3\'s CSP
|
|
39
|
-
remoteScriptSrc: 'Injecting remote `<script src>` is forbidden under MV3\'s CSP
|
|
37
|
+
eval: '`eval()` is forbidden under MV3\'s CSP. Use static code instead.',
|
|
38
|
+
newFunction: '`new Function()` is forbidden under MV3\'s CSP. Use a real function instead.',
|
|
39
|
+
stringTimer: 'String-argument `{{fn}}` is forbidden under MV3\'s CSP. Pass a function reference instead.',
|
|
40
|
+
dynamicImport: 'Remote dynamic `import()` is forbidden under MV3\'s CSP. Use a relative path instead.',
|
|
41
|
+
remoteScriptSrc: 'Injecting remote `<script src>` is forbidden under MV3\'s CSP. Use `chrome.runtime.getURL()` for bundled scripts instead.',
|
|
40
42
|
executeScriptURL: '`chrome.scripting.executeScript` must use local `files`, not a remote URL.',
|
|
41
|
-
executeScriptClosure: '`chrome.scripting.executeScript({ func })` captures outer-scope variables that fail after serialization
|
|
43
|
+
executeScriptClosure: '`chrome.scripting.executeScript({ func })` captures outer-scope variables that fail after serialization. Pass them via `args` instead.'
|
|
42
44
|
}
|
|
43
45
|
},
|
|
44
46
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { getFilename } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
const re = { broadHost: /^(?:<all_urls>|\*:\/\/\*\/\*|\w+:\/\/\*\/\*)$/, manifestFilename: /manifest\.json$/ },
|
|
4
|
+
hostKeys = new Set(['host_permissions', 'optional_host_permissions', 'optional_permissions', 'permissions'])
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'problem',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Disallow broad host permissions that grant access to every site',
|
|
11
|
+
category: 'Possible Errors',
|
|
12
|
+
recommended: false,
|
|
13
|
+
manifest: true
|
|
14
|
+
},
|
|
15
|
+
defaultOptions: [{ allowedPatterns: [] }],
|
|
16
|
+
schema: [{
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
allowedPatterns: {
|
|
20
|
+
type: 'array',
|
|
21
|
+
items: { type: 'string' },
|
|
22
|
+
description: 'Host patterns to explicitly allow despite being broad'
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
additionalProperties: false
|
|
26
|
+
}],
|
|
27
|
+
messages: {
|
|
28
|
+
unrestrictedHost: 'Host permission "{{pattern}}" grants access to every site. Narrow to specific origins or allow explicitly instead.'
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
create(context) {
|
|
33
|
+
if (!re.manifestFilename.test(getFilename(context))) return {}
|
|
34
|
+
const allowed = new Set(context.options[0]?.allowedPatterns || [])
|
|
35
|
+
return {
|
|
36
|
+
'*'(node) {
|
|
37
|
+
const keyNode = node.name || node.key ; if (!keyNode) return
|
|
38
|
+
const keyName = keyNode.value ?? keyNode.name
|
|
39
|
+
if (!hostKeys.has(keyName)) return
|
|
40
|
+
const arrNode = node.value ; if (arrNode?.type != 'Array') return
|
|
41
|
+
const elements = arrNode.elements || arrNode.items || []
|
|
42
|
+
for (const el of elements) {
|
|
43
|
+
const valueNode = el.value || el,
|
|
44
|
+
pattern = valueNode?.value
|
|
45
|
+
if (typeof pattern != 'string'
|
|
46
|
+
|| allowed.has(pattern)
|
|
47
|
+
|| !re.broadHost.test(pattern)
|
|
48
|
+
) continue
|
|
49
|
+
context.report({ node: valueNode, messageId: 'unrestrictedHost', data: { pattern }})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/rules/require-mv3.js
CHANGED
|
@@ -8,12 +8,12 @@ export default {
|
|
|
8
8
|
docs: {
|
|
9
9
|
description: 'Require manifest_version 3',
|
|
10
10
|
category: 'Possible Errors',
|
|
11
|
-
recommended:
|
|
11
|
+
recommended: false,
|
|
12
12
|
manifest: true
|
|
13
13
|
},
|
|
14
14
|
schema: [],
|
|
15
15
|
messages: {
|
|
16
|
-
notMV3: 'manifest.json must declare `"manifest_version": 3
|
|
16
|
+
notMV3: 'manifest.json must declare `"manifest_version": 3`.'
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { findExtensionRoot, getFilename } from '../utils.js'
|
|
5
|
+
|
|
6
|
+
const re = { manifestFilename: /manifest\.json$/, svg: /\.svg$/i },
|
|
7
|
+
sizes = { recommended: ['16', '32', '48', '128'], required: '128' }
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'problem',
|
|
12
|
+
docs: {
|
|
13
|
+
description: 'Validate manifest icons: sizes, existing files, no SVG',
|
|
14
|
+
category: 'Possible Errors',
|
|
15
|
+
recommended: false,
|
|
16
|
+
manifest: true
|
|
17
|
+
},
|
|
18
|
+
defaultOptions: [{
|
|
19
|
+
requireRecommendedSizes: true,
|
|
20
|
+
checkIconExists: true,
|
|
21
|
+
noSVG: true
|
|
22
|
+
}],
|
|
23
|
+
schema: [{
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
requireRecommendedSizes: {
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
description: 'Require all recommended sizes (16, 32, 48, 128)'
|
|
29
|
+
},
|
|
30
|
+
checkIconExists: {
|
|
31
|
+
type: 'boolean',
|
|
32
|
+
description: 'Flag icon paths that do not resolve to a file'
|
|
33
|
+
},
|
|
34
|
+
noSVG: {
|
|
35
|
+
type: 'boolean',
|
|
36
|
+
description: 'Flag SVG icon paths (unsupported by Chrome)'
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
additionalProperties: false
|
|
40
|
+
}],
|
|
41
|
+
messages: {
|
|
42
|
+
missingSize: 'Recommended icon size "{{size}}" is missing (16, 32, 48, 128).',
|
|
43
|
+
missingRequiredSize: 'Icons must include a 128x128 icon.',
|
|
44
|
+
missingFile: 'Icon file "{{file}}" does not exist in the extension root.',
|
|
45
|
+
svgNotSupported: 'SVG icons are not supported by Chrome. Use PNG instead.'
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
create(context) {
|
|
50
|
+
if (!re.manifestFilename.test(getFilename(context))) return {}
|
|
51
|
+
const opts = context.options[0] || {},
|
|
52
|
+
requireRecommended = opts.requireRecommendedSizes !== false,
|
|
53
|
+
checkFiles = opts.checkIconExists !== false,
|
|
54
|
+
checkSVG = opts.noSVG !== false
|
|
55
|
+
const root = checkFiles ? findExtensionRoot(context) : null
|
|
56
|
+
return {
|
|
57
|
+
'*'(node) {
|
|
58
|
+
const keyNode = node.name || node.key ; if (!keyNode) return
|
|
59
|
+
const keyName = keyNode.value ?? keyNode.name ; if (keyName != 'icons') return
|
|
60
|
+
const iconsObj = node.value ; if (iconsObj?.type != 'Object') return
|
|
61
|
+
const members = iconsObj.members || iconsObj.properties || []
|
|
62
|
+
const declaredSizes = new Set()
|
|
63
|
+
for (const member of members) {
|
|
64
|
+
const sizeNode = member.name || member.key,
|
|
65
|
+
size = sizeNode?.value ?? sizeNode?.name,
|
|
66
|
+
fileNode = member.value,
|
|
67
|
+
file = fileNode?.value
|
|
68
|
+
if (typeof size == 'string') declaredSizes.add(size)
|
|
69
|
+
if (typeof file != 'string') continue
|
|
70
|
+
if (checkSVG && re.svg.test(file))
|
|
71
|
+
context.report({ node: fileNode, messageId: 'svgNotSupported' })
|
|
72
|
+
else if (checkFiles && root // eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
73
|
+
&& !fs.existsSync(path.resolve(root, file))
|
|
74
|
+
) context.report({ node: fileNode, messageId: 'missingFile', data: { file }})
|
|
75
|
+
}
|
|
76
|
+
if (requireRecommended) {
|
|
77
|
+
for (const size of sizes.recommended)
|
|
78
|
+
if (!declaredSizes.has(size))
|
|
79
|
+
context.report({ node: iconsObj, messageId: 'missingSize', data: { size }})
|
|
80
|
+
} else if (!declaredSizes.has(sizes.required))
|
|
81
|
+
context.report({ node: iconsObj, messageId: 'missingRequiredSize' })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { mv2OnlyKeys, forbiddenCSPdirectives } from '../data/manifest.js'
|
|
2
1
|
import { getFilename, readManifest } from '../utils.js'
|
|
2
|
+
import { mv2OnlyKeys, forbiddenCSPdirectives, ffMV3bgKeys, ffDataCollectionRequired, ffDataCollectionOptional }
|
|
3
|
+
from '../data/manifest.js'
|
|
3
4
|
|
|
4
5
|
const re = { manifestFilename: /manifest\.json$/ }
|
|
5
6
|
|
|
@@ -9,12 +10,13 @@ export default {
|
|
|
9
10
|
docs: {
|
|
10
11
|
description: 'Validate manifest against the MV3 schema',
|
|
11
12
|
category: 'Possible Errors',
|
|
12
|
-
recommended:
|
|
13
|
+
recommended: false,
|
|
13
14
|
manifest: true
|
|
14
15
|
},
|
|
15
16
|
schema: [],
|
|
16
17
|
messages: {
|
|
17
|
-
|
|
18
|
+
invalidDataCollectionValue: '`{{value}}` is not a valid data collection permission value for {{kind}}.',
|
|
19
|
+
mv2Key: '`{{key}}` is not allowed in MV3. {{reason}}.',
|
|
18
20
|
unsafeCSP: 'MV3 CSP must not include `{{directive}}`.',
|
|
19
21
|
wrongCSPType: '`content_security_policy` must be an object in MV3, not a string.'
|
|
20
22
|
}
|
|
@@ -22,11 +24,11 @@ export default {
|
|
|
22
24
|
|
|
23
25
|
create(context) {
|
|
24
26
|
if (re.manifestFilename.test(getFilename(context))) { // lint it directly
|
|
25
|
-
const ancestors = [], pendingReports = []
|
|
26
|
-
let isMV3 = false
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
return
|
|
27
|
+
const ancestors = [], pendingReports = [], deferredMV2 = []
|
|
28
|
+
let isMV3 = false, isFF = false
|
|
29
|
+
function keyOf(node) {
|
|
30
|
+
const keyNode = node?.name || node?.key
|
|
31
|
+
return keyNode?.value ?? keyNode?.name
|
|
30
32
|
}
|
|
31
33
|
return {
|
|
32
34
|
'*'(node) {
|
|
@@ -35,18 +37,23 @@ export default {
|
|
|
35
37
|
const isTopLevel = ancestors.length == 2
|
|
36
38
|
&& ancestors[0].type == 'Document'
|
|
37
39
|
&& ancestors[1].type == 'Object'
|
|
38
|
-
const inBackground = ancestors.some(
|
|
39
|
-
const inCSP = ancestors.some(
|
|
40
|
+
const inBackground = ancestors.some(ancestor => keyOf(ancestor) == 'background')
|
|
41
|
+
const inCSP = ancestors.some(ancestor => keyOf(ancestor) == 'content_security_policy')
|
|
42
|
+
const inDataCollection = ancestors.some(ancestor =>
|
|
43
|
+
keyOf(ancestor) == 'data_collection_permissions')
|
|
40
44
|
if (keyName == 'manifest_version' && node.value?.value == 3) isMV3 = true
|
|
45
|
+
if (isTopLevel && keyName == 'browser_specific_settings') isFF = true
|
|
41
46
|
if (isTopLevel && mv2OnlyKeys.has(keyName))
|
|
42
47
|
pendingReports.push({ node, messageId: 'mv2Key',
|
|
43
48
|
data: { key: keyName, reason: 'MV2-only key' }})
|
|
44
|
-
if (inBackground && keyName
|
|
45
|
-
|
|
46
|
-
data: { key:
|
|
49
|
+
if (inBackground && ffMV3bgKeys.has(keyName))
|
|
50
|
+
deferredMV2.push({ node, messageId: 'mv2Key',
|
|
51
|
+
data: { key: `background.${keyName}`,
|
|
52
|
+
reason: 'Use background.service_worker instead' }})
|
|
47
53
|
if (inBackground && keyName == 'persistent')
|
|
48
54
|
pendingReports.push({ node, messageId: 'mv2Key',
|
|
49
|
-
data: { key: 'background.persistent',
|
|
55
|
+
data: { key: 'background.persistent',
|
|
56
|
+
reason: 'MV3 service workers are never persistent' }})
|
|
50
57
|
if (isTopLevel && keyName == 'content_security_policy' && node.value?.type == 'String')
|
|
51
58
|
pendingReports.push({ node: node.value, messageId: 'wrongCSPType' })
|
|
52
59
|
if (inCSP && keyName == 'extension_pages' && typeof node.value?.value == 'string')
|
|
@@ -54,6 +61,17 @@ export default {
|
|
|
54
61
|
if (node.value.value.includes(directive))
|
|
55
62
|
pendingReports.push({ node: node.value, messageId: 'unsafeCSP',
|
|
56
63
|
data: { directive }})
|
|
64
|
+
if (inDataCollection && (keyName == 'required' || keyName == 'optional')
|
|
65
|
+
&& Array.isArray(node.value?.elements)
|
|
66
|
+
) for (const el of node.value.elements) {
|
|
67
|
+
const value = typeof el.value == 'string' ? el.value : el.value?.value
|
|
68
|
+
if (typeof value == 'string') {
|
|
69
|
+
const set = keyName == 'required' ? ffDataCollectionRequired : ffDataCollectionOptional
|
|
70
|
+
if (!set.has(value))
|
|
71
|
+
pendingReports.push({ node: el, messageId: 'invalidDataCollectionValue',
|
|
72
|
+
data: { value, kind: keyName }})
|
|
73
|
+
}
|
|
74
|
+
}
|
|
57
75
|
}
|
|
58
76
|
ancestors.push(node)
|
|
59
77
|
},
|
|
@@ -61,20 +79,23 @@ export default {
|
|
|
61
79
|
ancestors.pop()
|
|
62
80
|
if (node.type == 'Document') {
|
|
63
81
|
if (!isMV3) return
|
|
64
|
-
|
|
82
|
+
if (!isFF) pendingReports.push(...deferredMV2)
|
|
83
|
+
for (const report of pendingReports) context.report(report)
|
|
65
84
|
}
|
|
66
85
|
}
|
|
67
86
|
}
|
|
68
87
|
}
|
|
69
88
|
const manifest = readManifest(context) ; if (!manifest || manifest.manifest_version != 3) return {}
|
|
89
|
+
const isFF = manifest.browser_specific_settings != null
|
|
70
90
|
return {
|
|
71
|
-
Program(node) {
|
|
91
|
+
Program(node) { /* eslint-disable security/detect-object-injection */
|
|
72
92
|
for (const key of Object.keys(manifest))
|
|
73
93
|
if (mv2OnlyKeys.has(key))
|
|
74
94
|
context.report({ node, messageId: 'mv2Key', data: { key, reason: 'MV2-only key' }})
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
95
|
+
for (const key of ffMV3bgKeys)
|
|
96
|
+
if (manifest.background?.[key] && !isFF)
|
|
97
|
+
context.report({ node, messageId: 'mv2Key',
|
|
98
|
+
data: { key: `background.${key}`, reason: 'Use background.service_worker instead' }})
|
|
78
99
|
if (manifest.background?.persistent != null)
|
|
79
100
|
context.report({ node, messageId: 'mv2Key',
|
|
80
101
|
data: { key: 'background.persistent', reason: 'MV3 service workers are never persistent' }})
|
|
@@ -86,7 +107,16 @@ export default {
|
|
|
86
107
|
for (const directive of forbiddenCSPdirectives)
|
|
87
108
|
if (cspPages.includes(directive))
|
|
88
109
|
context.report({ node, messageId: 'unsafeCSP', data: { directive }})
|
|
89
|
-
|
|
110
|
+
if (isFF) { // validate data collection permissions
|
|
111
|
+
const dcp = manifest.browser_specific_settings?.gecko?.data_collection_permissions
|
|
112
|
+
for (const [kind, set] of [['required', ffDataCollectionRequired], ['optional', ffDataCollectionOptional]])
|
|
113
|
+
if (Array.isArray(dcp?.[kind]))
|
|
114
|
+
for (const value of dcp[kind])
|
|
115
|
+
if (!set.has(value))
|
|
116
|
+
context.report({ node, messageId: 'invalidDataCollectionValue',
|
|
117
|
+
data: { value, kind }})
|
|
118
|
+
}
|
|
119
|
+
} /* eslint-enable security/detect-object-injection */
|
|
90
120
|
}
|
|
91
121
|
}
|
|
92
122
|
}
|
|
@@ -12,7 +12,7 @@ export default {
|
|
|
12
12
|
schema: [],
|
|
13
13
|
messages: {
|
|
14
14
|
listenerNotTopLevel: '`chrome.*.addListener()` must be registered at module top level so it attaches on service worker wake-up.',
|
|
15
|
-
keepaliveTimer: 'Sub-30s `{{fn}}` will not keep the MV3 service worker alive
|
|
15
|
+
keepaliveTimer: 'Sub-30s `{{fn}}` will not keep the MV3 service worker alive. Use `chrome.alarms` instead.',
|
|
16
16
|
domInSW: '`{{global}}` is not available in a service worker.'
|
|
17
17
|
}
|
|
18
18
|
},
|
|
@@ -30,13 +30,18 @@ export default {
|
|
|
30
30
|
&& node.arguments[1]?.type == 'Literal'
|
|
31
31
|
&& typeof node.arguments[1].value == 'number'
|
|
32
32
|
&& node.arguments[1].value < serviceWorkerKeepaliveMinMS
|
|
33
|
+
&& !isInsideNestedFunction(node)
|
|
33
34
|
) context.report({ node, messageId: 'keepaliveTimer', data: { fn: node.callee.name }})
|
|
34
35
|
},
|
|
35
36
|
Identifier(node) {
|
|
36
37
|
if (node.parent?.type == 'MemberExpression' && node.parent.property === node) return
|
|
37
38
|
if (node.parent?.type == 'Property' && node.parent.key === node && !node.parent.computed) return
|
|
38
|
-
if (domGlobals.has(node.name))
|
|
39
|
-
|
|
39
|
+
if (!domGlobals.has(node.name)) return
|
|
40
|
+
for (let scope = context.sourceCode.getScope(node) ;
|
|
41
|
+
scope && scope.type != 'global' ;
|
|
42
|
+
scope = scope.upper
|
|
43
|
+
) if (scope.set.has(node.name)) return // shadowed by a local binding
|
|
44
|
+
context.report({ node, messageId: 'domInSW', data: { global: node.name }})
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
47
|
}
|
|
@@ -13,7 +13,7 @@ export default {
|
|
|
13
13
|
docs: {
|
|
14
14
|
description: 'Validate the manifest version field format',
|
|
15
15
|
category: 'Possible Errors',
|
|
16
|
-
recommended:
|
|
16
|
+
recommended: false,
|
|
17
17
|
manifest: true
|
|
18
18
|
},
|
|
19
19
|
schema: [{
|
|
@@ -37,8 +37,8 @@ export default {
|
|
|
37
37
|
}],
|
|
38
38
|
defaultOptions: [{}],
|
|
39
39
|
messages: {
|
|
40
|
-
invalidVersion: 'Invalid manifest `version`: must be a valid {{versionType}} version (expected format: {{example}})',
|
|
41
|
-
futureVersion: 'Invalid manifest `version`: date cannot be in the future'
|
|
40
|
+
invalidVersion: 'Invalid manifest `version`: must be a valid {{versionType}} version (expected format: {{example}}).',
|
|
41
|
+
futureVersion: 'Invalid manifest `version`: date cannot be in the future.'
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
|
package/src/utils.js
CHANGED
|
@@ -10,6 +10,17 @@ export function bundleRules(rules, predicate, pluginName) {
|
|
|
10
10
|
.map(([ruleId]) => [`${pluginName}/${ruleId}`, 'error']))
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export function initBundleDeprecation(plugin, [ruleId, bundleNames], pluginName) {
|
|
14
|
+
for (const bundleName of bundleNames) {
|
|
15
|
+
/* eslint-disable security/detect-object-injection */
|
|
16
|
+
if (plugin.configs[bundleName])
|
|
17
|
+
plugin.configs[bundleName].rules[`${pluginName}/${ruleId}`] = 'error'
|
|
18
|
+
/* eslint-enable security/detect-object-injection */
|
|
19
|
+
if (process.env.DEBUG_MODE)
|
|
20
|
+
console.warn(`[${pluginName}] '${ruleId}' will be removed from '${bundleName}' in a future major release`)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
export function findExtensionRoot(context) {
|
|
14
25
|
let dir = path.dirname(getFilename(context))
|
|
15
26
|
while (dir) {
|