eslint-plugin-chrome-extensions 2.7.0 → 2.8.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 (42) hide show
  1. package/README.md +272 -44
  2. package/package.json +13 -10
  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
package/src/data/apis.js CHANGED
@@ -19,7 +19,7 @@ export default {
19
19
  ['chrome.pageAction.setPopup', 'chrome.action.setPopup'],
20
20
  ['chrome.pageAction.getTitle', 'chrome.action.getTitle'],
21
21
  ['chrome.pageAction.getPopup', 'chrome.action.getPopup'],
22
- ['chrome.pageAction', 'chrome.action'], // must stay after pageAction.* — prefix match
22
+ ['chrome.pageAction', 'chrome.action'],
23
23
  ['chrome.tabs.executeScript', 'chrome.scripting.executeScript'],
24
24
  ['chrome.tabs.getAllInWindow', 'chrome.tabs.query({ currentWindow: true })'],
25
25
  ['chrome.tabs.getSelected', 'chrome.tabs.query({ active: true })'],
@@ -31,77 +31,77 @@ export default {
31
31
  ]),
32
32
  externalMsgListeners: new Set(['chrome.runtime.onMessageExternal', 'chrome.runtime.onConnectExternal']),
33
33
  permissionMap: new Map([
34
- ['chrome.accessibilityFeatures.set', 'accessibilityFeatures.modify'],
35
- ['chrome.accessibilityFeatures.clear', 'accessibilityFeatures.modify'],
36
- ['chrome.accessibilityFeatures', 'accessibilityFeatures.read'],
37
- ['chrome.alarms', 'alarms'],
38
- ['chrome.audio', 'audio'],
39
- ['chrome.bookmarks', 'bookmarks'],
40
- ['chrome.browsingData', 'browsingData'],
41
- ['chrome.certificateProvider', 'certificateProvider'],
42
- ['chrome.contentSettings', 'contentSettings'],
43
- ['chrome.contextMenus', 'contextMenus'],
44
- ['chrome.cookies', 'cookies'],
45
- ['chrome.debugger', 'debugger'],
46
- ['chrome.declarativeContent', 'declarativeContent'],
47
- ['chrome.declarativeNetRequest', 'declarativeNetRequest'],
48
- ['chrome.dns', 'dns'],
49
- ['chrome.desktopCapture', 'desktopCapture'],
50
- ['chrome.documentScan', 'documentScan'],
51
- ['chrome.downloads.open', 'downloads.open'],
52
- ['chrome.downloads.setUiOptions', 'downloads.ui'],
53
- ['chrome.downloads', 'downloads'],
54
- ['chrome.enterprise.deviceAttributes', 'enterprise.deviceAttributes'],
55
- ['chrome.enterprise.hardwarePlatform', 'enterprise.hardwarePlatform'],
56
- ['chrome.enterprise.networkingAttributes','enterprise.networkingAttributes'],
57
- ['chrome.enterprise.platformKeys', 'enterprise.platformKeys'],
58
- ['chrome.favicon', 'favicon'],
59
- ['chrome.fileBrowserHandler', 'fileBrowserHandler'],
60
- ['chrome.fontSettings', 'fontSettings'],
61
- ['chrome.gcm', 'gcm'],
62
- ['chrome.instanceID', 'gcm'],
63
- ['chrome.history', 'history'],
64
- ['chrome.identity.getProfileUserInfo', 'identity.email'],
65
- ['chrome.identity', 'identity'],
66
- ['chrome.idle', 'idle'],
67
- ['chrome.loginState', 'loginState'],
68
- ['chrome.management', 'management'],
69
- ['chrome.runtime.connectNative', 'nativeMessaging'],
70
- ['chrome.runtime.sendNativeMessage', 'nativeMessaging'],
71
- ['chrome.notifications', 'notifications'],
72
- ['chrome.offscreen', 'offscreen'],
73
- ['chrome.pageCapture', 'pageCapture'],
74
- ['chrome.platformKeys', 'platformKeys'],
75
- ['chrome.power', 'power'],
76
- ['chrome.printerProvider', 'printerProvider'],
77
- ['chrome.printingMetrics', 'printingMetrics'],
78
- ['chrome.printing', 'printing'],
79
- ['chrome.privacy', 'privacy'],
80
- ['chrome.processes', 'processes'],
81
- ['chrome.proxy', 'proxy'],
82
- ['chrome.publicSuffix', 'publicSuffix'],
83
- ['chrome.readingList', 'readingList'],
84
- ['chrome.scripting', 'scripting'],
85
- ['chrome.search', 'search'],
86
- ['chrome.sessions', 'sessions'],
87
- ['chrome.sidePanel', 'sidePanel'],
88
- ['chrome.storage', 'storage'],
89
- ['chrome.system.cpu', 'system.cpu'],
90
- ['chrome.system.display', 'system.display'],
91
- ['chrome.system.memory', 'system.memory'],
92
- ['chrome.system.storage', 'system.storage'],
93
- ['chrome.tabCapture', 'tabCapture'],
94
- ['chrome.tabGroups', 'tabGroups'],
95
- ['chrome.tabs.query', 'tabs'],
96
- ['chrome.topSites', 'topSites'],
97
- ['chrome.ttsEngine', 'ttsEngine'],
98
- ['chrome.tts', 'tts'],
99
- ['chrome.userScripts', 'userScripts'],
100
- ['chrome.vpnProvider', 'vpnProvider'],
101
- ['chrome.wallpaper', 'wallpaper'],
102
- ['chrome.webAuthenticationProxy', 'webAuthenticationProxy'],
103
- ['chrome.webNavigation', 'webNavigation'],
104
- ['chrome.webRequest', 'webRequest']
34
+ ['chrome.accessibilityFeatures.set', 'accessibilityFeatures.modify'],
35
+ ['chrome.accessibilityFeatures.clear', 'accessibilityFeatures.modify'],
36
+ ['chrome.accessibilityFeatures', 'accessibilityFeatures.read'],
37
+ ['chrome.alarms', 'alarms'],
38
+ ['chrome.audio', 'audio'],
39
+ ['chrome.bookmarks', 'bookmarks'],
40
+ ['chrome.browsingData', 'browsingData'],
41
+ ['chrome.certificateProvider', 'certificateProvider'],
42
+ ['chrome.contentSettings', 'contentSettings'],
43
+ ['chrome.contextMenus', 'contextMenus'],
44
+ ['chrome.cookies', 'cookies'],
45
+ ['chrome.debugger', 'debugger'],
46
+ ['chrome.declarativeContent', 'declarativeContent'],
47
+ ['chrome.declarativeNetRequest', 'declarativeNetRequest'],
48
+ ['chrome.dns', 'dns'],
49
+ ['chrome.desktopCapture', 'desktopCapture'],
50
+ ['chrome.documentScan', 'documentScan'],
51
+ ['chrome.downloads.open', 'downloads.open'],
52
+ ['chrome.downloads.setUiOptions', 'downloads.ui'],
53
+ ['chrome.downloads', 'downloads'],
54
+ ['chrome.enterprise.deviceAttributes', 'enterprise.deviceAttributes'],
55
+ ['chrome.enterprise.hardwarePlatform', 'enterprise.hardwarePlatform'],
56
+ ['chrome.enterprise.networkingAttributes', 'enterprise.networkingAttributes'],
57
+ ['chrome.enterprise.platformKeys', 'enterprise.platformKeys'],
58
+ ['chrome.favicon', 'favicon'],
59
+ ['chrome.fileBrowserHandler', 'fileBrowserHandler'],
60
+ ['chrome.fontSettings', 'fontSettings'],
61
+ ['chrome.gcm', 'gcm'],
62
+ ['chrome.instanceID', 'gcm'],
63
+ ['chrome.history', 'history'],
64
+ ['chrome.identity.getProfileUserInfo', 'identity.email'],
65
+ ['chrome.identity', 'identity'],
66
+ ['chrome.idle', 'idle'],
67
+ ['chrome.loginState', 'loginState'],
68
+ ['chrome.management', 'management'],
69
+ ['chrome.runtime.connectNative', 'nativeMessaging'],
70
+ ['chrome.runtime.sendNativeMessage', 'nativeMessaging'],
71
+ ['chrome.notifications', 'notifications'],
72
+ ['chrome.offscreen', 'offscreen'],
73
+ ['chrome.pageCapture', 'pageCapture'],
74
+ ['chrome.platformKeys', 'platformKeys'],
75
+ ['chrome.power', 'power'],
76
+ ['chrome.printerProvider', 'printerProvider'],
77
+ ['chrome.printingMetrics', 'printingMetrics'],
78
+ ['chrome.printing', 'printing'],
79
+ ['chrome.privacy', 'privacy'],
80
+ ['chrome.processes', 'processes'],
81
+ ['chrome.proxy', 'proxy'],
82
+ ['chrome.publicSuffix', 'publicSuffix'],
83
+ ['chrome.readingList', 'readingList'],
84
+ ['chrome.scripting', 'scripting'],
85
+ ['chrome.search', 'search'],
86
+ ['chrome.sessions', 'sessions'],
87
+ ['chrome.sidePanel', 'sidePanel'],
88
+ ['chrome.storage', 'storage'],
89
+ ['chrome.system.cpu', 'system.cpu'],
90
+ ['chrome.system.display', 'system.display'],
91
+ ['chrome.system.memory', 'system.memory'],
92
+ ['chrome.system.storage', 'system.storage'],
93
+ ['chrome.tabCapture', 'tabCapture'],
94
+ ['chrome.tabGroups', 'tabGroups'],
95
+ ['chrome.tabs.query', 'tabs'],
96
+ ['chrome.topSites', 'topSites'],
97
+ ['chrome.ttsEngine', 'ttsEngine'],
98
+ ['chrome.tts', 'tts'],
99
+ ['chrome.userScripts', 'userScripts'],
100
+ ['chrome.vpnProvider', 'vpnProvider'],
101
+ ['chrome.wallpaper', 'wallpaper'],
102
+ ['chrome.webAuthenticationProxy', 'webAuthenticationProxy'],
103
+ ['chrome.webNavigation', 'webNavigation'],
104
+ ['chrome.webRequest', 'webRequest']
105
105
  ]),
106
106
  permless: new Set([
107
107
  'chrome.i18n',
@@ -112,7 +112,7 @@ export default {
112
112
  'chrome.runtime.getManifest',
113
113
  'chrome.runtime.getPlatformInfo',
114
114
  'chrome.runtime.getURL',
115
- 'chrome.runtime.onBrowserUpdateAvailable', // eslint-disable-line no-secrets/no-secrets
115
+ 'chrome.runtime.onBrowserUpdateAvailable', // oxlint-disable-line no-secrets/no-secrets
116
116
  'chrome.runtime.onConnect',
117
117
  'chrome.runtime.onConnectExternal',
118
118
  'chrome.runtime.onInstalled',
@@ -149,19 +149,5 @@ export default {
149
149
  'chrome.tabs.sendMessage',
150
150
  'chrome.tabs.update',
151
151
  'chrome.windows'
152
- ]),
153
- findDeprecated(api) {
154
- for (const [deprecated, replacement] of this.deprecated)
155
- if (api == deprecated || api.startsWith(`${deprecated}.`))
156
- return { deprecated, replacement }
157
- return null
158
- },
159
- findPermission(api) {
160
- if (this.permless.has(api)) return null
161
- for (const [prefix, permission] of this.permissionMap)
162
- if (api == prefix || api.startsWith(`${prefix}.`))
163
- return permission
164
- return null
165
- },
166
- isExternalMsgListener(api) { return this.externalMsgListeners.has(api) }
152
+ ])
167
153
  }
@@ -60,6 +60,8 @@ export default {
60
60
  },
61
61
  patterns: {
62
62
  remoteCode: [
63
- 'eval', 'new-function', 'string-timer', 'dynamic-import', 'remote-script-src', 'execute-script-url']
63
+ 'dynamic-import', 'eval', 'execute-script-url', 'inline-event-handler', 'inline-script-body',
64
+ 'new-function', 'remote-script-src', 'string-timer'
65
+ ]
64
66
  }
65
67
  }
package/src/data/regex.js CHANGED
@@ -1,11 +1,12 @@
1
1
  export default {
2
2
  filename: {
3
3
  css: /\.css$/i,
4
- license: // eslint-disable-next-line security/detect-unsafe-regex
4
+ license: // oxlint-disable-next-line security/detect-unsafe-regex
5
5
  /^(?:[\w.-]*-)?(?:LICEN[SC]E|COPYING)(?:[-.][\w.-]+)?$/i,
6
6
  manifest: /manifest\.json$/,
7
7
  script: /\.[cm]?[jt]sx?$/i
8
8
  },
9
9
  prefix: { at: /^@/, css: /^CSS/ },
10
+ scheme: /^(?:([a-z][a-z0-9+.-]*):|\/\/)/i,
10
11
  url: /^https?:\/\//
11
12
  }
package/src/index.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ type RuleSeverity = 0 | 1 | 2 | 'off' | 'warn' | 'error'
2
+ type RuleSetting = RuleSeverity | [RuleSeverity, ...unknown[]]
3
+
4
+ type ConfigName =
5
+ | 'recommended'
6
+ | 'strict'
7
+ | 'problem'
8
+ | 'security'
9
+ | 'manifest'
10
+ | 'license'
11
+ | 'css'
12
+ | 'html'
13
+ | 'json'
14
+ | 'json5'
15
+ | 'jsonc'
16
+
17
+ interface PluginConfig {
18
+ plugins: Record<string, unknown>
19
+ rules: Record<string, RuleSetting>
20
+ language?: string
21
+ languageOptions?: Record<string, unknown>
22
+ }
23
+
24
+ interface PluginRule {
25
+ meta: {
26
+ type?: 'problem' | 'suggestion' | 'layout'
27
+ deprecated?: boolean | {
28
+ message?: string
29
+ replacedBy?: Array<{ rule: { name: string } }>
30
+ }
31
+ docs?: Record<string, unknown>
32
+ }
33
+ create: (context: unknown) => Record<string, (...args: unknown[]) => void>
34
+ }
35
+
36
+ declare const plugin: {
37
+ meta: { name: string }
38
+ rules: Record<string, PluginRule>
39
+ configs: Record<ConfigName, PluginConfig>
40
+ }
41
+
42
+ export { plugin as default, plugin as chromeExtensions }
@@ -1,13 +1,18 @@
1
- /* eslint-disable security/detect-object-injection -- array indices are controlled */
1
+ // oxlint-disable security/detect-object-injection
2
2
 
3
3
  import extension from '../utils/extension.js'
4
4
  import regex from '../data/regex.js'
5
5
 
6
- const re = { leadingWhitespace: /^[ \t]+/, trailingWhitespace: /[ \t]+$/ },
6
+ const re = { leadingWhitespace: /^[ \t]+/ },
7
7
  combinators = new Set(['>', '+', '~'])
8
8
 
9
9
  function endOf(node) { return node.loc.end.offset }
10
10
  function startOf(node) { return node.loc.start.offset }
11
+ function getTrailingWs(str) {
12
+ let end = str.length
13
+ while (end > 0 && (str[end -1] == ' ' || str[end -1] == '\t')) end--
14
+ return str.slice(end)
15
+ }
11
16
 
12
17
  export default {
13
18
  meta: {
@@ -92,7 +97,7 @@ export default {
92
97
  loc: block.loc.start, messageId: 'spaceAfterBrace',
93
98
  fix(fixer) { return fixer.replaceTextRange([innerStart, innerStart + leadingWs.length], ' ') }
94
99
  })
95
- const trailingWs = interior.match(re.trailingWhitespace)?.[0] ?? ''
100
+ const trailingWs = getTrailingWs(interior)
96
101
  if (trailingWs != ' ')
97
102
  context.report({
98
103
  loc: block.loc.end, messageId: 'spaceBeforeBrace',
@@ -117,7 +122,7 @@ export default {
117
122
  loc: node.loc.start, messageId: 'spaceAfterBracket',
118
123
  fix(fixer) { return fixer.replaceTextRange([innerStart, innerStart + leadingWs.length], '') }
119
124
  })
120
- const trailingWs = interior.match(re.trailingWhitespace)?.[0] ?? ''
125
+ const trailingWs = getTrailingWs(interior)
121
126
  if (trailingWs)
122
127
  context.report({
123
128
  loc: node.loc.end, messageId: 'spaceBeforeBracket',
@@ -134,7 +139,7 @@ export default {
134
139
  if (colonPos < 0) return
135
140
  const before = between.slice(0, colonPos),
136
141
  after = between.slice(colonPos +1),
137
- beforeTrailing = before.match(re.trailingWhitespace)?.[0] ?? ''
142
+ beforeTrailing = getTrailingWs(before)
138
143
  if (beforeTrailing) {
139
144
  const fixEnd = declStart + colonPos
140
145
  context.report({
@@ -210,20 +215,20 @@ export default {
210
215
  if (openIdx < 0 || closeIdx <= openIdx) return
211
216
  const interior = nodeText.slice(openIdx +1, closeIdx)
212
217
  if (interior.includes('\n')) return
213
- let depth = 0, inString = null
214
- for (let i = 0 ; i < interior.length ; i++) {
218
+ let depth = 0, inString = null, i = 0
219
+ while (i < interior.length) {
215
220
  const ch = interior[i]
216
221
  if (inString) {
217
- if (ch == '\\') { i++; continue }
222
+ if (ch == '\\') { i += 2 ; continue }
218
223
  if (ch === inString) inString = null
219
- continue
224
+ i++ ; continue
220
225
  }
221
- if (ch == '"' || ch === `'`) { inString = ch; continue }
222
- if (ch == '(') { depth++; continue }
223
- if (ch == ')') { depth--; continue }
226
+ if (ch == '"' || ch === `'`) { inString = ch ; i++ ; continue }
227
+ if (ch == '(') { depth++ ; i++ ; continue }
228
+ if (ch == ')') { depth-- ; i++ ; continue }
224
229
  if (ch == ',' && depth === 0) {
225
230
  const commaPos = nodeStart + openIdx +1 + i,
226
- before = interior.slice(0, i).match(re.trailingWhitespace)?.[0] ?? '',
231
+ before = getTrailingWs(interior.slice(0, i)),
227
232
  after = interior.slice(i +1).match(re.leadingWhitespace)?.[0] ?? ''
228
233
  if (before)
229
234
  context.report({
@@ -236,6 +241,7 @@ export default {
236
241
  fix(fixer) { return fixer.replaceTextRange([commaPos +1, commaPos +1 + after.length], ' ') }
237
242
  })
238
243
  }
244
+ i++
239
245
  }
240
246
  }
241
247
  function checkIndent(lineNum, expectedDepth) {
@@ -277,7 +283,7 @@ export default {
277
283
  [nodeStart + openIdx +1, nodeStart + openIdx +1 + leadingWs.length], '')
278
284
  }
279
285
  })
280
- const trailingWs = interior.match(re.trailingWhitespace)?.[0] ?? ''
286
+ const trailingWs = getTrailingWs(interior)
281
287
  if (trailingWs)
282
288
  context.report({
283
289
  loc: node.loc.end, messageId: 'spaceBeforeParen',
@@ -302,19 +308,19 @@ export default {
302
308
  function checkValueSpacing(valueStart, valueEnd) {
303
309
  const rawText = text.slice(valueStart, valueEnd),
304
310
  scanStart = valueStart + (rawText.length - rawText.trimStart().length),
305
- scanEnd = valueEnd - (rawText.length - rawText.trimEnd().length)
311
+ scanEnd = valueEnd - getTrailingWs(rawText).length
306
312
  if (scanEnd <= scanStart) return
307
313
  const valueText = text.slice(scanStart, scanEnd)
308
314
  if (valueText.includes('\n')) return
309
- let inString = null
310
- for (let i = 0 ; i < valueText.length ; i++) {
315
+ let inString = null, i = 0
316
+ while (i < valueText.length) {
311
317
  const ch = valueText[i]
312
318
  if (inString) {
313
- if (ch == '\\') { i++; continue }
319
+ if (ch == '\\') { i += 2 ; continue }
314
320
  if (ch === inString) inString = null
315
- continue
321
+ i++ ; continue
316
322
  }
317
- if (ch == '"' || ch === `'`) { inString = ch; continue }
323
+ if (ch == '"' || ch === `'`) { inString = ch ; i++ ; continue }
318
324
  if (ch == ' ' && valueText[i +1] == ' ') {
319
325
  let end = i
320
326
  while (end < valueText.length -1 && valueText[end +1] == ' ') end++
@@ -323,8 +329,10 @@ export default {
323
329
  loc: offsetToLoc(runStart), messageId: 'spaceBetweenValues',
324
330
  fix(fixer) { return fixer.replaceTextRange([runStart, scanStart + end +1], ' ') }
325
331
  })
326
- i = end
332
+ i = end +1
333
+ continue
327
334
  }
335
+ i++
328
336
  }
329
337
  }
330
338
  function walkTree(node, depth) {
@@ -14,7 +14,7 @@ function findViolation(members, keys) {
14
14
  const key = keyOf(member)
15
15
  return { member, key, rank: ranks.get(key) }
16
16
  }).filter(item => item.rank !== undefined)
17
- for (let i = 1 ; i < known.length ; i++) // eslint-disable-next-line security/detect-object-injection
17
+ for (let i = 1 ; i < known.length ; i++) // oxlint-disable-next-line security/detect-object-injection
18
18
  if (known[i -1].rank > known[i].rank) return [known[i -1], known[i]]
19
19
  }
20
20
 
@@ -102,7 +102,7 @@ export default {
102
102
  prevToken = token
103
103
  }
104
104
  for (let t = 0 ; t < tokens.length ; t++) { // check inline spacing
105
- const token = tokens[t], // eslint-disable-line security/detect-object-injection
105
+ const token = tokens[t], // oxlint-disable-line security/detect-object-injection
106
106
  type = token.type,
107
107
  prev = tokens[t -1], next = tokens[t +1]
108
108
  if (next) {
@@ -2,6 +2,18 @@ import extension from '../utils/extension.js'
2
2
 
3
3
  const re = { appendMethod: /^append(?:Child)?$/ }
4
4
 
5
+ function isCreateStyle(node) {
6
+ return node?.type == 'CallExpression'
7
+ && node.callee?.property?.name == 'createElement'
8
+ && node.arguments[0]?.value == 'style'
9
+ }
10
+ function isObjectAssign(node) {
11
+ return node?.type == 'CallExpression'
12
+ && node.callee?.type == 'MemberExpression'
13
+ && node.callee.object?.name == 'Object'
14
+ && node.callee.property?.name == 'assign'
15
+ }
16
+
5
17
  export default {
6
18
  meta: {
7
19
  type: 'suggestion',
@@ -20,17 +32,6 @@ export default {
20
32
  const manifest = extension.manifest.read(context)
21
33
  if (!manifest || !extension.file.isContentScript(context, manifest)) return {}
22
34
  const styleElems = new Set(), docAliases = new Set()
23
- function isCreateStyle(node) {
24
- return node?.type == 'CallExpression'
25
- && node.callee?.property?.name == 'createElement'
26
- && node.arguments[0]?.value == 'style'
27
- }
28
- function isObjectAssign(node) {
29
- return node?.type == 'CallExpression'
30
- && node.callee?.type == 'MemberExpression'
31
- && node.callee.object?.name == 'Object'
32
- && node.callee.property?.name == 'assign'
33
- }
34
35
  function isDocumentRef(node) {
35
36
  return node?.type == 'Identifier'
36
37
  && (node.name == 'document' || docAliases.has(node.name))
@@ -1,5 +1,5 @@
1
+ import apiUtils from '../utils/api.js'
1
2
  import nodeUtils from '../utils/node.js'
2
- import apis from '../data/apis.js'
3
3
 
4
4
  export default {
5
5
  meta: {
@@ -51,7 +51,7 @@ export default {
51
51
  MemberExpression(node) {
52
52
  if (node.parent.type == 'MemberExpression' && node.parent.object === node) return
53
53
  const api = nodeUtils.getMemberPath(node) ; if (!api?.startsWith('chrome.') || isAllowed(api)) return
54
- const found = apis.findDeprecated(api) ; if (!found) return
54
+ const found = apiUtils.findDeprecated(api) ; if (!found) return
55
55
  context.report({ node, messageId: 'deprecatedAPI',
56
56
  data: { api: found.deprecated, replacement: found.replacement, target }})
57
57
  }
@@ -1,5 +1,5 @@
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 = { keyframeSelector: /^(?:from|to|[\d.]+%)$/i }
5
5
 
@@ -61,7 +61,7 @@ export default {
61
61
  if (re.keyframeSelector.test(text) || allowedSelectors.has(text)) return
62
62
  const simples = []
63
63
  for (const child of selector.children ?? []) {
64
- if (plainType(child) == 'Combinator') break
64
+ if (node.plainType(child) == 'Combinator') break
65
65
  simples.push(child)
66
66
  }
67
67
  let hasQualifier = false,
@@ -69,8 +69,8 @@ export default {
69
69
  isRoot = false,
70
70
  elementTypes = []
71
71
  for (const simple of simples) {
72
- const type = plainType(simple)
73
- if (type == 'ClassSelector' || type == 'IdSelector' || type == 'AttributeSelector') hasQualifier = true
72
+ const type = node.plainType(simple)
73
+ if (['AttributeSelector', 'ClassSelector', 'IdSelector'].includes(type)) hasQualifier = true
74
74
  else if (type == 'PseudoClassSelector' && (simple.name ?? '').toLowerCase() == 'root') isRoot = true
75
75
  else if (type == 'TypeSelector') {
76
76
  const name = (simple.name ?? '').toLowerCase()
@@ -93,14 +93,13 @@ export default {
93
93
  }
94
94
  function getSelectorText(selector) {
95
95
  const range = selector.range
96
- if (range && range.length == 2) return context.sourceCode.text.slice(range[0], range[1])
96
+ if (range?.length == 2) return context.sourceCode.text.slice(range[0], range[1])
97
97
  return context.sourceCode.getText(selector)
98
98
  }
99
99
  function handleRule(node) {
100
100
  for (const selector of node.prelude?.children ?? [])
101
101
  checkSelector(selector)
102
102
  }
103
- function plainType(node) { return node?.type?.replace(regex.prefix.css, '') ?? '' }
104
103
  return { Rule: handleRule, CSSRule: handleRule }
105
104
  }
106
105
  }
@@ -0,0 +1,116 @@
1
+ /* oxlint-disable security/detect-object-injection */
2
+
3
+ import array from '../utils/array.js'
4
+ import html from '../utils/html.js'
5
+
6
+ const re = {
7
+ attrNameEnd: /[\s=>]/,
8
+ block: {
9
+ script: /(<script\b[^>]*>)([\s\S]*?)(<\/script\s*>)/gi,
10
+ style: /(<style\b[^>]*>)([\s\S]*?)(<\/style\s*>)/gi
11
+ },
12
+ htmlComment: /<!--[\s\S]*?-->/g,
13
+ inlineHandler: /^on[a-z]+$/i,
14
+ quote: /["']/,
15
+ tagOpen: /<[a-z][\w-]*/gi,
16
+ whitespace: /\s/
17
+ }
18
+
19
+ function isQuote(char) { return re.quote.test(char) }
20
+ function maskInner(_, open, inner, close) { return open + spaces(inner.length) + close }
21
+ function spaces(n) { return ' '.repeat(n) }
22
+
23
+ function parseAttrs(attrPart) {
24
+ const attrs = [] ; let i = 0
25
+ while (i < attrPart.length) {
26
+ while (i < attrPart.length && re.whitespace.test(attrPart[i])) i++
27
+ if (i >= attrPart.length) break
28
+ const nameStart = i
29
+ while (i < attrPart.length && !re.attrNameEnd.test(attrPart[i])) i++
30
+ const name = attrPart.slice(nameStart, i)
31
+ if (!name) { i++ ; continue }
32
+ while (i < attrPart.length && re.whitespace.test(attrPart[i])) i++
33
+ if (attrPart[i] == '=') {
34
+ i++
35
+ while (i < attrPart.length && re.whitespace.test(attrPart[i])) i++
36
+ if (isQuote(attrPart[i])) {
37
+ const q = attrPart[i]
38
+ i++
39
+ while (i < attrPart.length && attrPart[i] !== q) i++
40
+ i++
41
+ } else
42
+ while (i < attrPart.length && !re.whitespace.test(attrPart[i]) && attrPart[i] != '>') i++
43
+ }
44
+ attrs.push({ name, offset: nameStart })
45
+ }
46
+ return attrs
47
+ }
48
+
49
+ export default {
50
+ meta: {
51
+ type: 'problem',
52
+ docs: {
53
+ description: 'Disallow inline event handlers (e.g. `onclick`, `onload`) in HTML '
54
+ + '(blocked by MV3\'s CSP at runtime)',
55
+ category: 'Possible Errors',
56
+ html: true
57
+ },
58
+ defaultOptions: [{ allowedAttributes: [] }],
59
+ schema: [{
60
+ type: 'object',
61
+ properties: {
62
+ allowedAttributes: {
63
+ oneOf: [
64
+ { type: 'string', description: 'A single attribute name' },
65
+ { type: 'array', items: { type: 'string' }, description: 'List of attribute names' }
66
+ ],
67
+ description: 'Inline event handler attributes to permit'
68
+ }
69
+ },
70
+ additionalProperties: false
71
+ }],
72
+ messages: {
73
+ inlineHandler: 'Inline event handler `{{attr}}` is blocked by MV3\'s `script-src \'self\'`. '
74
+ + 'Move the handler to JS with `addEventListener()`.'
75
+ }
76
+ },
77
+
78
+ create(context) {
79
+ const opts = context.options[0] || {},
80
+ allowedAttributes = new Set(array.to(opts.allowedAttributes ?? []).map(a => a.toLowerCase())),
81
+ text = context.sourceCode.text
82
+ let scanned = false
83
+ function maskContent() {
84
+ return text
85
+ .replace(re.htmlComment, match => spaces(match.length))
86
+ .replace(re.block.script, maskInner)
87
+ .replace(re.block.style, maskInner)
88
+ }
89
+ function scan() {
90
+ if (scanned) return
91
+ scanned = true
92
+ if (!html.isHTML(text)) return
93
+ const masked = maskContent()
94
+ for (const tagMatch of masked.matchAll(re.tagOpen)) {
95
+ const tagEnd = tagMatch.index + tagMatch[0].length
96
+ let i = tagEnd, quote = null
97
+ while (i < masked.length) {
98
+ const char = masked[i]
99
+ if (quote) { if (char === quote) quote = null }
100
+ else if (isQuote(char)) quote = char
101
+ else if (char == '>') break
102
+ i++
103
+ }
104
+ const attrPart = masked.slice(tagEnd, i)
105
+ for (const attr of parseAttrs(attrPart)) {
106
+ if (!re.inlineHandler.test(attr.name)) continue
107
+ const attrLower = attr.name.toLowerCase()
108
+ if (allowedAttributes.has(attrLower)) continue
109
+ context.report({ loc: html.offsetToLoc(text, tagEnd + attr.offset),
110
+ messageId: 'inlineHandler', data: { attr: attrLower }})
111
+ }
112
+ }
113
+ }
114
+ return { 'Program:exit': scan, 'Document:exit': scan, '*': scan }
115
+ }
116
+ }