eslint-plugin-chrome-extensions 2.12.0 → 2.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-chrome-extensions",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.2",
|
|
4
4
|
"description": "ESLint rules for Chrome extensions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eslint",
|
|
@@ -61,10 +61,13 @@
|
|
|
61
61
|
"test": "node tests/run"
|
|
62
62
|
},
|
|
63
63
|
"overrides": {
|
|
64
|
-
"@eslint/
|
|
64
|
+
"@eslint/markdown": {
|
|
65
|
+
"katex": "^0.19.1"
|
|
66
|
+
},
|
|
67
|
+
"eslint": {
|
|
65
68
|
"minimatch": "^11.0.5"
|
|
66
69
|
},
|
|
67
|
-
"
|
|
70
|
+
"eslint-plugin-import-x": {
|
|
68
71
|
"minimatch": "^11.0.5"
|
|
69
72
|
},
|
|
70
73
|
"eslint-plugin-sonarjs": {
|
|
@@ -80,15 +83,15 @@
|
|
|
80
83
|
"@e18e/eslint-plugin": "^0.8.1",
|
|
81
84
|
"@eslint/css": "^2.0.1",
|
|
82
85
|
"@eslint/json": "^2.1.1",
|
|
83
|
-
"@eslint/markdown": "^
|
|
84
|
-
"@html-eslint/eslint-plugin": "^0.
|
|
86
|
+
"@eslint/markdown": "^8.0.3",
|
|
87
|
+
"@html-eslint/eslint-plugin": "^0.68.0",
|
|
85
88
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
86
|
-
"@typescript-eslint/parser": "^8.72.
|
|
87
|
-
"@typescript-eslint/utils": "^8.72.
|
|
89
|
+
"@typescript-eslint/parser": "^8.72.2",
|
|
90
|
+
"@typescript-eslint/utils": "^8.72.2",
|
|
88
91
|
"eslint": "^10.14.0",
|
|
89
|
-
"eslint-plugin-constructors": "^1.7.
|
|
92
|
+
"eslint-plugin-constructors": "^1.7.2",
|
|
90
93
|
"eslint-plugin-eslint-plugin": "^7.6.2",
|
|
91
|
-
"eslint-plugin-import-x": "^4.18.
|
|
94
|
+
"eslint-plugin-import-x": "^4.18.1",
|
|
92
95
|
"eslint-plugin-n": "^18.5.0",
|
|
93
96
|
"eslint-plugin-no-secrets": "^2.3.3",
|
|
94
97
|
"eslint-plugin-no-unsanitized": "^4.1.5",
|
|
@@ -97,8 +100,8 @@
|
|
|
97
100
|
"eslint-plugin-regexp": "^3.3.3",
|
|
98
101
|
"eslint-plugin-security": "^4.2.1",
|
|
99
102
|
"eslint-plugin-sonarjs": "^4.2.2",
|
|
100
|
-
"eslint-plugin-woodpecker": "^1.1.
|
|
101
|
-
"oxlint": "^1.
|
|
103
|
+
"eslint-plugin-woodpecker": "^1.1.4",
|
|
104
|
+
"oxlint": "^1.93.0",
|
|
102
105
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
103
106
|
"typescript-latest": "npm:typescript@^7.0.2"
|
|
104
107
|
},
|
|
@@ -5,11 +5,6 @@ import regex from '../data/regex.js'
|
|
|
5
5
|
|
|
6
6
|
const defaultOptions = { order: [] }
|
|
7
7
|
|
|
8
|
-
function keyOf(member) {
|
|
9
|
-
const keyNode = member.name || member.key
|
|
10
|
-
return keyNode?.value ?? keyNode?.name
|
|
11
|
-
}
|
|
12
|
-
|
|
13
8
|
function findViolation(members, keys) {
|
|
14
9
|
const ranks = new Map(keys.map((key, index) => [key, index])),
|
|
15
10
|
known = members.map(member => {
|
|
@@ -17,7 +12,26 @@ function findViolation(members, keys) {
|
|
|
17
12
|
return { member, key, rank: ranks.get(key) }
|
|
18
13
|
}).filter(item => item.rank !== undefined)
|
|
19
14
|
for (let i = 1 ; i < known.length ; i++) // oxlint-disable-next-line security/detect-object-injection
|
|
20
|
-
if (known[i -1].rank > known[i].rank) return [known[i -1], known[i]]
|
|
15
|
+
if (known[i - 1].rank > known[i].rank) return [known[i - 1], known[i]]
|
|
16
|
+
}
|
|
17
|
+
function isSubsequence(sub, arr) {
|
|
18
|
+
let i = 0
|
|
19
|
+
for (const item of arr) if (item === sub[i]) i++ // oxlint-disable-line security/detect-object-injection
|
|
20
|
+
return i === sub.length
|
|
21
|
+
}
|
|
22
|
+
function keyOf(member) {
|
|
23
|
+
const keyNode = member.name || member.key
|
|
24
|
+
return keyNode?.value ?? keyNode?.name
|
|
25
|
+
}
|
|
26
|
+
function mergeOrder(master, custom) {
|
|
27
|
+
if (!custom.length || isSubsequence(custom, master)) return master
|
|
28
|
+
const customSet = new Set(custom),
|
|
29
|
+
insertAt = Math.min(...custom.map(key => master.indexOf(key)).filter(idx => idx >= 0))
|
|
30
|
+
return [
|
|
31
|
+
...master.slice(0, insertAt),
|
|
32
|
+
...custom,
|
|
33
|
+
...master.slice(insertAt).filter(key => !customSet.has(key))
|
|
34
|
+
]
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
export default {
|
|
@@ -46,14 +60,12 @@ export default {
|
|
|
46
60
|
create(context) {
|
|
47
61
|
if (!regex.filename.manifest.test(extension.file.getName(context))) return {}
|
|
48
62
|
const { order } = { ...defaultOptions, ...context.options[0] },
|
|
49
|
-
|
|
63
|
+
keys = mergeOrder(manifest.keys.order, order)
|
|
50
64
|
return {
|
|
51
65
|
'Document:exit'(document) {
|
|
52
66
|
const root = document.body ; if (root?.type != 'Object') return
|
|
53
67
|
const members = root.members || root.properties || []
|
|
54
|
-
|
|
55
|
-
if (!violation && order.length) { keys = order ; violation = findViolation(members, order) }
|
|
56
|
-
if (!violation) return
|
|
68
|
+
const violation = findViolation(members, keys) ; if (!violation) return
|
|
57
69
|
const [first, second] = violation
|
|
58
70
|
context.report({
|
|
59
71
|
node: second.member, messageId: 'wrongOrder',
|
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import extension from '../utils/extension.js'
|
|
2
2
|
import nodeUtils from '../utils/node.js'
|
|
3
3
|
|
|
4
|
-
const re = {
|
|
4
|
+
const re = {
|
|
5
|
+
appendMethod: /^(?:append|appendChild|prepend|replaceChildren|insertBefore)$/,
|
|
6
|
+
invokeMethod: /^(?:apply|call)$/,
|
|
7
|
+
styleName: /style/i
|
|
8
|
+
}
|
|
5
9
|
|
|
10
|
+
function flattenArgs(args) {
|
|
11
|
+
return args.filter(Boolean).flatMap(arg =>
|
|
12
|
+
arg.type != 'SpreadElement' ? [arg]
|
|
13
|
+
: arg.argument?.type == 'ArrayExpression' ? arg.argument.elements.filter(Boolean)
|
|
14
|
+
: [arg.argument]
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
function getPropName(node) { return node?.name ?? node?.value }
|
|
6
18
|
function isCreateStyle(node) {
|
|
7
19
|
return node?.type == 'CallExpression'
|
|
8
|
-
&& node.callee?.property
|
|
20
|
+
&& getPropName(node.callee?.property) == 'createElement'
|
|
9
21
|
&& nodeUtils.getStringLiteral(node.arguments[0]) == 'style'
|
|
10
22
|
}
|
|
11
23
|
function isObjectAssign(node) {
|
|
12
24
|
return node?.type == 'CallExpression'
|
|
13
25
|
&& node.callee?.type == 'MemberExpression'
|
|
14
26
|
&& node.callee.object?.name == 'Object'
|
|
15
|
-
&& node.callee.property
|
|
27
|
+
&& getPropName(node.callee.property) == 'assign'
|
|
16
28
|
}
|
|
17
29
|
|
|
18
30
|
export default {
|
|
@@ -37,33 +49,60 @@ export default {
|
|
|
37
49
|
return node?.type == 'Identifier'
|
|
38
50
|
&& (node.name == 'document' || docAliases.has(node.name))
|
|
39
51
|
}
|
|
40
|
-
function
|
|
41
|
-
|
|
52
|
+
function isDocRooted(node) {
|
|
53
|
+
if (!node) return false
|
|
54
|
+
if (node.type == 'Identifier') return isDocumentRef(node)
|
|
55
|
+
if (node.type == 'MemberExpression')
|
|
56
|
+
return node.object?.type == 'Identifier'
|
|
57
|
+
&& (node.object.name == 'window' || node.object.name == 'self')
|
|
58
|
+
&& getPropName(node.property) == 'document'
|
|
59
|
+
|| isDocRooted(node.object)
|
|
60
|
+
if (node.type == 'CallExpression' && getPropName(node.callee?.property) != 'createElement')
|
|
61
|
+
return isDocRooted(node.callee)
|
|
62
|
+
return false
|
|
42
63
|
}
|
|
43
64
|
function isStyleTarget(node) {
|
|
44
|
-
if (
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
if (isObjectAssign(node)
|
|
48
|
-
&& node.arguments[0]?.type == 'Identifier'
|
|
49
|
-
&& styleElems.has(node.arguments[0].name)
|
|
65
|
+
if (!node) return false
|
|
66
|
+
if (isCreateStyle(node)
|
|
67
|
+
|| node.type == 'Identifier' && (styleElems.has(node.name) || re.styleName.test(node.name))
|
|
50
68
|
) return true
|
|
69
|
+
if (isObjectAssign(node)) {
|
|
70
|
+
const target = node.arguments[0]
|
|
71
|
+
if (isCreateStyle(target)
|
|
72
|
+
|| target?.type == 'Identifier' && styleElems.has(target.name)
|
|
73
|
+
) return true
|
|
74
|
+
}
|
|
51
75
|
return false
|
|
52
76
|
}
|
|
77
|
+
function addAliases(pattern, source) {
|
|
78
|
+
if (!isDocRooted(source)) return
|
|
79
|
+
if (pattern.type == 'Identifier') docAliases.add(pattern.name)
|
|
80
|
+
else if (pattern.type == 'ObjectPattern')
|
|
81
|
+
for (const prop of pattern.properties) {
|
|
82
|
+
if (prop.type != 'Property') continue
|
|
83
|
+
if (prop.value?.type == 'Identifier') docAliases.add(prop.value.name)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
53
86
|
return {
|
|
54
87
|
VariableDeclarator(node) {
|
|
55
|
-
|
|
56
|
-
if (isCreateStyle(
|
|
57
|
-
|
|
58
|
-
docAliases.add(node.id.name)
|
|
88
|
+
const init = nodeUtils.unwrapTS(node.init)
|
|
89
|
+
if (node.id.type == 'Identifier' && isCreateStyle(init)) styleElems.add(node.id.name)
|
|
90
|
+
addAliases(node.id, init)
|
|
59
91
|
},
|
|
60
92
|
CallExpression(node) {
|
|
61
93
|
if (node.callee.type != 'MemberExpression') return
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
94
|
+
const outerMethod = getPropName(node.callee.property),
|
|
95
|
+
isInvoke = re.invokeMethod.test(outerMethod),
|
|
96
|
+
callee = isInvoke ? node.callee.object : node.callee
|
|
97
|
+
if (callee?.type != 'MemberExpression'
|
|
98
|
+
|| !re.appendMethod.test(getPropName(callee.property))
|
|
99
|
+
|| !isDocRooted(callee.object)
|
|
65
100
|
) return
|
|
66
|
-
|
|
101
|
+
const args = !isInvoke ? node.arguments
|
|
102
|
+
: outerMethod == 'apply'
|
|
103
|
+
? node.arguments[1]?.type == 'ArrayExpression' ? node.arguments[1].elements : []
|
|
104
|
+
: node.arguments.slice(1)
|
|
105
|
+
for (const arg of flattenArgs(args))
|
|
67
106
|
if (isStyleTarget(arg))
|
|
68
107
|
context.report({ node: arg, messageId: 'styleLeak' })
|
|
69
108
|
}
|
|
@@ -6,27 +6,57 @@ import path from 'node:path'
|
|
|
6
6
|
import apis from '../data/apis.js'
|
|
7
7
|
import regex from '../data/regex.js'
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
const re = {
|
|
10
|
+
chromeAPI: /\bchrome\.\w+(?:\.\w+)*/g, // oxlint-disable-line security/detect-unsafe-regex
|
|
11
|
+
htmlExt: /\.html?$/i,
|
|
12
|
+
htmlScriptSrc: /<script\b[^>]*?\ssrc\s*=\s*(?:"([^"]+)"|'([^']+)')/gi,
|
|
13
|
+
urlLike: /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function collectScannableFiles(manifest) {
|
|
17
|
+
const files = new Set(),
|
|
18
|
+
add = file => { if (typeof file == 'string' && file) files.add(file) }
|
|
19
|
+
add(manifest.background?.service_worker)
|
|
20
|
+
add(manifest.background?.page)
|
|
21
|
+
for (const script of manifest.background?.scripts || []) add(script)
|
|
13
22
|
for (const contentScript of manifest.content_scripts || [])
|
|
14
|
-
for (const file of contentScript.js || [])
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
for (const file of contentScript.js || []) add(file)
|
|
24
|
+
add(manifest.action?.default_popup)
|
|
25
|
+
add(manifest.browser_action?.default_popup)
|
|
26
|
+
add(manifest.page_action?.default_popup)
|
|
27
|
+
add(manifest.options_ui?.page)
|
|
28
|
+
add(manifest.options_page)
|
|
29
|
+
add(manifest.devtools_page)
|
|
30
|
+
add(manifest.side_panel?.default_path)
|
|
31
|
+
for (const page of Object.values(manifest.chrome_url_overrides || {})) add(page)
|
|
32
|
+
return files
|
|
33
|
+
}
|
|
34
|
+
function collectUsedAPIs(manifest, root) {
|
|
35
|
+
const queue = [...collectScannableFiles(manifest)],
|
|
36
|
+
visited = new Set(),
|
|
37
|
+
used = new Set()
|
|
38
|
+
while (queue.length) {
|
|
39
|
+
const relPath = queue.shift() ; if (!relPath) continue
|
|
40
|
+
const absPath = path.resolve(root, relPath) ; if (visited.has(absPath)) continue
|
|
41
|
+
visited.add(absPath)
|
|
42
|
+
let src
|
|
43
|
+
try { src = fs.readFileSync(absPath, 'utf8') } // oxlint-disable-line security/detect-non-literal-fs-filename
|
|
44
|
+
catch { continue }
|
|
45
|
+
for (const match of src.matchAll(re.chromeAPI)) used.add(match[0])
|
|
46
|
+
if (re.htmlExt.test(relPath))
|
|
47
|
+
for (const nested of extractScriptSrcs(src, relPath)) queue.push(nested)
|
|
27
48
|
}
|
|
28
49
|
return used
|
|
29
50
|
}
|
|
51
|
+
function extractScriptSrcs(html, htmlRelPath) {
|
|
52
|
+
const srcs = []
|
|
53
|
+
for (const match of html.matchAll(re.htmlScriptSrc)) {
|
|
54
|
+
const src = (match[1] ?? match[2])?.trim()
|
|
55
|
+
if (!src || re.urlLike.test(src)) continue
|
|
56
|
+
srcs.push(src.startsWith('/') ? src.slice(1) : path.join(path.dirname(htmlRelPath), src))
|
|
57
|
+
}
|
|
58
|
+
return srcs
|
|
59
|
+
}
|
|
30
60
|
|
|
31
61
|
export default {
|
|
32
62
|
meta: {
|
|
@@ -50,10 +80,10 @@ export default {
|
|
|
50
80
|
const root = extension.findRoot(context) ; if (!root) return {}
|
|
51
81
|
const usedAPIs = collectUsedAPIs(manifest, root)
|
|
52
82
|
function isUsed(permission) {
|
|
53
|
-
let
|
|
54
|
-
for (const [,
|
|
55
|
-
if (
|
|
56
|
-
if (!
|
|
83
|
+
let knownPermission = false
|
|
84
|
+
for (const [, mappedPermission] of apis.permissionMap)
|
|
85
|
+
if (mappedPermission == permission) { knownPermission = true ; break }
|
|
86
|
+
if (!knownPermission) return true
|
|
57
87
|
for (const api of usedAPIs)
|
|
58
88
|
if (apiUtils.findPermission(api) == permission) return true
|
|
59
89
|
return false
|
|
@@ -72,9 +102,9 @@ export default {
|
|
|
72
102
|
const keyNode = node.name || node.key ; if (!keyNode) return
|
|
73
103
|
const keyName = keyNode.value ?? keyNode.name
|
|
74
104
|
if (keyName != 'permissions' && keyName != 'optional_permissions') return
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
for (const elem of
|
|
105
|
+
const arrayNode = node.value ; if (arrayNode?.type != 'Array') return
|
|
106
|
+
const elems = arrayNode.elements || arrayNode.items || []
|
|
107
|
+
for (const elem of elems) {
|
|
78
108
|
const valueNode = elem.value || elem,
|
|
79
109
|
permission = valueNode?.value
|
|
80
110
|
if (typeof permission != 'string') continue
|
|
@@ -58,8 +58,9 @@ export default {
|
|
|
58
58
|
allowedLicenses = array.to(rawAllowedLicenses),
|
|
59
59
|
{ filename } = context,
|
|
60
60
|
basename = path.basename(filename)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
return !regex.filename.license.test(basename) || regex.filename.script.test(basename) ? {} : {
|
|
62
|
+
Program: node => validateContent(context, node, { requireSPDX, allowedLicenses }),
|
|
63
|
+
Document: node => validateContent(context, node, { requireSPDX, allowedLicenses })
|
|
64
|
+
}
|
|
64
65
|
}
|
|
65
66
|
}
|