eslint-plugin-chrome-extensions 2.7.0 → 2.8.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.
Files changed (42) hide show
  1. package/README.md +266 -43
  2. package/package.json +9 -8
  3. package/src/data/apis.js +74 -88
  4. package/src/data/manifest.js +3 -1
  5. package/src/data/regex.js +2 -1
  6. package/src/index.d.ts +42 -0
  7. package/src/rules/css-spacing.js +29 -21
  8. package/src/rules/manifest-key-order.js +1 -1
  9. package/src/rules/manifest-spacing.js +1 -1
  10. package/src/rules/no-content-style-leak.js +12 -11
  11. package/src/rules/no-deprecated-apis.js +2 -2
  12. package/src/rules/no-host-page-selectors.js +5 -6
  13. package/src/rules/no-inline-event-handlers.js +116 -0
  14. package/src/rules/no-inline-scripts.js +65 -0
  15. package/src/rules/no-insecure-fetch.js +7 -6
  16. package/src/rules/no-insecure-messaging.js +4 -4
  17. package/src/rules/no-invalid-locales.js +3 -5
  18. package/src/rules/no-missing-default-locale.js +1 -1
  19. package/src/rules/no-missing-permissions.js +2 -3
  20. package/src/rules/no-missing-web-accessible-resources.js +2 -2
  21. package/src/rules/no-remote-code.js +77 -28
  22. package/src/rules/no-remote-objects.js +55 -0
  23. package/src/rules/no-remote-urls.js +20 -20
  24. package/src/rules/no-scrollbar-styling.js +8 -10
  25. package/src/rules/no-unrestricted-host-permissions.js +1 -1
  26. package/src/rules/no-unused-permissions.js +5 -4
  27. package/src/rules/require-license-file.js +1 -1
  28. package/src/rules/require-mv3.js +3 -2
  29. package/src/rules/valid-filenames.js +3 -3
  30. package/src/rules/valid-manifest-icons.js +2 -2
  31. package/src/rules/valid-mv3-schema.js +5 -5
  32. package/src/rules/valid-url-fields.js +2 -2
  33. package/src/rules/valid-version-field.js +2 -2
  34. package/src/utils/api.js +20 -0
  35. package/src/utils/extension.js +1 -3
  36. package/src/utils/html.js +29 -0
  37. package/src/utils/i18n.js +2 -2
  38. package/src/utils/init.js +11 -3
  39. package/src/utils/license.js +1 -1
  40. package/src/utils/node.js +3 -1
  41. package/src/utils/pkg.js +19 -2
  42. package/src/utils/rules.js +1 -1
@@ -0,0 +1,65 @@
1
+ import array from '../utils/array.js'
2
+ import html from '../utils/html.js'
3
+
4
+ const re = {
5
+ nonExecutable: /^(?:application\/(?:ld\+)?json|importmap|speculationrules|text\/(?:x-)?template)$/i,
6
+ script: /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi
7
+ }
8
+
9
+ export default {
10
+ meta: {
11
+ type: 'problem',
12
+ docs: {
13
+ description: 'Disallow inline `<script>` bodies and remote `<script src>` in extension HTML',
14
+ category: 'Possible Errors',
15
+ html: true
16
+ },
17
+ defaultOptions: [{ allowedSrcPrefixes: ['chrome-extension://'] }],
18
+ schema: [{
19
+ type: 'object',
20
+ properties: {
21
+ allowedSrcPrefixes: {
22
+ oneOf: [
23
+ { type: 'string', description: 'A single allowed src prefix' },
24
+ { type: 'array', items: { type: 'string' }, description: 'List of allowed src prefixes' }
25
+ ],
26
+ description: 'Additional src prefixes to permit'
27
+ }
28
+ },
29
+ additionalProperties: false
30
+ }],
31
+ messages: {
32
+ inlineBody: 'Inline `<script>` bodies are blocked by MV3\'s `script-src \'self\'`. Move the code to an external file.',
33
+ remoteSrc: 'Remote `<script src="{{src}}">` is blocked by MV3\'s `script-src \'self\'`. Bundle the script or use `chrome.runtime.getURL()`.'
34
+ }
35
+ },
36
+
37
+ create(context) {
38
+ const opts = context.options[0] || {},
39
+ allowedPrefixes = array.to(opts.allowedSrcPrefixes ?? ['chrome-extension://']),
40
+ text = context.sourceCode.text
41
+ let scanned = false
42
+ function scan() {
43
+ if (scanned) return
44
+ scanned = true
45
+ if (!html.isHTML(text)) return
46
+ for (const match of text.matchAll(re.script)) {
47
+ const [, attrs, body] = match,
48
+ type = html.pickAttrVal(attrs.match(html.re.attr.type))
49
+ if (type && re.nonExecutable.test(type.trim())) continue
50
+ const src = html.pickAttrVal(attrs.match(html.re.attr.src))
51
+ if (src !== null) {
52
+ const trimmed = src.trim()
53
+ if (trimmed && html.isRemote(trimmed, allowedPrefixes))
54
+ context.report({ loc: html.offsetToLoc(text, match.index),
55
+ messageId: 'remoteSrc', data: { src: trimmed }})
56
+ continue
57
+ }
58
+ if (body && body.trim().length > 0)
59
+ context.report({ loc: html.offsetToLoc(text, match.index),
60
+ messageId: 'inlineBody' })
61
+ }
62
+ }
63
+ return { 'Program:exit': scan, 'Document:exit': scan, '*': scan }
64
+ }
65
+ }
@@ -1,5 +1,12 @@
1
1
  const re = { insecureURL: /^http:\/\/(?!localhost\b|127\.0\.0\.1\b|\[::1\])/i }
2
2
 
3
+ function urlFromLiteral(node) {
4
+ return ( node?.type == 'Literal'
5
+ && typeof node.value == 'string'
6
+ && re.insecureURL.test(node.value)
7
+ ) ? node.value : null
8
+ }
9
+
3
10
  export default {
4
11
  meta: {
5
12
  type: 'problem',
@@ -18,12 +25,6 @@ export default {
18
25
 
19
26
  create(context) {
20
27
  const xhrVars = new Set()
21
- function urlFromLiteral(node) {
22
- return ( node?.type == 'Literal'
23
- && typeof node.value == 'string'
24
- && re.insecureURL.test(node.value)
25
- ) ? node.value : null
26
- }
27
28
  return {
28
29
  VariableDeclarator(node) {
29
30
  if (node.init?.type == 'NewExpression'
@@ -1,7 +1,8 @@
1
+ import api from '../utils/api.js'
1
2
  import nodeUtils from '../utils/node.js'
2
- import apis from '../data/apis.js'
3
3
 
4
- const re = { postmsgValidation: /\b\w+\.(?:origin|source)\b/, senderValidation: /sender\.(?:id|origin)/ }
4
+ const re = { postmsgValidation: /\b\w+\.(?:origin|source)\b/, senderValidation: /sender\.(?:id|origin)/ },
5
+ isMessageTarget = obj => obj?.type == 'Identifier' && ['self', 'window'].includes(obj.name)
5
6
 
6
7
  export default {
7
8
  meta: {
@@ -40,13 +41,12 @@ export default {
40
41
  if (re.postmsgValidation.test(body)) return
41
42
  context.report({ node, messageId: 'unvalidatedPostMessage', data: { target }})
42
43
  }
43
- function isMessageTarget(obj) { return obj?.type == 'Identifier' && ['self', 'window'].includes(obj.name) }
44
44
  return {
45
45
  CallExpression(node) {
46
46
  if (node.callee.type != 'MemberExpression') return
47
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 && apis.isExternalMsgListener(listenerRoot)) {
49
+ if (listenerRoot && api.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))
@@ -1,5 +1,3 @@
1
- /* eslint-disable security/detect-non-literal-fs-filename -- lint-time project inspection only */
2
-
3
1
  import extension from '../utils/extension.js'
4
2
  import fs from 'node:fs'
5
3
  import i18nUtils from '../utils/i18n.js'
@@ -51,11 +49,11 @@ export default {
51
49
  root = extension.findRoot(context)
52
50
  if (root) {
53
51
  const localesDir = path.join(root, '_locales')
54
- try {
52
+ try { // oxlint-disable-next-line security/detect-non-literal-fs-filename
55
53
  for (const entry of fs.readdirSync(localesDir, { withFileTypes: true }))
56
54
  if (entry.isDirectory() && isInvalid(entry.name)) invalidFolders.push(entry.name)
57
- invalidFolders.sort()
58
- } catch {} // eslint-disable-line no-empty
55
+ invalidFolders.sort((a, b) => a < b ? -1 : a > b ? 1 : 0)
56
+ } catch {}
59
57
  }
60
58
  const loc = { line: 1, column: 1 }
61
59
  if (!isManifest) {
@@ -1,4 +1,4 @@
1
- /* eslint-disable security/detect-non-literal-fs-filename -- lint-time project inspection only */
1
+ /* oxlint-disable security/detect-non-literal-fs-filename */
2
2
 
3
3
  import extension from '../utils/extension.js'
4
4
  import fs from 'node:fs'
@@ -1,8 +1,7 @@
1
+ import apiUtils from '../utils/api.js'
1
2
  import extension from '../utils/extension.js'
2
3
  import nodeUtils from '../utils/node.js'
3
4
 
4
- import apis from '../data/apis.js'
5
-
6
5
  function queriesActiveTab(node) {
7
6
  const arg = node.arguments[0] ; if (arg?.type != 'ObjectExpression') return false
8
7
  return arg.properties.some(prop => {
@@ -34,7 +33,7 @@ export default {
34
33
  CallExpression(node) {
35
34
  if (node.callee.type != 'MemberExpression') return
36
35
  const api = nodeUtils.getMemberPath(node.callee) ; if (!api) return
37
- const permission = apis.findPermission(api) ; if (!permission || declared.has(permission)) return
36
+ const permission = apiUtils.findPermission(api) ; if (!permission || declared.has(permission)) return
38
37
  const isActiveTabQuery = permission == 'tabs' && queriesActiveTab(node)
39
38
  if (isActiveTabQuery && declared.has('activeTab')) return
40
39
  const messageId = `missing${ isActiveTabQuery ? 'ActiveTab' : 'Permission' }`
@@ -16,8 +16,8 @@ function collectWARpatterns(manifest) {
16
16
  }
17
17
 
18
18
  function patternToRegex(pattern) {
19
- const escaped = pattern.replace(re.regexSpecial, '\\$&').replace(/\*/g, '.*')
20
- return new RegExp(`^${escaped}$`) // eslint-disable-line security/detect-non-literal-regexp
19
+ const escaped = pattern.replaceAll(re.regexSpecial, '\\$&').replaceAll('*', '.*')
20
+ return new RegExp(`^${escaped}$`) // oxlint-disable-line security/detect-non-literal-regexp
21
21
  }
22
22
 
23
23
  export default {
@@ -3,6 +3,38 @@ import nodeUtils from '../utils/node.js'
3
3
  import manifest from '../data/manifest.js'
4
4
  import regex from '../data/regex.js'
5
5
 
6
+ const re = { inlineHandler: /^on[a-z]+$/i, inlineHandlerInHTML: /\son[a-z]+\s*=/i }
7
+
8
+ function isCreateScript(node) {
9
+ return node?.type == 'CallExpression'
10
+ && node.callee?.property?.name == 'createElement'
11
+ && node.arguments[0]?.value == 'script'
12
+ }
13
+ function isInlineHandlerInString(node) {
14
+ return node?.type == 'Literal'
15
+ && typeof node.value == 'string'
16
+ && re.inlineHandlerInHTML.test(node.value)
17
+ }
18
+ function isLiteralURL(node) {
19
+ return node?.type == 'Literal'
20
+ && typeof node.value == 'string'
21
+ && regex.url.test(node.value)
22
+ }
23
+ function isObjectAssign(node) {
24
+ return node?.type == 'CallExpression'
25
+ && node.callee?.type == 'MemberExpression'
26
+ && node.callee.object?.name == 'Object'
27
+ && node.callee.property?.name == 'assign'
28
+ }
29
+ function isScriptSrcProperty(node) {
30
+ const obj = node.parent
31
+ if (obj?.type != 'ObjectExpression') return false
32
+ const parent = obj.parent
33
+ if (isCreateScript(parent)) return true
34
+ if (isObjectAssign(parent) && isCreateScript(parent.arguments[0])) return true
35
+ return false
36
+ }
37
+
6
38
  export default {
7
39
  meta: {
8
40
  type: 'problem',
@@ -13,8 +45,7 @@ export default {
13
45
  security: true
14
46
  },
15
47
  defaultOptions: [{
16
- patterns: ['eval', 'new-function', 'string-timer',
17
- 'dynamic-import', 'remote-script-src', 'execute-script-url'],
48
+ patterns: manifest.patterns.remoteCode,
18
49
  checkExecuteScriptClosures: true
19
50
  }],
20
51
  schema: [{
@@ -39,31 +70,17 @@ export default {
39
70
  dynamicImport: 'Remote dynamic `import()` is forbidden under MV3\'s CSP. Use a relative path instead.',
40
71
  remoteScriptSrc: 'Injecting remote `<script src>` is forbidden under MV3\'s CSP. Use `chrome.runtime.getURL()` for bundled scripts instead.',
41
72
  executeScriptURL: '`chrome.scripting.executeScript` must use local `files`, not a remote URL.',
42
- executeScriptClosure: '`chrome.scripting.executeScript({ func })` captures outer-scope variables that fail after serialization. Pass them via `args` instead.'
73
+ executeScriptClosure: '`chrome.scripting.executeScript({ func })` captures outer-scope variables that fail after serialization. Pass them via `args` instead.',
74
+ inlineEventHandler: 'Inline event handler `{{attr}}` is blocked by MV3\'s `script-src \'self\'`. Use `addEventListener()` instead.',
75
+ inlineScriptBody: 'Inline `<script>` body injection is blocked by MV3\'s `script-src \'self\'`. Move the code to an external file.'
43
76
  }
44
77
  },
45
78
 
46
79
  create(context) {
47
80
  const opts = context.options[0] || {},
48
- patterns = new Set(opts.patterns || manifest.regexmoteCode),
81
+ patterns = new Set(opts.patterns || manifest.patterns.remoteCode),
49
82
  checkClosures = opts.checkExecuteScriptClosures !== false,
50
83
  scriptElems = new Set()
51
- function isCreateScript(node) {
52
- return node?.type == 'CallExpression'
53
- && node.callee?.property?.name == 'createElement'
54
- && node.arguments[0]?.value == 'script'
55
- }
56
- function isLiteralURL(node) {
57
- return node?.type == 'Literal'
58
- && typeof node.value == 'string'
59
- && regex.url.test(node.value)
60
- }
61
- function isObjectAssign(node) {
62
- return node?.type == 'CallExpression'
63
- && node.callee?.type == 'MemberExpression'
64
- && node.callee.object?.name == 'Object'
65
- && node.callee.property?.name == 'assign'
66
- }
67
84
  function isScriptSrcAssignment(left, right) {
68
85
  if (left.type != 'MemberExpression') return false
69
86
  if (left.property?.name != 'src') return false
@@ -72,12 +89,10 @@ export default {
72
89
  if (left.object.type == 'Identifier' && scriptElems.has(left.object.name)) return true
73
90
  return false
74
91
  }
75
- function isScriptSrcProperty(node) {
76
- const obj = node.parent
77
- if (obj?.type != 'ObjectExpression') return false
78
- const parent = obj.parent
79
- if (isCreateScript(parent)) return true
80
- if (isObjectAssign(parent) && isCreateScript(parent.arguments[0])) return true
92
+ function isScriptElement(node) {
93
+ if (!node) return false
94
+ if (isCreateScript(node)) return true
95
+ if (node.type == 'Identifier' && scriptElems.has(node.name)) return true
81
96
  return false
82
97
  }
83
98
  return {
@@ -112,12 +127,31 @@ export default {
112
127
  if (optsNode?.type != 'ObjectExpression') return
113
128
  const urlProp = optsNode.properties.find(prop => prop.key?.name == 'url'),
114
129
  funcProp = optsNode.properties.find(prop => prop.key?.name == 'func'),
115
- argsProp = optsNode.properties.find(prop => prop.key?.name == 'args')
130
+ hasArgs = optsNode.properties.some(prop => prop.key?.name == 'args')
116
131
  if (patterns.has('execute-script-url') && urlProp)
117
132
  context.report({ node: urlProp, messageId: 'executeScriptURL' })
118
- if (checkClosures && funcProp && !argsProp)
133
+ if (checkClosures && funcProp && !hasArgs)
119
134
  context.report({ node: funcProp, messageId: 'executeScriptClosure' })
120
135
  }
136
+ if (patterns.has('inline-event-handler')
137
+ && node.callee.property?.name == 'setAttribute'
138
+ && node.arguments[0]?.type == 'Literal'
139
+ && typeof node.arguments[0].value == 'string'
140
+ && re.inlineHandler.test(node.arguments[0].value)
141
+ ) context.report({ node, messageId: 'inlineEventHandler',
142
+ data: { attr: node.arguments[0].value.toLowerCase() }})
143
+ if (patterns.has('inline-event-handler')
144
+ && node.callee.property?.name == 'insertAdjacentHTML'
145
+ && isInlineHandlerInString(node.arguments[1])
146
+ ) context.report({ node: node.arguments[1], messageId: 'inlineEventHandler',
147
+ data: { attr: 'on*' }})
148
+ if (patterns.has('inline-script-body')
149
+ && ['append', 'prepend'].includes(node.callee.property?.name)
150
+ && isScriptElement(node.callee.object)
151
+ && node.arguments[0]?.type == 'Literal'
152
+ && typeof node.arguments[0].value == 'string'
153
+ && node.arguments[0].value.trim().length > 0
154
+ ) context.report({ node: node.arguments[0], messageId: 'inlineScriptBody' })
121
155
  }
122
156
  },
123
157
  NewExpression(node) {
@@ -125,6 +159,21 @@ export default {
125
159
  context.report({ node, messageId: 'newFunction' })
126
160
  },
127
161
  AssignmentExpression(node) {
162
+ if (patterns.has('inline-event-handler')
163
+ && node.left.type == 'MemberExpression'
164
+ && ['innerHTML', 'outerHTML'].includes(node.left.property?.name)
165
+ && !isScriptElement(node.left.object)
166
+ && isInlineHandlerInString(node.right)
167
+ ) context.report({ node: node.right, messageId: 'inlineEventHandler',
168
+ data: { attr: 'on*' }})
169
+ if (patterns.has('inline-script-body')
170
+ && node.left.type == 'MemberExpression'
171
+ && ['textContent', 'innerText', 'innerHTML', 'text'].includes(node.left.property?.name)
172
+ && isScriptElement(node.left.object)
173
+ && node.right?.type == 'Literal'
174
+ && typeof node.right.value == 'string'
175
+ && node.right.value.trim().length > 0
176
+ ) context.report({ node, messageId: 'inlineScriptBody' })
128
177
  if (!patterns.has('remote-script-src')) return
129
178
  if (isScriptSrcAssignment(node.left, node.right))
130
179
  context.report({ node: node.right, messageId: 'remoteScriptSrc' })
@@ -0,0 +1,55 @@
1
+ import array from '../utils/array.js'
2
+ import html from '../utils/html.js'
3
+
4
+ const re = { objectTag: /<(?:object|embed)\b([^>]*)>/gi }
5
+
6
+ export default {
7
+ meta: {
8
+ type: 'problem',
9
+ docs: {
10
+ description: 'Disallow remote `<object data>` and `<embed src>` in extension HTML',
11
+ category: 'Possible Errors',
12
+ html: true
13
+ },
14
+ defaultOptions: [{ allowedPrefixes: ['chrome-extension://'] }],
15
+ schema: [{
16
+ type: 'object',
17
+ properties: {
18
+ allowedPrefixes: {
19
+ oneOf: [
20
+ { type: 'string', description: 'A single allowed prefix' },
21
+ { type: 'array', items: { type: 'string' }, description: 'List of allowed prefixes' }
22
+ ],
23
+ description: 'Additional prefixes to permit'
24
+ }
25
+ },
26
+ additionalProperties: false
27
+ }],
28
+ messages: {
29
+ remoteObject: 'Remote `<{{tag}} {{attr}}="{{src}}">` is blocked by MV3\'s `object-src \'self\'`. '
30
+ + 'Bundle the resource or use `chrome.runtime.getURL()`.'
31
+ }
32
+ },
33
+
34
+ create(context) {
35
+ const opts = context.options[0] || {},
36
+ allowedPrefixes = array.to(opts.allowedPrefixes ?? ['chrome-extension://']),
37
+ text = context.sourceCode.text
38
+ let scanned = false
39
+ function scan() {
40
+ if (scanned) return
41
+ scanned = true
42
+ if (!html.isHTML(text)) return
43
+ for (const match of text.matchAll(re.objectTag)) {
44
+ const [full, attrs] = match,
45
+ tag = full.trimStart().toLowerCase().startsWith('<object') ? 'object' : 'embed',
46
+ attr = tag == 'object' ? 'data' : 'src',
47
+ val = html.pickAttrVal(attrs.match(tag == 'object' ? html.re.attr.data : html.re.attr.src))
48
+ if (val !== null && html.isRemote(val.trim(), allowedPrefixes))
49
+ context.report({ loc: html.offsetToLoc(text, match.index), messageId: 'remoteObject',
50
+ data: { tag, attr, src: val.trim() }})
51
+ }
52
+ }
53
+ return { 'Program:exit': scan, 'Document:exit': scan, '*': scan }
54
+ }
55
+ }
@@ -1,6 +1,24 @@
1
1
  import array from '../utils/array.js'
2
+ import regex from '../data/regex.js'
2
3
 
3
- const re = { urlScheme: /^(?:([a-z][a-z0-9+.-]*):|\/\/)/i }
4
+ function getPreludeURLnode(prelude) {
5
+ if (!prelude) return null
6
+ if (['Url', 'CSSUrl', 'String', 'CSSString'].includes(prelude.type))
7
+ return prelude
8
+ for (const child of prelude.children ?? [])
9
+ if (['Url', 'CSSUrl', 'String', 'CSSString'].includes(child.type))
10
+ return child
11
+ return null
12
+ }
13
+ function getURLstr(node) {
14
+ if (!node) return null
15
+ if (typeof node == 'string') return node
16
+ if (typeof node.url == 'string') return node.url
17
+ if (typeof node.value == 'string') return node.value
18
+ if (node.value && typeof node.value == 'object' && typeof node.value.value == 'string')
19
+ return node.value.value
20
+ return null
21
+ }
4
22
 
5
23
  export default {
6
24
  meta: {
@@ -46,24 +64,6 @@ export default {
46
64
  allowedSchemes = new Set(array.to(opts.allowedSchemes ?? ['blob', 'chrome-extension', 'data'])),
47
65
  patterns = new Set(array.to(opts.patterns ?? ['at-import', 'url'])),
48
66
  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
67
  function handleAtrule(node) {
68
68
  if (!node.prelude) return
69
69
  if (node.name == 'import' && patterns.has('at-import')) {
@@ -90,7 +90,7 @@ export default {
90
90
  }
91
91
  function isRemote(str) {
92
92
  if (typeof str != 'string') return false
93
- const match = str.match(re.urlScheme)
93
+ const match = regex.scheme.exec(str)
94
94
  if (!match) return false
95
95
  if (match[0] == '//') return true
96
96
  const scheme = (match[1] || '').toLowerCase()
@@ -1,8 +1,8 @@
1
1
  import array from '../utils/array.js'
2
- import regex from '../data/regex.js'
2
+ import node from '../utils/node.js'
3
3
 
4
4
  const re = {
5
- scrollbarPseudo: /^(?:-webkit-)?scrollbar(?:-[\w-]+)?$/i } // eslint-disable-line security/detect-unsafe-regex
5
+ scrollbarPseudo: /^(?:-webkit-)?scrollbar(?:-[\w-]+)?$/i } // oxlint-disable-line security/detect-unsafe-regex
6
6
 
7
7
  export default {
8
8
  meta: {
@@ -43,31 +43,29 @@ export default {
43
43
  hasQualifier = false,
44
44
  pastFirstCompound = false
45
45
  for (const simple of selector.children ?? []) {
46
- const type = plainType(simple)
46
+ const type = node.plainType(simple)
47
47
  if (type == 'Combinator') { pastFirstCompound = true; continue }
48
48
  if (type == 'PseudoElementSelector' && re.scrollbarPseudo.test(simple.name ?? '')) {
49
49
  hasScrollbarPseudo = true
50
50
  continue
51
51
  }
52
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
53
+ if (['AttributeSelector', 'ClassSelector', 'IdSelector'].includes(type)
54
+ || (type == 'PseudoClassSelector' && (simple.name ?? '').toLowerCase().startsWith('host'))
55
+ ) hasQualifier = true
57
56
  }
58
57
  if (hasScrollbarPseudo && !hasQualifier)
59
58
  context.report({ node: selector, messageId: 'noScrollbarStyling', data: { selector: text }})
60
59
  }
61
60
  function getSelectorText(selector) {
62
- const range = selector.range
63
- if (range && range.length == 2) return context.sourceCode.text.slice(range[0], range[1])
61
+ const { range } = selector
62
+ if (range?.length == 2) return context.sourceCode.text.slice(range[0], range[1])
64
63
  return context.sourceCode.getText(selector)
65
64
  }
66
65
  function handleRule(node) {
67
66
  for (const selector of node.prelude?.children ?? [])
68
67
  checkSelector(selector)
69
68
  }
70
- function plainType(node) { return node?.type?.replace(regex.prefix.css, '') ?? '' }
71
69
  return { Rule: handleRule, CSSRule: handleRule }
72
70
  }
73
71
  }
@@ -58,7 +58,7 @@ export default {
58
58
  return {
59
59
  'Program:exit'() {
60
60
  for (const key of hostKeys) {
61
- const patterns = manifest[key] // eslint-disable-line security/detect-object-injection
61
+ const patterns = manifest[key] // oxlint-disable-line security/detect-object-injection
62
62
  if (!Array.isArray(patterns)) continue
63
63
  for (const pattern of patterns)
64
64
  if (isBroad(pattern))
@@ -1,3 +1,4 @@
1
+ import apiUtils from '../utils/api.js'
1
2
  import extension from '../utils/extension.js'
2
3
  import fs from 'node:fs'
3
4
  import path from 'node:path'
@@ -14,15 +15,15 @@ function collectUsedAPIs(manifest, root) {
14
15
  if (manifest.action?.default_popup) files.add(manifest.action.default_popup)
15
16
  if (manifest.options_ui?.page) files.add(manifest.options_ui.page)
16
17
  if (manifest.devtools_page) files.add(manifest.devtools_page)
17
- const re = { /* eslint-disable-next-line security/detect-unsafe-regex */
18
+ const re = { /* oxlint-disable-next-line security/detect-unsafe-regex */
18
19
  chromeAPI: /\bchrome\.\w+(?:\.\w+)*/g }
19
20
  const used = new Set()
20
21
  for (const file of files) {
21
22
  try {
22
- const src = // eslint-disable-next-line security/detect-non-literal-fs-filename
23
+ const src = // oxlint-disable-next-line security/detect-non-literal-fs-filename
23
24
  fs.readFileSync(path.resolve(root, file), 'utf8')
24
25
  for (const match of src.matchAll(re.chromeAPI)) used.add(match[0])
25
- } catch {} // eslint-disable-line no-empty
26
+ } catch {}
26
27
  }
27
28
  return used
28
29
  }
@@ -54,7 +55,7 @@ export default {
54
55
  if (perm == permission) { knownPerm = true ; break }
55
56
  if (!knownPerm) return true
56
57
  for (const api of usedAPIs)
57
- if (apis.findPermission(api) == permission) return true
58
+ if (apiUtils.findPermission(api) == permission) return true
58
59
  return false
59
60
  }
60
61
  if (!regex.filename.manifest.test(filename)) {
@@ -19,7 +19,7 @@ export default {
19
19
  const { filename } = context
20
20
  function check(node) {
21
21
  const pkgRoot = pkg.findRoot(path.dirname(filename))
22
- if (!pkgRoot || !license.find(pkgRoot)) // eslint-disable-line e18e/prefer-array-some
22
+ if (!pkgRoot || !license.find(pkgRoot)) // oxlint-disable-line e18e/prefer-array-some
23
23
  context.report({ node, messageId: 'missingFile' })
24
24
  }
25
25
  return { Program: check, Document: check }
@@ -32,7 +32,8 @@ export default {
32
32
  }
33
33
  }
34
34
  }
35
- const manifest = extension.manifest.read(context) ; if (!manifest || manifest.manifest_version == 3) return {}
36
- return { Program(node) { context.report({ node, messageId: 'notMV3' }) }}
35
+ const manifest = extension.manifest.read(context)
36
+ if (!manifest || manifest.manifest_version == 3) return {}
37
+ return { Program(node) { context.report({ node, messageId: 'notMV3' })}}
37
38
  }
38
39
  }
@@ -1,10 +1,10 @@
1
- /* eslint-disable security/detect-non-literal-fs-filename -- lint-time project inspection only */
1
+ /* oxlint-disable security/detect-non-literal-fs-filename -- lint-time project inspection only */
2
2
 
3
3
  import extension from '../utils/extension.js'
4
4
  import fs from 'node:fs'
5
5
  import path from 'node:path'
6
6
 
7
- const re = { invalidChars: /[^\w\-.]/, reservedPrefix: /^_/ }
7
+ const re = { invalidChars: /[^\w\-.]/ }
8
8
 
9
9
  const refKeys = [
10
10
  ['action.default_popup', manifest => manifest.action?.default_popup],
@@ -112,7 +112,7 @@ export default {
112
112
  segments = path.relative(extRoot, filename).split(path.sep)
113
113
  if (noReservedPrefix)
114
114
  for (const seg of segments)
115
- if (re.reservedPrefix.test(seg) && seg != '_locales')
115
+ if (seg.startsWith('_') && seg != '_locales')
116
116
  context.report({ loc: { line: 1, column: 1 }, messageId: 'reservedPrefix', data: { name: seg }})
117
117
  if (noSpecialChars)
118
118
  for (const seg of segments)
@@ -20,7 +20,7 @@ function validateIcons(context, icons, root, opts, loc) {
20
20
  if (typeof file != 'string') continue
21
21
  if (checkSVG && re.svg.test(file))
22
22
  context.report({ loc, messageId: 'svgNotSupported' })
23
- else if (checkFiles && root // eslint-disable-next-line security/detect-non-literal-fs-filename
23
+ else if (checkFiles && root // oxlint-disable-next-line security/detect-non-literal-fs-filename
24
24
  && !fs.existsSync(path.resolve(root, file))
25
25
  ) context.report({ loc, messageId: 'missingFile', data: { file }})
26
26
  }
@@ -98,7 +98,7 @@ export default {
98
98
  if (typeof file != 'string') continue
99
99
  if (checkSVG && re.svg.test(file))
100
100
  context.report({ node: fileNode, messageId: 'svgNotSupported' })
101
- else if (checkFiles && root // eslint-disable-next-line security/detect-non-literal-fs-filename
101
+ else if (checkFiles && root // oxlint-disable-next-line security/detect-non-literal-fs-filename
102
102
  && !fs.existsSync(path.resolve(root, file))
103
103
  ) context.report({ node: fileNode, messageId: 'missingFile', data: { file }})
104
104
  }
@@ -85,10 +85,10 @@ export default {
85
85
  }
86
86
  }
87
87
  }
88
- const manifest = extension.manifest.read(context) ; if (!manifest || manifest.manifest_version != 3) return {}
88
+ const manifest = extension.manifest.read(context) ; if (manifest?.manifest_version != 3) return {}
89
89
  const isFF = manifest.browser_specific_settings != null
90
- return {
91
- Program(node) { /* eslint-disable security/detect-object-injection */
90
+ return { // oxlint-disable security/detect-object-injection
91
+ Program(node) {
92
92
  for (const key of Object.keys(manifest))
93
93
  if (manifestData.keys.mv2Only.has(key))
94
94
  context.report({ node, messageId: 'mv2Key', data: { key, reason: 'MV2-only key' }})
@@ -117,7 +117,7 @@ export default {
117
117
  if (!set.has(value))
118
118
  context.report({ node, messageId: 'invalidDataCollectionValue', data: { value, kind }})
119
119
  }
120
- } /* eslint-enable security/detect-object-injection */
121
- }
120
+ }
121
+ } // oxlint-enable security/detect-object-injection
122
122
  }
123
123
  }