eslint-plugin-chrome-extensions 2.8.1 → 2.9.1

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 (37) hide show
  1. package/README.md +76 -83
  2. package/docs/LICENSE.md +1 -1
  3. package/package.json +14 -10
  4. package/src/rules/css-spacing.js +9 -14
  5. package/src/rules/manifest-key-order.js +6 -5
  6. package/src/rules/manifest-spacing.js +5 -8
  7. package/src/rules/no-content-style-leak.js +4 -3
  8. package/src/rules/no-deprecated-apis.js +6 -6
  9. package/src/rules/no-global-keyframes.js +5 -8
  10. package/src/rules/no-host-page-selectors.js +12 -16
  11. package/src/rules/no-inline-event-handlers.js +7 -5
  12. package/src/rules/no-inline-scripts.js +10 -6
  13. package/src/rules/no-insecure-fetch.js +10 -6
  14. package/src/rules/no-insecure-messaging.js +8 -10
  15. package/src/rules/no-invalid-locales.js +7 -6
  16. package/src/rules/no-invalid-permissions.js +5 -5
  17. package/src/rules/no-missing-default-locale.js +5 -4
  18. package/src/rules/no-missing-i18n-keys.js +7 -5
  19. package/src/rules/no-missing-permissions.js +1 -1
  20. package/src/rules/no-missing-web-accessible-resources.js +10 -10
  21. package/src/rules/no-remote-code.js +30 -33
  22. package/src/rules/no-remote-objects.js +7 -6
  23. package/src/rules/no-remote-urls.js +11 -12
  24. package/src/rules/no-scrollbar-styling.js +6 -6
  25. package/src/rules/no-sensitive-permissions.js +6 -4
  26. package/src/rules/no-unrestricted-host-permissions.js +6 -7
  27. package/src/rules/no-unused-i18n-keys.js +5 -4
  28. package/src/rules/require-short-name-when-useful.js +3 -8
  29. package/src/rules/valid-filenames.js +9 -11
  30. package/src/rules/valid-license-file.js +7 -6
  31. package/src/rules/valid-manifest-icons.js +14 -20
  32. package/src/rules/valid-service-worker.js +8 -10
  33. package/src/rules/valid-url-fields.js +8 -13
  34. package/src/rules/valid-version-field.js +5 -6
  35. package/src/utils/extension.js +7 -3
  36. package/src/utils/init.js +5 -4
  37. package/src/utils/node.js +21 -1
@@ -1,10 +1,13 @@
1
+ import nodeUtils from '../utils/node.js'
2
+
1
3
  const re = { insecureURL: /^http:\/\/(?!localhost\b|127\.0\.0\.1\b|\[::1\])/i }
2
4
 
3
5
  function urlFromLiteral(node) {
4
- return ( node?.type == 'Literal'
5
- && typeof node.value == 'string'
6
- && re.insecureURL.test(node.value)
7
- ) ? node.value : null
6
+ const inner = nodeUtils.unwrapTS(node)
7
+ return ( inner?.type == 'Literal'
8
+ && typeof inner.value == 'string'
9
+ && re.insecureURL.test(inner.value)
10
+ ) ? inner.value : null
8
11
  }
9
12
 
10
13
  export default {
@@ -27,8 +30,9 @@ export default {
27
30
  const xhrVars = new Set()
28
31
  return {
29
32
  VariableDeclarator(node) {
30
- if (node.init?.type == 'NewExpression'
31
- && node.init.callee?.name == 'XMLHttpRequest'
33
+ const init = nodeUtils.unwrapTS(node.init)
34
+ if (init?.type == 'NewExpression'
35
+ && init.callee?.name == 'XMLHttpRequest'
32
36
  && node.id.type == 'Identifier'
33
37
  ) xhrVars.add(node.id.name)
34
38
  },
@@ -1,7 +1,8 @@
1
1
  import api from '../utils/api.js'
2
2
  import nodeUtils from '../utils/node.js'
3
3
 
4
- const re = { postmsgValidation: /\b\w+\.(?:origin|source)\b/, senderValidation: /sender\.(?:id|origin)/ },
4
+ const defaultOptions = { disallowWildcardTarget: true },
5
+ re = { postmsgValidation: /\b\w+\.(?:origin|source)\b/, senderValidation: /sender\.(?:id|origin)/ },
5
6
  isMessageTarget = obj => obj?.type == 'Identifier' && ['self', 'window'].includes(obj.name)
6
7
 
7
8
  export default {
@@ -13,7 +14,7 @@ export default {
13
14
  recommended: true,
14
15
  security: true
15
16
  },
16
- defaultOptions: [{ disallowWildcardTarget: true }],
17
+ defaultOptions: [defaultOptions],
17
18
  schema: [{
18
19
  type: 'object',
19
20
  properties: {
@@ -33,8 +34,7 @@ export default {
33
34
  },
34
35
 
35
36
  create(context) {
36
- const opts = context.options[0] || {},
37
- disallowWildcard = opts.disallowWildcardTarget !== false
37
+ const { disallowWildcardTarget } = { ...defaultOptions, ...context.options[0] }
38
38
  function checkPostMessageHandler(node, handlerNode, target) {
39
39
  if (!handlerNode) return
40
40
  const body = context.sourceCode.getText(handlerNode)
@@ -59,14 +59,12 @@ export default {
59
59
  context.report({ node, messageId: 'unfilteredBroadcast' })
60
60
  }
61
61
  if (node.callee.property?.name == 'addEventListener'
62
- && node.arguments[0]?.value == 'message'
62
+ && nodeUtils.getStringLiteral(node.arguments[0]) == 'message'
63
63
  && isMessageTarget(node.callee.object)
64
64
  ) checkPostMessageHandler(node, node.arguments[1], node.callee.object.name)
65
- if (disallowWildcard && node.callee.property?.name == 'postMessage') {
66
- const target = node.arguments[1]
67
- if (target?.type == 'Literal' && target.value == '*')
68
- context.report({ node: target, messageId: 'wildcardTarget' })
69
- }
65
+ if (disallowWildcardTarget && node.callee.property?.name == 'postMessage'
66
+ && nodeUtils.getStringLiteral(node.arguments[1]) == '*'
67
+ ) context.report({ node: node.arguments[1], messageId: 'wildcardTarget' })
70
68
  },
71
69
  AssignmentExpression(node) {
72
70
  if (node.left.type == 'MemberExpression'
@@ -5,6 +5,8 @@ import path from 'node:path'
5
5
 
6
6
  import regex from '../data/regex.js'
7
7
 
8
+ const defaultOptions = { allowedLocales: [] }
9
+
8
10
  export default {
9
11
  meta: {
10
12
  type: 'problem',
@@ -31,7 +33,7 @@ export default {
31
33
  },
32
34
  additionalProperties: false
33
35
  }],
34
- defaultOptions: [{ allowedLocales: [] }],
36
+ defaultOptions: [defaultOptions],
35
37
  messages: {
36
38
  invalidLocale: '`{{value}}` is not a Chrome-supported locale code.'
37
39
  }
@@ -42,11 +44,10 @@ export default {
42
44
  isManifest = regex.filename.manifest.test(filename),
43
45
  isScript = regex.filename.script.test(filename)
44
46
  if (!isManifest && !isScript) return {}
45
- const opts = context.options[0] || {},
46
- allowed = new Set(Array.isArray(opts.allowedLocales) ? opts.allowedLocales
47
- : opts.allowedLocales ? [opts.allowedLocales] : []),
48
- invalidFolders = [],
49
- root = extension.findRoot(context)
47
+ const { allowedLocales } = { ...defaultOptions, ...context.options[0] },
48
+ allowed = new Set(Array.isArray(allowedLocales) ? allowedLocales : [allowedLocales]),
49
+ invalidFolders = [],
50
+ root = extension.findRoot(context)
50
51
  if (root) {
51
52
  const localesDir = path.join(root, '_locales')
52
53
  try { // oxlint-disable-next-line security/detect-non-literal-fs-filename
@@ -1,6 +1,8 @@
1
1
  import extension from '../utils/extension.js'
2
2
  import regex from '../data/regex.js'
3
3
 
4
+ const defaultOptions = { target: 'mv3', allowedPermissions: [] }
5
+
4
6
  export default {
5
7
  meta: {
6
8
  type: 'problem',
@@ -10,7 +12,7 @@ export default {
10
12
  recommended: true,
11
13
  manifest: true
12
14
  },
13
- defaultOptions: [{ target: 'mv3', allowedPermissions: [] }],
15
+ defaultOptions: [defaultOptions],
14
16
  schema: [{
15
17
  type: 'object',
16
18
  properties: {
@@ -38,10 +40,8 @@ export default {
38
40
  },
39
41
 
40
42
  create(context) {
41
- const opts = context.options[0] || {},
42
- target = opts.target || 'mv3',
43
- allowed = new Set(Array.isArray(opts.allowedPermissions) ? opts.allowedPermissions
44
- : opts.allowedPermissions ? [opts.allowedPermissions] : [])
43
+ const { target, allowedPermissions } = { ...defaultOptions, ...context.options[0] },
44
+ allowed = new Set(Array.isArray(allowedPermissions) ? allowedPermissions : [allowedPermissions])
45
45
  function checkPermission(node, permission) {
46
46
  if (typeof permission != 'string') return
47
47
  if (allowed.has(permission) || extension.manifest.permission.isValid(permission, target)) return
@@ -7,6 +7,8 @@ import path from 'node:path'
7
7
 
8
8
  import regex from '../data/regex.js'
9
9
 
10
+ const defaultOptions = { ignoreKeys: [], checkAllLocales: false }
11
+
10
12
  function readLocaleDirs(localesDir) {
11
13
  try {
12
14
  return fs.readdirSync(localesDir, { withFileTypes: true })
@@ -45,7 +47,7 @@ export default {
45
47
  },
46
48
  additionalProperties: false
47
49
  }],
48
- defaultOptions: [{ ignoreKeys: [], checkAllLocales: false }],
50
+ defaultOptions: [defaultOptions],
49
51
  messages: {
50
52
  missingDefaultLocale: '`_locales/` exists but `default_locale` is not declared.',
51
53
  unexpectedDefaultLocale: '`default_locale` is declared but no `_locales/` directory exists.',
@@ -64,9 +66,8 @@ export default {
64
66
  const localesDir = path.join(root, '_locales'),
65
67
  hasLocales = fs.existsSync(localesDir),
66
68
  defaultLocale = manifest.default_locale,
67
- opts = context.options[0] || {},
68
- ignored = new Set(opts.ignoreKeys || []),
69
- checkAllLocales = opts.checkAllLocales ?? false,
69
+ { ignoreKeys, checkAllLocales } = { ...defaultOptions, ...context.options[0] },
70
+ ignored = new Set(ignoreKeys),
70
71
  loc = { line: 1, column: 1 }
71
72
  if (hasLocales && typeof defaultLocale != 'string') {
72
73
  context.report({ loc, messageId: 'missingDefaultLocale' })
@@ -5,6 +5,8 @@ import nodeUtils from '../utils/node.js'
5
5
  import i18nData from '../data/i18n.js'
6
6
  import regex from '../data/regex.js'
7
7
 
8
+ const defaultOptions = { ignoreKeys: [] }
9
+
8
10
  export default {
9
11
  meta: {
10
12
  type: 'problem',
@@ -25,7 +27,7 @@ export default {
25
27
  },
26
28
  additionalProperties: false
27
29
  }],
28
- defaultOptions: [{ ignoreKeys: [] }],
30
+ defaultOptions: [defaultOptions],
29
31
  messages: {
30
32
  missingKey: '`{{key}}` is not in `_locales/{{locale}}/messages.json`.'
31
33
  }
@@ -36,10 +38,10 @@ export default {
36
38
  const manifest = extension.manifest.read(context) ; if (!manifest?.default_locale) return {}
37
39
  const root = extension.findRoot(context) ; if (!root) return {}
38
40
  const i18n = i18nUtils.readDefaultLocaleMessages(manifest, root) ; if (!i18n) return {}
39
- const opts = context.options[0] || {},
40
- ignored = new Set(opts.ignoreKeys || []),
41
- isMissing = key => !i18nUtils.isPredefined(key) && !ignored.has(key) && !(key in i18n.messages),
42
- loc = { line: 1, column: 1 }
41
+ const { ignoreKeys } = { ...defaultOptions, ...context.options[0] },
42
+ ignored = new Set(ignoreKeys),
43
+ isMissing = key => !i18nUtils.isPredefined(key) && !ignored.has(key) && !(key in i18n.messages),
44
+ loc = { line: 1, column: 1 }
43
45
  if (regex.filename.script.test(filename))
44
46
  return {
45
47
  CallExpression(node) {
@@ -7,7 +7,7 @@ function queriesActiveTab(node) {
7
7
  return arg.properties.some(prop => {
8
8
  if (prop.type != 'Property') return false
9
9
  const key = prop.key?.name ?? prop.key?.value
10
- return key == 'active' && prop.value?.type == 'Literal' && prop.value.value === true
10
+ return key == 'active' && nodeUtils.getLiteral(prop.value)?.value === true
11
11
  })
12
12
  }
13
13
 
@@ -8,13 +8,12 @@ function collectWARpatterns(manifest) {
8
8
  if (!Array.isArray(war)) return []
9
9
  const patterns = []
10
10
  for (const entry of war) {
11
- if (typeof entry == 'string') patterns.push(entry) // MV2 flat
11
+ if (typeof entry == 'string') patterns.push(entry)
12
12
  else if (Array.isArray(entry?.resources))
13
- for (const res of entry.resources) patterns.push(res) // MV3 object
13
+ for (const res of entry.resources) patterns.push(res)
14
14
  }
15
15
  return patterns
16
16
  }
17
-
18
17
  function patternToRegex(pattern) {
19
18
  const escaped = pattern.replaceAll(re.regexSpecial, '\\$&').replaceAll('*', '.*')
20
19
  return new RegExp(`^${escaped}$`) // oxlint-disable-line security/detect-non-literal-regexp
@@ -29,9 +28,7 @@ export default {
29
28
  recommended: true
30
29
  },
31
30
  schema: [],
32
- messages: {
33
- missingWAR: '`{{path}}` is not in `web_accessible_resources`.'
34
- }
31
+ messages: { missingWAR: '`{{path}}` is not in `web_accessible_resources`.' }
35
32
  },
36
33
 
37
34
  create(context) {
@@ -43,10 +40,13 @@ export default {
43
40
  if (node.callee.type != 'MemberExpression'
44
41
  || nodeUtils.getMemberPath(node.callee) != 'chrome.runtime.getURL'
45
42
  ) return
46
- const arg = node.arguments[0] ; if (arg?.type != 'Literal' || typeof arg.value != 'string') return
47
- const resourcePath = arg.value.replace(re.leadingSlash, '')
48
- for (const regex of regexes) if (regex.test(resourcePath)) return
49
- context.report({ node: arg, messageId: 'missingWAR', data: { path: arg.value }})
43
+ const arg = node.arguments[0],
44
+ val = nodeUtils.getStringLiteral(arg)
45
+ if (val === null) return
46
+ const resourcePath = val.replace(re.leadingSlash, '')
47
+ for (const regex of regexes)
48
+ if (regex.test(resourcePath)) return
49
+ context.report({ node: arg, messageId: 'missingWAR', data: { path: val }})
50
50
  }
51
51
  }
52
52
  }
@@ -3,22 +3,21 @@ 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 }
6
+ const defaultOptions = { patterns: manifest.patterns.remoteCode, checkExecuteScriptClosures: true },
7
+ re = { inlineHandler: /^on[a-z]+$/i, inlineHandlerInHTML: /\son[a-z]+\s*=/i }
7
8
 
8
9
  function isCreateScript(node) {
9
10
  return node?.type == 'CallExpression'
10
11
  && node.callee?.property?.name == 'createElement'
11
- && node.arguments[0]?.value == 'script'
12
+ && nodeUtils.getStringLiteral(node.arguments[0]) == 'script'
12
13
  }
13
14
  function isInlineHandlerInString(node) {
14
- return node?.type == 'Literal'
15
- && typeof node.value == 'string'
16
- && re.inlineHandlerInHTML.test(node.value)
15
+ const value = nodeUtils.getStringLiteral(node)
16
+ return value !== null && re.inlineHandlerInHTML.test(value)
17
17
  }
18
18
  function isLiteralURL(node) {
19
- return node?.type == 'Literal'
20
- && typeof node.value == 'string'
21
- && regex.url.test(node.value)
19
+ const value = nodeUtils.getStringLiteral(node)
20
+ return value !== null && regex.url.test(value)
22
21
  }
23
22
  function isObjectAssign(node) {
24
23
  return node?.type == 'CallExpression'
@@ -44,10 +43,7 @@ export default {
44
43
  recommended: true,
45
44
  security: true
46
45
  },
47
- defaultOptions: [{
48
- patterns: manifest.patterns.remoteCode,
49
- checkExecuteScriptClosures: true
50
- }],
46
+ defaultOptions: [defaultOptions],
51
47
  schema: [{
52
48
  type: 'object',
53
49
  properties: {
@@ -77,10 +73,9 @@ export default {
77
73
  },
78
74
 
79
75
  create(context) {
80
- const opts = context.options[0] || {},
81
- patterns = new Set(opts.patterns || manifest.patterns.remoteCode),
82
- checkClosures = opts.checkExecuteScriptClosures !== false,
83
- scriptElems = new Set()
76
+ const { patterns: rawPatterns, checkExecuteScriptClosures } = { ...defaultOptions, ...context.options[0] },
77
+ patterns = new Set(rawPatterns),
78
+ scriptElems = new Set()
84
79
  function isScriptSrcAssignment(left, right) {
85
80
  if (left.type != 'MemberExpression') return false
86
81
  if (left.property?.name != 'src') return false
@@ -97,7 +92,7 @@ export default {
97
92
  }
98
93
  return {
99
94
  VariableDeclarator(node) {
100
- if (isCreateScript(node.init) && node.id.type == 'Identifier')
95
+ if (isCreateScript(nodeUtils.unwrapTS(node.init)) && node.id.type == 'Identifier')
101
96
  scriptElems.add(node.id.name)
102
97
  },
103
98
  ImportExpression(node) {
@@ -113,8 +108,7 @@ export default {
113
108
  if (patterns.has('string-timer')
114
109
  && node.callee.type == 'Identifier'
115
110
  && ['setTimeout', 'setInterval'].includes(node.callee.name)
116
- && node.arguments[0]?.type == 'Literal'
117
- && typeof node.arguments[0].value == 'string'
111
+ && nodeUtils.getStringLiteral(node.arguments[0]) !== null
118
112
  ) context.report({ node, messageId: 'stringTimer', data: { fn: node.callee.name }})
119
113
  if (patterns.has('dynamic-import')
120
114
  && node.callee.type == 'Import'
@@ -130,16 +124,17 @@ export default {
130
124
  hasArgs = optsNode.properties.some(prop => prop.key?.name == 'args')
131
125
  if (patterns.has('execute-script-url') && urlProp)
132
126
  context.report({ node: urlProp, messageId: 'executeScriptURL' })
133
- if (checkClosures && funcProp && !hasArgs)
127
+ if (checkExecuteScriptClosures && funcProp && !hasArgs)
134
128
  context.report({ node: funcProp, messageId: 'executeScriptClosure' })
135
129
  }
136
130
  if (patterns.has('inline-event-handler')
137
131
  && 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() }})
132
+ ) {
133
+ const attr = nodeUtils.getStringLiteral(node.arguments[0])
134
+ if (attr !== null && re.inlineHandler.test(attr))
135
+ context.report({ node, messageId: 'inlineEventHandler',
136
+ data: { attr: attr.toLowerCase() }})
137
+ }
143
138
  if (patterns.has('inline-event-handler')
144
139
  && node.callee.property?.name == 'insertAdjacentHTML'
145
140
  && isInlineHandlerInString(node.arguments[1])
@@ -148,10 +143,11 @@ export default {
148
143
  if (patterns.has('inline-script-body')
149
144
  && ['append', 'prepend'].includes(node.callee.property?.name)
150
145
  && 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' })
146
+ ) {
147
+ const body = nodeUtils.getStringLiteral(node.arguments[0])
148
+ if (body !== null && body.trim().length > 0)
149
+ context.report({ node: node.arguments[0], messageId: 'inlineScriptBody' })
150
+ }
155
151
  }
156
152
  },
157
153
  NewExpression(node) {
@@ -170,10 +166,11 @@ export default {
170
166
  && node.left.type == 'MemberExpression'
171
167
  && ['textContent', 'innerText', 'innerHTML', 'text'].includes(node.left.property?.name)
172
168
  && 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' })
169
+ ) {
170
+ const body = nodeUtils.getStringLiteral(node.right)
171
+ if (body !== null && body.trim().length > 0)
172
+ context.report({ node, messageId: 'inlineScriptBody' })
173
+ }
177
174
  if (!patterns.has('remote-script-src')) return
178
175
  if (isScriptSrcAssignment(node.left, node.right))
179
176
  context.report({ node: node.right, messageId: 'remoteScriptSrc' })
@@ -1,7 +1,8 @@
1
1
  import array from '../utils/array.js'
2
2
  import html from '../utils/html.js'
3
3
 
4
- const re = { objectTag: /<(?:object|embed)\b([^>]*)>/gi }
4
+ const defaultOptions = { allowedPrefixes: ['chrome-extension://'] },
5
+ re = { objectTag: /<(?:object|embed)\b([^>]*)>/gi }
5
6
 
6
7
  export default {
7
8
  meta: {
@@ -11,7 +12,7 @@ export default {
11
12
  category: 'Possible Errors',
12
13
  html: true
13
14
  },
14
- defaultOptions: [{ allowedPrefixes: ['chrome-extension://'] }],
15
+ defaultOptions: [defaultOptions],
15
16
  schema: [{
16
17
  type: 'object',
17
18
  properties: {
@@ -32,9 +33,9 @@ export default {
32
33
  },
33
34
 
34
35
  create(context) {
35
- const opts = context.options[0] || {},
36
- allowedPrefixes = array.to(opts.allowedPrefixes ?? ['chrome-extension://']),
37
- text = context.sourceCode.text
36
+ const { allowedPrefixes } = { ...defaultOptions, ...context.options[0] },
37
+ allowedPrefixesArray = array.to(allowedPrefixes),
38
+ text = context.sourceCode.text
38
39
  let scanned = false
39
40
  function scan() {
40
41
  if (scanned) return
@@ -45,7 +46,7 @@ export default {
45
46
  tag = full.trimStart().toLowerCase().startsWith('<object') ? 'object' : 'embed',
46
47
  attr = tag == 'object' ? 'data' : 'src',
47
48
  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
+ if (val !== null && html.isRemote(val.trim(), allowedPrefixesArray))
49
50
  context.report({ loc: html.offsetToLoc(text, match.index), messageId: 'remoteObject',
50
51
  data: { tag, attr, src: val.trim() }})
51
52
  }
@@ -1,6 +1,8 @@
1
1
  import array from '../utils/array.js'
2
2
  import regex from '../data/regex.js'
3
3
 
4
+ const defaultOptions = { allowedSchemes: ['blob', 'chrome-extension', 'data'], patterns: ['at-import', 'url'] }
5
+
4
6
  function getPreludeURLnode(prelude) {
5
7
  if (!prelude) return null
6
8
  if (['Url', 'CSSUrl', 'String', 'CSSString'].includes(prelude.type))
@@ -28,10 +30,7 @@ export default {
28
30
  category: 'Possible Errors',
29
31
  css: true
30
32
  },
31
- defaultOptions: [{
32
- allowedSchemes: ['blob', 'chrome-extension', 'data'],
33
- patterns: ['at-import', 'url']
34
- }],
33
+ defaultOptions: [defaultOptions],
35
34
  schema: [{
36
35
  type: 'object',
37
36
  properties: {
@@ -60,20 +59,20 @@ export default {
60
59
  },
61
60
 
62
61
  create(context) {
63
- const opts = context.options[0] || {},
64
- allowedSchemes = new Set(array.to(opts.allowedSchemes ?? ['blob', 'chrome-extension', 'data'])),
65
- patterns = new Set(array.to(opts.patterns ?? ['at-import', 'url'])),
66
- handledURLs = new WeakSet()
62
+ const { allowedSchemes, patterns } = { ...defaultOptions, ...context.options[0] },
63
+ allowedSchemeSet = new Set(array.to(allowedSchemes)),
64
+ patternSet = new Set(array.to(patterns)),
65
+ handledURLs = new WeakSet()
67
66
  function handleAtrule(node) {
68
67
  if (!node.prelude) return
69
- if (node.name == 'import' && patterns.has('at-import')) {
68
+ if (node.name == 'import' && patternSet.has('at-import')) {
70
69
  const urlNode = getPreludeURLnode(node.prelude)
71
70
  if (urlNode && isRemote(getURLstr(urlNode))) {
72
71
  handledURLs.add(urlNode)
73
72
  context.report({ node, messageId: 'remoteImport' })
74
73
  }
75
74
  }
76
- if (node.name == 'namespace' && patterns.has('namespace')) {
75
+ if (node.name == 'namespace' && patternSet.has('namespace')) {
77
76
  const urlNode = getPreludeURLnode(node.prelude)
78
77
  if (urlNode && isRemote(getURLstr(urlNode))) {
79
78
  handledURLs.add(urlNode)
@@ -82,7 +81,7 @@ export default {
82
81
  }
83
82
  }
84
83
  function handleURL(node) {
85
- if (!patterns.has('url')) return
84
+ if (!patternSet.has('url')) return
86
85
  if (handledURLs.has(node)) return
87
86
  if (node.parent?.type == 'AtrulePrelude' || node.parent?.type == 'CSSAtRulePrelude') return
88
87
  if (isRemote(getURLstr(node)))
@@ -95,7 +94,7 @@ export default {
95
94
  if (match[0] == '//') return true
96
95
  const scheme = (match[1] || '').toLowerCase()
97
96
  if (['http', 'https', 'ftp'].includes(scheme)) return true
98
- return !allowedSchemes.has(scheme)
97
+ return !allowedSchemeSet.has(scheme)
99
98
  }
100
99
  return { Atrule: handleAtrule, CSSAtRule: handleAtrule, Url: handleURL, CSSUrl: handleURL }
101
100
  }
@@ -1,8 +1,8 @@
1
1
  import array from '../utils/array.js'
2
2
  import node from '../utils/node.js'
3
3
 
4
- const re = {
5
- scrollbarPseudo: /^(?:-webkit-)?scrollbar(?:-[\w-]+)?$/i } // oxlint-disable-line security/detect-unsafe-regex
4
+ const defaultOptions = { allowedSelectors: [] }, // oxlint-disable-next-line security/detect-unsafe-regex
5
+ re = { scrollbarPseudo: /^(?:-webkit-)?scrollbar(?:-[\w-]+)?$/i }
6
6
 
7
7
  export default {
8
8
  meta: {
@@ -13,7 +13,7 @@ export default {
13
13
  category: 'Possible Errors',
14
14
  css: true
15
15
  },
16
- defaultOptions: [{ allowedSelectors: [] }],
16
+ defaultOptions: [defaultOptions],
17
17
  schema: [{
18
18
  type: 'object',
19
19
  properties: {
@@ -34,11 +34,11 @@ export default {
34
34
  },
35
35
 
36
36
  create(context) {
37
- const opts = context.options[0] || {},
38
- allowedSelectors = new Set(array.to(opts.allowedSelectors))
37
+ const { allowedSelectors } = { ...defaultOptions, ...context.options[0] },
38
+ allowedSelectorSet = new Set(array.to(allowedSelectors))
39
39
  function checkSelector(selector) {
40
40
  if (!selector) return
41
- const text = getSelectorText(selector).trim() ; if (allowedSelectors.has(text)) return
41
+ const text = getSelectorText(selector).trim() ; if (allowedSelectorSet.has(text)) return
42
42
  let hasScrollbarPseudo = false,
43
43
  hasQualifier = false,
44
44
  pastFirstCompound = false
@@ -1,8 +1,11 @@
1
+ import array from '../utils/array.js'
1
2
  import extension from '../utils/extension.js'
2
3
 
3
4
  import permissions from '../data/permissions.js'
4
5
  import regex from '../data/regex.js'
5
6
 
7
+ const defaultOptions = { allowedPermissions: [] }
8
+
6
9
  export default {
7
10
  meta: {
8
11
  type: 'problem',
@@ -13,7 +16,7 @@ export default {
13
16
  security: true,
14
17
  manifest: true
15
18
  },
16
- defaultOptions: [{ allowedPermissions: [] }],
19
+ defaultOptions: [defaultOptions],
17
20
  schema: [{
18
21
  type: 'object',
19
22
  properties: {
@@ -38,9 +41,8 @@ export default {
38
41
  },
39
42
 
40
43
  create(context) {
41
- const opts = context.options[0] || {},
42
- allowed = new Set(Array.isArray(opts.allowedPermissions) ? opts.allowedPermissions
43
- : opts.allowedPermissions ? [opts.allowedPermissions] : [])
44
+ const { allowedPermissions } = { ...defaultOptions, ...context.options[0] },
45
+ allowed = new Set(array.to(allowedPermissions))
44
46
  function checkPermission(node, permission) {
45
47
  if (typeof permission != 'string') return
46
48
  if (allowed.has(permission) || !permissions.sensitive.has(permission)) return
@@ -1,7 +1,8 @@
1
1
  import extension from '../utils/extension.js'
2
2
  import regex from '../data/regex.js'
3
3
 
4
- const re = { broadHost: /^(?:<all_urls>|\*:\/\/\*\/\*|\w+:\/\/\*\/\*)$/ },
4
+ const defaultOptions = { allowedPatterns: [] },
5
+ re = { broadHost: /^(?:<all_urls>|\*:\/\/\*\/\*|\w+:\/\/\*\/\*)$/ },
5
6
  hostKeys = new Set(['host_permissions', 'optional_host_permissions', 'optional_permissions', 'permissions'])
6
7
 
7
8
  export default {
@@ -14,7 +15,7 @@ export default {
14
15
  security: true,
15
16
  manifest: true
16
17
  },
17
- defaultOptions: [{ allowedPatterns: [] }],
18
+ defaultOptions: [defaultOptions],
18
19
  schema: [{
19
20
  type: 'object',
20
21
  properties: {
@@ -33,12 +34,10 @@ export default {
33
34
  },
34
35
 
35
36
  create(context) {
36
- const allowedPatterns = new Set(context.options[0]?.allowedPatterns || [])
37
+ const { allowedPatterns } = { ...defaultOptions, ...context.options[0] },
38
+ allowedPatternSet = new Set(allowedPatterns)
37
39
  function isBroad(pattern) {
38
- return typeof pattern == 'string'
39
- && !allowedPatterns.has(pattern)
40
- && re.broadHost.test(pattern)
41
- }
40
+ return typeof pattern == 'string' && !allowedPatternSet.has(pattern) && re.broadHost.test(pattern) }
42
41
  if (regex.filename.manifest.test(extension.file.getName(context))) // lint it directly
43
42
  return {
44
43
  '*'(node) {
@@ -4,6 +4,8 @@ import i18nUtils from '../utils/i18n.js'
4
4
  import i18nData from '../data/i18n.js'
5
5
  import regex from '../data/regex.js'
6
6
 
7
+ const defaultOptions = { ignoreKeys: [], scanExtensions: i18nData.defaultScanExtensions }
8
+
7
9
  export default {
8
10
  meta: {
9
11
  type: 'suggestion',
@@ -26,7 +28,7 @@ export default {
26
28
  },
27
29
  additionalProperties: false
28
30
  }],
29
- defaultOptions: [{ ignoreKeys: [], scanExtensions: i18nData.defaultScanExtensions }],
31
+ defaultOptions: [defaultOptions],
30
32
  messages: {
31
33
  unusedKey: '`{{key}}` in `_locales/{{locale}}/messages.json` is never referenced.'
32
34
  }
@@ -38,9 +40,8 @@ export default {
38
40
  const manifest = extension.manifest.read(context) ; if (!manifest?.default_locale) return {}
39
41
  const root = extension.findRoot(context) ; if (!root) return {}
40
42
  const i18n = i18nUtils.readDefaultLocaleMessages(manifest, root) ; if (!i18n) return {}
41
- const opts = context.options[0] || {},
42
- ignored = new Set(opts.ignoreKeys || []),
43
- scanExtensions = opts.scanExtensions || i18nData.defaultScanExtensions
43
+ const { ignoreKeys, scanExtensions } = { ...defaultOptions, ...context.options[0] },
44
+ ignored = new Set(ignoreKeys)
44
45
  function check() {
45
46
  const { used, patterns } = i18nUtils.collectUsedKeys(root, scanExtensions)
46
47
  for (const key of Object.keys(i18n.messages)) {