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.
- package/README.md +272 -44
- package/package.json +13 -10
- package/src/data/apis.js +74 -88
- package/src/data/manifest.js +3 -1
- package/src/data/regex.js +2 -1
- package/src/index.d.ts +42 -0
- package/src/rules/css-spacing.js +29 -21
- package/src/rules/manifest-key-order.js +1 -1
- package/src/rules/manifest-spacing.js +1 -1
- package/src/rules/no-content-style-leak.js +12 -11
- package/src/rules/no-deprecated-apis.js +2 -2
- package/src/rules/no-host-page-selectors.js +5 -6
- package/src/rules/no-inline-event-handlers.js +116 -0
- package/src/rules/no-inline-scripts.js +65 -0
- package/src/rules/no-insecure-fetch.js +7 -6
- package/src/rules/no-insecure-messaging.js +4 -4
- package/src/rules/no-invalid-locales.js +3 -5
- package/src/rules/no-missing-default-locale.js +1 -1
- package/src/rules/no-missing-permissions.js +2 -3
- package/src/rules/no-missing-web-accessible-resources.js +2 -2
- package/src/rules/no-remote-code.js +77 -28
- package/src/rules/no-remote-objects.js +55 -0
- package/src/rules/no-remote-urls.js +20 -20
- package/src/rules/no-scrollbar-styling.js +8 -10
- package/src/rules/no-unrestricted-host-permissions.js +1 -1
- package/src/rules/no-unused-permissions.js +5 -4
- package/src/rules/require-license-file.js +1 -1
- package/src/rules/require-mv3.js +3 -2
- package/src/rules/valid-filenames.js +3 -3
- package/src/rules/valid-manifest-icons.js +2 -2
- package/src/rules/valid-mv3-schema.js +5 -5
- package/src/rules/valid-url-fields.js +2 -2
- package/src/rules/valid-version-field.js +2 -2
- package/src/utils/api.js +20 -0
- package/src/utils/extension.js +1 -3
- package/src/utils/html.js +29 -0
- package/src/utils/i18n.js +2 -2
- package/src/utils/init.js +11 -3
- package/src/utils/license.js +1 -1
- package/src/utils/node.js +3 -1
- package/src/utils/pkg.js +19 -2
- 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'],
|
|
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',
|
|
35
|
-
['chrome.accessibilityFeatures.clear',
|
|
36
|
-
['chrome.accessibilityFeatures',
|
|
37
|
-
['chrome.alarms',
|
|
38
|
-
['chrome.audio',
|
|
39
|
-
['chrome.bookmarks',
|
|
40
|
-
['chrome.browsingData',
|
|
41
|
-
['chrome.certificateProvider',
|
|
42
|
-
['chrome.contentSettings',
|
|
43
|
-
['chrome.contextMenus',
|
|
44
|
-
['chrome.cookies',
|
|
45
|
-
['chrome.debugger',
|
|
46
|
-
['chrome.declarativeContent',
|
|
47
|
-
['chrome.declarativeNetRequest',
|
|
48
|
-
['chrome.dns',
|
|
49
|
-
['chrome.desktopCapture',
|
|
50
|
-
['chrome.documentScan',
|
|
51
|
-
['chrome.downloads.open',
|
|
52
|
-
['chrome.downloads.setUiOptions',
|
|
53
|
-
['chrome.downloads',
|
|
54
|
-
['chrome.enterprise.deviceAttributes',
|
|
55
|
-
['chrome.enterprise.hardwarePlatform',
|
|
56
|
-
['chrome.enterprise.networkingAttributes','enterprise.networkingAttributes'],
|
|
57
|
-
['chrome.enterprise.platformKeys',
|
|
58
|
-
['chrome.favicon',
|
|
59
|
-
['chrome.fileBrowserHandler',
|
|
60
|
-
['chrome.fontSettings',
|
|
61
|
-
['chrome.gcm',
|
|
62
|
-
['chrome.instanceID',
|
|
63
|
-
['chrome.history',
|
|
64
|
-
['chrome.identity.getProfileUserInfo',
|
|
65
|
-
['chrome.identity',
|
|
66
|
-
['chrome.idle',
|
|
67
|
-
['chrome.loginState',
|
|
68
|
-
['chrome.management',
|
|
69
|
-
['chrome.runtime.connectNative',
|
|
70
|
-
['chrome.runtime.sendNativeMessage',
|
|
71
|
-
['chrome.notifications',
|
|
72
|
-
['chrome.offscreen',
|
|
73
|
-
['chrome.pageCapture',
|
|
74
|
-
['chrome.platformKeys',
|
|
75
|
-
['chrome.power',
|
|
76
|
-
['chrome.printerProvider',
|
|
77
|
-
['chrome.printingMetrics',
|
|
78
|
-
['chrome.printing',
|
|
79
|
-
['chrome.privacy',
|
|
80
|
-
['chrome.processes',
|
|
81
|
-
['chrome.proxy',
|
|
82
|
-
['chrome.publicSuffix',
|
|
83
|
-
['chrome.readingList',
|
|
84
|
-
['chrome.scripting',
|
|
85
|
-
['chrome.search',
|
|
86
|
-
['chrome.sessions',
|
|
87
|
-
['chrome.sidePanel',
|
|
88
|
-
['chrome.storage',
|
|
89
|
-
['chrome.system.cpu',
|
|
90
|
-
['chrome.system.display',
|
|
91
|
-
['chrome.system.memory',
|
|
92
|
-
['chrome.system.storage',
|
|
93
|
-
['chrome.tabCapture',
|
|
94
|
-
['chrome.tabGroups',
|
|
95
|
-
['chrome.tabs.query',
|
|
96
|
-
['chrome.topSites',
|
|
97
|
-
['chrome.ttsEngine',
|
|
98
|
-
['chrome.tts',
|
|
99
|
-
['chrome.userScripts',
|
|
100
|
-
['chrome.vpnProvider',
|
|
101
|
-
['chrome.wallpaper',
|
|
102
|
-
['chrome.webAuthenticationProxy',
|
|
103
|
-
['chrome.webNavigation',
|
|
104
|
-
['chrome.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', //
|
|
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
|
}
|
package/src/data/manifest.js
CHANGED
|
@@ -60,6 +60,8 @@ export default {
|
|
|
60
60
|
},
|
|
61
61
|
patterns: {
|
|
62
62
|
remoteCode: [
|
|
63
|
-
'
|
|
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: //
|
|
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 }
|
package/src/rules/css-spacing.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
|
|
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]
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
223
|
-
if (ch == ')') { depth
|
|
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)
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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++) //
|
|
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], //
|
|
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 =
|
|
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
|
|
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 (
|
|
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
|
|
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
|
+
}
|