eslint-plugin-chrome-extensions 2.6.0 → 2.7.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 +403 -11
- package/package.json +4 -3
- package/src/data/regex.js +1 -0
- package/src/index.js +0 -4
- package/src/rules/css-spacing.js +367 -0
- package/src/rules/manifest-key-order.js +0 -1
- package/src/rules/manifest-spacing.js +0 -1
- package/src/rules/no-duplicate-manifest-keys.js +0 -1
- package/src/rules/no-global-keyframes.js +84 -0
- package/src/rules/no-host-page-selectors.js +106 -0
- package/src/rules/no-remote-urls.js +102 -0
- package/src/rules/no-scrollbar-styling.js +73 -0
- package/src/rules/no-unused-i18n-keys.js +0 -1
- package/src/rules/require-license-file.js +0 -1
- package/src/rules/require-short-name-when-useful.js +0 -1
- package/src/rules/valid-license-file.js +2 -4
- package/src/utils/array.js +1 -0
- package/src/utils/init.js +10 -6
- package/src/data/deprecations.js +0 -8
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import array from '../utils/array.js'
|
|
2
|
+
|
|
3
|
+
const re = { urlScheme: /^(?:([a-z][a-z0-9+.-]*):|\/\/)/i }
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'problem',
|
|
8
|
+
docs: {
|
|
9
|
+
description: 'Disallow remote resources (`@import`, `url()`, `@namespace`) in extension CSS',
|
|
10
|
+
category: 'Possible Errors',
|
|
11
|
+
css: true
|
|
12
|
+
},
|
|
13
|
+
defaultOptions: [{
|
|
14
|
+
allowedSchemes: ['blob', 'chrome-extension', 'data'],
|
|
15
|
+
patterns: ['at-import', 'url']
|
|
16
|
+
}],
|
|
17
|
+
schema: [{
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
allowedSchemes: {
|
|
21
|
+
oneOf: [
|
|
22
|
+
{ type: 'string', description: 'A single URL scheme' },
|
|
23
|
+
{ type: 'array', items: { type: 'string' }, description: 'List of URL schemes' }
|
|
24
|
+
],
|
|
25
|
+
description: 'URL schemes to allow (e.g. `chrome-extension`, `data`, `blob`)'
|
|
26
|
+
},
|
|
27
|
+
patterns: {
|
|
28
|
+
oneOf: [
|
|
29
|
+
{ enum: ['at-import', 'url', 'namespace'], description: 'A single detection path' },
|
|
30
|
+
{ type: 'array', items: { enum: ['at-import', 'url', 'namespace'] }, description: 'List of detection paths' }
|
|
31
|
+
],
|
|
32
|
+
description: 'Detection paths to enable'
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
additionalProperties: false
|
|
36
|
+
}],
|
|
37
|
+
messages: {
|
|
38
|
+
remoteImport: 'Remote `@import` is disallowed. Bundle the stylesheet or use `chrome.runtime.getURL()`.',
|
|
39
|
+
remoteURL: 'Remote `url()` is disallowed. Bundle the asset or use `chrome.runtime.getURL()`.',
|
|
40
|
+
remoteNamespace: 'Remote `@namespace` is disallowed. Reference a bundled stylesheet.'
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
create(context) {
|
|
45
|
+
const opts = context.options[0] || {},
|
|
46
|
+
allowedSchemes = new Set(array.to(opts.allowedSchemes ?? ['blob', 'chrome-extension', 'data'])),
|
|
47
|
+
patterns = new Set(array.to(opts.patterns ?? ['at-import', 'url'])),
|
|
48
|
+
handledURLs = new WeakSet()
|
|
49
|
+
function getPreludeURLnode(prelude) {
|
|
50
|
+
if (!prelude) return null
|
|
51
|
+
if (['Url', 'CSSUrl', 'String', 'CSSString'].includes(prelude.type))
|
|
52
|
+
return prelude
|
|
53
|
+
for (const child of prelude.children ?? [])
|
|
54
|
+
if (['Url', 'CSSUrl', 'String', 'CSSString'].includes(child.type))
|
|
55
|
+
return child
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
function getURLstr(node) {
|
|
59
|
+
if (!node) return null
|
|
60
|
+
if (typeof node == 'string') return node
|
|
61
|
+
if (typeof node.url == 'string') return node.url
|
|
62
|
+
if (typeof node.value == 'string') return node.value
|
|
63
|
+
if (node.value && typeof node.value == 'object' && typeof node.value.value == 'string')
|
|
64
|
+
return node.value.value
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
function handleAtrule(node) {
|
|
68
|
+
if (!node.prelude) return
|
|
69
|
+
if (node.name == 'import' && patterns.has('at-import')) {
|
|
70
|
+
const urlNode = getPreludeURLnode(node.prelude)
|
|
71
|
+
if (urlNode && isRemote(getURLstr(urlNode))) {
|
|
72
|
+
handledURLs.add(urlNode)
|
|
73
|
+
context.report({ node, messageId: 'remoteImport' })
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (node.name == 'namespace' && patterns.has('namespace')) {
|
|
77
|
+
const urlNode = getPreludeURLnode(node.prelude)
|
|
78
|
+
if (urlNode && isRemote(getURLstr(urlNode))) {
|
|
79
|
+
handledURLs.add(urlNode)
|
|
80
|
+
context.report({ node, messageId: 'remoteNamespace' })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function handleURL(node) {
|
|
85
|
+
if (!patterns.has('url')) return
|
|
86
|
+
if (handledURLs.has(node)) return
|
|
87
|
+
if (node.parent?.type == 'AtrulePrelude' || node.parent?.type == 'CSSAtRulePrelude') return
|
|
88
|
+
if (isRemote(getURLstr(node)))
|
|
89
|
+
context.report({ node, messageId: 'remoteURL' })
|
|
90
|
+
}
|
|
91
|
+
function isRemote(str) {
|
|
92
|
+
if (typeof str != 'string') return false
|
|
93
|
+
const match = str.match(re.urlScheme)
|
|
94
|
+
if (!match) return false
|
|
95
|
+
if (match[0] == '//') return true
|
|
96
|
+
const scheme = (match[1] || '').toLowerCase()
|
|
97
|
+
if (['http', 'https', 'ftp'].includes(scheme)) return true
|
|
98
|
+
return !allowedSchemes.has(scheme)
|
|
99
|
+
}
|
|
100
|
+
return { Atrule: handleAtrule, CSSAtRule: handleAtrule, Url: handleURL, CSSUrl: handleURL }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import array from '../utils/array.js'
|
|
2
|
+
import regex from '../data/regex.js'
|
|
3
|
+
|
|
4
|
+
const re = {
|
|
5
|
+
scrollbarPseudo: /^(?:-webkit-)?scrollbar(?:-[\w-]+)?$/i } // eslint-disable-line security/detect-unsafe-regex
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'problem',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Disallow scrollbar pseudo-elements (e.g. `::-webkit-scrollbar`) '
|
|
12
|
+
+ 'on unscoped selectors in content-script CSS',
|
|
13
|
+
category: 'Possible Errors',
|
|
14
|
+
css: true
|
|
15
|
+
},
|
|
16
|
+
defaultOptions: [{ allowedSelectors: [] }],
|
|
17
|
+
schema: [{
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
allowedSelectors: {
|
|
21
|
+
oneOf: [
|
|
22
|
+
{ type: 'string', description: 'A single selector' },
|
|
23
|
+
{ type: 'array', items: { type: 'string' }, description: 'List of selectors' }
|
|
24
|
+
],
|
|
25
|
+
description: 'Selectors to explicitly allow (e.g. `body::-webkit-scrollbar`)'
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
additionalProperties: false
|
|
29
|
+
}],
|
|
30
|
+
messages: {
|
|
31
|
+
noScrollbarStyling: 'Scrollbar pseudo-element on `{{selector}}` styles the host page\'s scrollbar. '
|
|
32
|
+
+ 'Scope it to a namespaced class (e.g. `.myext-panel::...`) or use `allowedSelectors`.'
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
create(context) {
|
|
37
|
+
const opts = context.options[0] || {},
|
|
38
|
+
allowedSelectors = new Set(array.to(opts.allowedSelectors))
|
|
39
|
+
function checkSelector(selector) {
|
|
40
|
+
if (!selector) return
|
|
41
|
+
const text = getSelectorText(selector).trim() ; if (allowedSelectors.has(text)) return
|
|
42
|
+
let hasScrollbarPseudo = false,
|
|
43
|
+
hasQualifier = false,
|
|
44
|
+
pastFirstCompound = false
|
|
45
|
+
for (const simple of selector.children ?? []) {
|
|
46
|
+
const type = plainType(simple)
|
|
47
|
+
if (type == 'Combinator') { pastFirstCompound = true; continue }
|
|
48
|
+
if (type == 'PseudoElementSelector' && re.scrollbarPseudo.test(simple.name ?? '')) {
|
|
49
|
+
hasScrollbarPseudo = true
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
if (pastFirstCompound) continue
|
|
53
|
+
if (type == 'ClassSelector' || type == 'IdSelector' || type == 'AttributeSelector')
|
|
54
|
+
hasQualifier = true
|
|
55
|
+
else if (type == 'PseudoClassSelector' && (simple.name ?? '').toLowerCase().startsWith('host'))
|
|
56
|
+
hasQualifier = true
|
|
57
|
+
}
|
|
58
|
+
if (hasScrollbarPseudo && !hasQualifier)
|
|
59
|
+
context.report({ node: selector, messageId: 'noScrollbarStyling', data: { selector: text }})
|
|
60
|
+
}
|
|
61
|
+
function getSelectorText(selector) {
|
|
62
|
+
const range = selector.range
|
|
63
|
+
if (range && range.length == 2) return context.sourceCode.text.slice(range[0], range[1])
|
|
64
|
+
return context.sourceCode.getText(selector)
|
|
65
|
+
}
|
|
66
|
+
function handleRule(node) {
|
|
67
|
+
for (const selector of node.prelude?.children ?? [])
|
|
68
|
+
checkSelector(selector)
|
|
69
|
+
}
|
|
70
|
+
function plainType(node) { return node?.type?.replace(regex.prefix.css, '') ?? '' }
|
|
71
|
+
return { Rule: handleRule, CSSRule: handleRule }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
+
import array from '../utils/array.js'
|
|
1
2
|
import license from '../utils/license.js'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
|
|
4
5
|
import regex from '../data/regex.js'
|
|
5
6
|
|
|
6
|
-
function toArray(value) { return value == null ? [] : Array.isArray(value) ? value : [value] }
|
|
7
|
-
|
|
8
7
|
function validateContent(context, node, { requireSPDX, allowedLicenses }) {
|
|
9
8
|
const text = context.sourceCode.text,
|
|
10
9
|
id = license.spdx.parse(text)
|
|
@@ -28,7 +27,6 @@ export default {
|
|
|
28
27
|
fixable: 'code',
|
|
29
28
|
docs: {
|
|
30
29
|
description: 'Validates SPDX identifiers in license files',
|
|
31
|
-
recommended: false,
|
|
32
30
|
license: true
|
|
33
31
|
},
|
|
34
32
|
defaultOptions: [{ requireSPDX: false, allowedLicenses: [] }],
|
|
@@ -56,7 +54,7 @@ export default {
|
|
|
56
54
|
create(context) {
|
|
57
55
|
const opts = context.options[0],
|
|
58
56
|
{ requireSPDX } = opts,
|
|
59
|
-
allowedLicenses =
|
|
57
|
+
allowedLicenses = array.to(opts.allowedLicenses),
|
|
60
58
|
{ filename } = context,
|
|
61
59
|
basename = path.basename(filename)
|
|
62
60
|
if (!regex.filename.license.test(basename) || regex.filename.script.test(basename))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default { to(val) { return Array.isArray(val) ? val : val == null ? [] : [val] }}
|
package/src/utils/init.js
CHANGED
|
@@ -4,11 +4,14 @@ import json from '@eslint/json'
|
|
|
4
4
|
import rulesUtils from './rules.js'
|
|
5
5
|
import text from './text.js'
|
|
6
6
|
|
|
7
|
+
import cssSpacing from '../rules/css-spacing.js'
|
|
7
8
|
import manifestKeyOrder from '../rules/manifest-key-order.js'
|
|
8
9
|
import manifestSpacing from '../rules/manifest-spacing.js'
|
|
9
10
|
import noContentStyleLeak from '../rules/no-content-style-leak.js'
|
|
10
11
|
import noDeprecatedAPIs from '../rules/no-deprecated-apis.js'
|
|
11
12
|
import noDuplicateManifestKeys from '../rules/no-duplicate-manifest-keys.js'
|
|
13
|
+
import noGlobalKeyframes from '../rules/no-global-keyframes.js'
|
|
14
|
+
import noHostPageSelectors from '../rules/no-host-page-selectors.js'
|
|
12
15
|
import noInsecureFetch from '../rules/no-insecure-fetch.js'
|
|
13
16
|
import noInsecureMessaging from '../rules/no-insecure-messaging.js'
|
|
14
17
|
import noInvalidLocales from '../rules/no-invalid-locales.js'
|
|
@@ -18,6 +21,8 @@ import noMissingI18nKeys from '../rules/no-missing-i18n-keys.js'
|
|
|
18
21
|
import noMissingPermissions from '../rules/no-missing-permissions.js'
|
|
19
22
|
import noMissingWebAccessibleResources from '../rules/no-missing-web-accessible-resources.js'
|
|
20
23
|
import noRemoteCode from '../rules/no-remote-code.js'
|
|
24
|
+
import noRemoteURLs from '../rules/no-remote-urls.js'
|
|
25
|
+
import noScrollbarStyling from '../rules/no-scrollbar-styling.js'
|
|
21
26
|
import noSensitivePermissions from '../rules/no-sensitive-permissions.js'
|
|
22
27
|
import noUnrestrictedHostPermissions from '../rules/no-unrestricted-host-permissions.js'
|
|
23
28
|
import noUnusedI18nKeys from '../rules/no-unused-i18n-keys.js'
|
|
@@ -36,12 +41,6 @@ import validVersionField from '../rules/valid-version-field.js'
|
|
|
36
41
|
const namespace = 'chrome-extensions'
|
|
37
42
|
|
|
38
43
|
export default {
|
|
39
|
-
bundleDeprecation(plugin, [ruleId, bundleNames], pluginName) { /* eslint-disable security/detect-object-injection */
|
|
40
|
-
for (const bundleName of bundleNames)
|
|
41
|
-
if (plugin.configs[bundleName])
|
|
42
|
-
plugin.configs[bundleName].rules[`${pluginName}/${ruleId}`] = 'error'
|
|
43
|
-
}, /* eslint-enable security/detect-object-injection */
|
|
44
|
-
|
|
45
44
|
configs(plugin) {
|
|
46
45
|
const plugins = { [namespace]: plugin },
|
|
47
46
|
rules = plugin.rules,
|
|
@@ -72,11 +71,14 @@ export default {
|
|
|
72
71
|
|
|
73
72
|
rules(plugin) {
|
|
74
73
|
plugin.rules = {
|
|
74
|
+
'css-spacing': cssSpacing,
|
|
75
75
|
'manifest-key-order': manifestKeyOrder,
|
|
76
76
|
'manifest-spacing': manifestSpacing,
|
|
77
77
|
'no-content-style-leak': noContentStyleLeak,
|
|
78
78
|
'no-deprecated-apis': noDeprecatedAPIs,
|
|
79
79
|
'no-duplicate-manifest-keys': noDuplicateManifestKeys,
|
|
80
|
+
'no-global-keyframes': noGlobalKeyframes,
|
|
81
|
+
'no-host-page-selectors': noHostPageSelectors,
|
|
80
82
|
'no-insecure-fetch': noInsecureFetch,
|
|
81
83
|
'no-insecure-messaging': noInsecureMessaging,
|
|
82
84
|
'no-invalid-locales': noInvalidLocales,
|
|
@@ -86,6 +88,8 @@ export default {
|
|
|
86
88
|
'no-missing-permissions': noMissingPermissions,
|
|
87
89
|
'no-missing-web-accessible-resources': noMissingWebAccessibleResources,
|
|
88
90
|
'no-remote-code': noRemoteCode,
|
|
91
|
+
'no-remote-urls': noRemoteURLs,
|
|
92
|
+
'no-scrollbar-styling': noScrollbarStyling,
|
|
89
93
|
'no-sensitive-permissions': noSensitivePermissions,
|
|
90
94
|
'no-unrestricted-host-permissions': noUnrestrictedHostPermissions,
|
|
91
95
|
'no-unused-i18n-keys': noUnusedI18nKeys,
|