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.
@@ -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
+ }
@@ -10,7 +10,6 @@ export default {
10
10
  docs: {
11
11
  description: 'Flag i18n keys in `messages.json` that are never referenced',
12
12
  category: 'Stylistic Issues',
13
- recommended: false,
14
13
  manifest: true
15
14
  },
16
15
  schema: [{
@@ -7,7 +7,6 @@ export default {
7
7
  type: 'problem',
8
8
  docs: {
9
9
  description: 'Requires a license file in the extension',
10
- recommended: false,
11
10
  manifest: true
12
11
  },
13
12
  schema: [],
@@ -12,7 +12,6 @@ export default {
12
12
  docs: {
13
13
  description: 'Requires a `short_name` when `name` exceeds 12 chars',
14
14
  category: 'Stylistic Issues',
15
- recommended: false,
16
15
  manifest: true
17
16
  },
18
17
  schema: [{
@@ -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 = toArray(opts.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,
@@ -1,8 +0,0 @@
1
- // Delete a line to end support
2
-
3
- export default {
4
- bundles: {
5
- // <rule-id-a>: [<old-bundle>],
6
- // <rule-id-b>: [<old-bundle>]
7
- }
8
- }