eslint-plugin-chrome-extensions 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ # 🏛️ MIT License
2
+
3
+ **Copyright © 2026 [Adam Lui](https://codeberg.org/adamlui).**
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
@@ -0,0 +1,3 @@
1
+ # Security Policy
2
+
3
+ If you find a vulnerability, please e-mail <security@kudoai.org>.
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "eslint-plugin-chrome-extensions",
3
+ "version": "1.0.0",
4
+ "description": "ESLint rules for Chrome extensions",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=18.18.0"
8
+ },
9
+ "author": {
10
+ "name": "Adam Lui",
11
+ "email": "adam@kudoai.org",
12
+ "url": "https://codeberg.org/adamlui"
13
+ },
14
+ "homepage": "https://codeberg.org/adamlui/eslint-plugin-chrome-extensions/#readme",
15
+ "bugs": {
16
+ "url": "https://codeberg.org/adamlui/eslint-plugin-chrome-extensions/issues",
17
+ "email": "support@adamlui.com"
18
+ },
19
+ "license": "MIT",
20
+ "funding": [
21
+ {
22
+ "type": "ko-fi",
23
+ "url": "https://ko-fi.com/adamlui"
24
+ },
25
+ {
26
+ "type": "paypal",
27
+ "url": "https://paypal.me/adamlui"
28
+ }
29
+ ],
30
+ "main": "src/index.js",
31
+ "files": [
32
+ "docs/",
33
+ "src/"
34
+ ],
35
+ "types": "src/index.d.ts",
36
+ "scripts": {
37
+ "prepare": "husky",
38
+ "lint": "eslint . --cache && yamllint .",
39
+ "lint:all": "eslint . && yamllint .",
40
+ "lint:fix": "eslint . --fix --cache && yamllint .",
41
+ "lint:fix-all": "eslint . --fix && yamllint .",
42
+ "test": "node tests/run.js",
43
+ "demo": "node demo/run.js",
44
+ "bump:patch": "bash utils/bump.sh patch",
45
+ "bump:minor": "bash utils/bump.sh minor",
46
+ "bump:feat": "npm run bump:minor",
47
+ "bump:major": "bash utils/bump.sh major"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://codeberg.org/adamlui/eslint-plugin-chrome-extensions.git"
52
+ },
53
+ "keywords": [
54
+ "eslint",
55
+ "eslint-plugin",
56
+ "eslintplugin",
57
+ "chrome-extensions",
58
+ "chromium"
59
+ ],
60
+ "peerDependencies": {
61
+ "eslint": ">=9.0.0"
62
+ },
63
+ "devDependencies": {
64
+ "@e18e/eslint-plugin": "^0.8.1",
65
+ "@eslint/js": "^10.0.1",
66
+ "@eslint/json": "^2.1.0",
67
+ "@eslint/markdown": "^8.0.3",
68
+ "@stylistic/eslint-plugin": "^5.10.0",
69
+ "eslint": "^10.11.0",
70
+ "eslint-plugin-constructors": "^1.4.0",
71
+ "eslint-plugin-eslint-plugin": "^7.6.2",
72
+ "eslint-plugin-import-x": "^4.17.1",
73
+ "eslint-plugin-n": "^18.4.0",
74
+ "eslint-plugin-no-secrets": "^2.3.3",
75
+ "eslint-plugin-no-unsanitized": "^4.1.5",
76
+ "eslint-plugin-promise": "^7.3.0",
77
+ "eslint-plugin-regexp": "^3.3.1",
78
+ "eslint-plugin-security": "^4.2.0",
79
+ "husky": "^9.1.7",
80
+ "yamllint-js": "^0.2.4"
81
+ },
82
+ "dependencies": {
83
+ "globals": "^17.13.0"
84
+ }
85
+ }
@@ -0,0 +1,65 @@
1
+ export const deprecatedAPIs = new Map([
2
+ ['chrome.pageAction.show', 'chrome.action.enable'],
3
+ ['chrome.pageAction.hide', 'chrome.action.disable'],
4
+ ['chrome.pageAction.setIcon', 'chrome.action.setIcon'],
5
+ ['chrome.pageAction.setTitle', 'chrome.action.setTitle'],
6
+ ['chrome.pageAction.setPopup', 'chrome.action.setPopup'],
7
+ ['chrome.pageAction.getTitle', 'chrome.action.getTitle'],
8
+ ['chrome.pageAction.getPopup', 'chrome.action.getPopup'],
9
+ ['chrome.browserAction', 'chrome.action'],
10
+ ['chrome.pageAction', 'chrome.action'],
11
+ ['chrome.extension.getBackgroundPage', 'chrome.runtime.getContexts'],
12
+ ['chrome.extension.getURL', 'chrome.runtime.getURL'],
13
+ ['chrome.extension.sendRequest', 'chrome.runtime.sendMessage'],
14
+ ['chrome.extension.onRequest', 'chrome.runtime.onMessage'],
15
+ ['chrome.extension.onRequestExternal', 'chrome.runtime.onMessageExternal'],
16
+ ['chrome.extension.connect', 'chrome.runtime.connect'],
17
+ ['chrome.extension.onConnect', 'chrome.runtime.onConnect'],
18
+ ['chrome.extension.onConnectExternal', 'chrome.runtime.onConnectExternal'],
19
+ ['chrome.tabs.executeScript', 'chrome.scripting.executeScript'],
20
+ ['chrome.tabs.insertCSS', 'chrome.scripting.insertCSS'],
21
+ ['chrome.tabs.removeCSS', 'chrome.scripting.removeCSS']
22
+ ])
23
+
24
+ export const apiPermissionMap = new Map([
25
+ ['chrome.alarms', 'alarms'],
26
+ ['chrome.bookmarks', 'bookmarks'],
27
+ ['chrome.browsingData', 'browsingData'],
28
+ ['chrome.contextMenus', 'contextMenus'],
29
+ ['chrome.cookies', 'cookies'],
30
+ ['chrome.debugger', 'debugger'],
31
+ ['chrome.declarativeNetRequest', 'declarativeNetRequest'],
32
+ ['chrome.downloads', 'downloads'],
33
+ ['chrome.history', 'history'],
34
+ ['chrome.idle', 'idle'],
35
+ ['chrome.management', 'management'],
36
+ ['chrome.notifications', 'notifications'],
37
+ ['chrome.pageCapture', 'pageCapture'],
38
+ ['chrome.privacy', 'privacy'],
39
+ ['chrome.proxy', 'proxy'],
40
+ ['chrome.scripting', 'scripting'],
41
+ ['chrome.sessions', 'sessions'],
42
+ ['chrome.storage', 'storage'],
43
+ ['chrome.tabCapture', 'tabCapture'],
44
+ ['chrome.topSites', 'topSites'],
45
+ ['chrome.webRequest', 'webRequest']
46
+ ])
47
+
48
+ export const externalMessagingListeners =
49
+ new Set(['chrome.runtime.onMessageExternal', 'chrome.runtime.onConnectExternal'])
50
+
51
+ export function findDeprecated(api) {
52
+ for (const [deprecated, replacement] of deprecatedAPIs)
53
+ if (api == deprecated || api.startsWith(`${deprecated}.`))
54
+ return { deprecated, replacement }
55
+ return null
56
+ }
57
+
58
+ export function findPermission(api) {
59
+ for (const [prefix, permission] of apiPermissionMap)
60
+ if (api == prefix || api.startsWith(`${prefix}.`))
61
+ return permission
62
+ return null
63
+ }
64
+
65
+ export function isExternalMessagingListener(api) { return externalMessagingListeners.has(api) }
@@ -0,0 +1,13 @@
1
+ import globals from 'globals'
2
+
3
+ export const mv2OnlyKeys = new Set(['browser_action', 'page_action'])
4
+ export const forbiddenCSPDirectives = [
5
+ `'unsafe-eval'`, `'unsafe-inline'`, `'unsafe-hashes'`, `'wasm-unsafe-eval'`, 'http://', 'https://']
6
+
7
+ export const serviceWorkerKeepaliveMinMS = 30000
8
+
9
+ const serviceWorkerGlobals = new Set(Object.keys(globals.serviceworker || {}))
10
+ export const domGlobals = new Set(Object.keys(globals.browser).filter(name => !serviceWorkerGlobals.has(name)))
11
+
12
+ export const remoteCodePatterns = [
13
+ 'eval', 'new-function', 'string-timer', 'dynamic-import', 'remote-script-src', 'execute-script-url']
@@ -0,0 +1,31 @@
1
+ const re = { url: /^https?:\/\// }
2
+
3
+ export const sensitivePermissions = new Set([
4
+ 'accessibilityFeatures.modify', 'accessibilityFeatures.read', 'bookmarks', 'browsingData', 'clipboardRead',
5
+ 'contentSettings', 'cookies', 'debugger', 'declarativeNetRequest', 'downloads', 'downloads.open', 'downloads.ui',
6
+ 'geolocation', 'history', 'management', 'nativeMessaging', 'pageCapture', 'privacy', 'proxy', 'readingList',
7
+ 'sessions', 'system.storage', 'tabCapture', 'tabGroups', 'tabs', 'topSites', 'ttsEngine',
8
+ 'webAuthenticationProxy', 'webNavigation', 'webRequest', 'webRequestBlocking'
9
+ ])
10
+
11
+ export const validPermissions = new Set([
12
+ 'accessibilityFeatures.modify', 'accessibilityFeatures.read', 'activeTab', 'alarms', 'audio', 'background',
13
+ 'bookmarks', 'browsingData', 'certificateProvider', 'clipboardRead', 'clipboardWrite', 'contentSettings',
14
+ 'contextMenus', 'cookies', 'debugger', 'declarativeContent', 'declarativeNetRequest',
15
+ 'declarativeNetRequestFeedback', 'declarativeNetRequestWithHostAccess', 'desktopCapture', 'dns', 'documentScan',
16
+ 'downloads', 'downloads.open', 'downloads.ui', 'enterprise.deviceAttributes', 'enterprise.hardwarePlatform',
17
+ 'enterprise.networkingAttributes', 'enterprise.platformKeys', 'favicon', 'fileBrowserHandler',
18
+ 'fileSystemProvider', 'fontSettings', 'gcm', 'geolocation', 'history', 'identity', 'identity.email', 'idle',
19
+ 'loginState', 'management', 'nativeMessaging', 'notifications', 'offscreen', 'pageCapture', 'platformKeys',
20
+ 'power', 'printerProvider', 'printing', 'printingMetrics', 'privacy', 'processes', 'proxy', 'publicSuffix',
21
+ 'readingList', 'runtime', 'scripting', 'search', 'sessions', 'sidePanel', 'storage', 'system.cpu',
22
+ 'system.display', 'system.memory', 'system.storage', 'tabCapture', 'tabGroups', 'tabs', 'topSites', 'tts',
23
+ 'ttsEngine', 'unlimitedStorage', 'userScripts', 'vpnProvider', 'wallpaper', 'webAuthenticationProxy',
24
+ 'webNavigation', 'webRequest', 'webRequestBlocking'
25
+ ])
26
+
27
+ export function isValidPermission(name, target = 'mv3') {
28
+ if (validPermissions.has(name)) return true
29
+ if (target == 'mv3') return false
30
+ return re.url.test(name) || name.startsWith('<') || name == '*://*/*'
31
+ }
package/src/index.js ADDED
@@ -0,0 +1,58 @@
1
+ import noContentStyleLeak from './rules/no-content-style-leak.js'
2
+ import noDeprecatedAPIs from './rules/no-deprecated-apis.js'
3
+ import noInsecureMessaging from './rules/no-insecure-messaging.js'
4
+ import noInvalidPermissions from './rules/no-invalid-permissions.js'
5
+ import noMissingPermissions from './rules/no-missing-permissions.js'
6
+ import noRemoteCode from './rules/no-remote-code.js'
7
+ import noUnusedPermissions from './rules/no-unused-permissions.js'
8
+ import requireMV3 from './rules/require-mv3.js'
9
+ import validMV3schema from './rules/valid-mv3-schema.js'
10
+ import validServiceWorker from './rules/valid-service-worker.js'
11
+
12
+ const plugin = {
13
+ rules: {
14
+ 'require-mv3': requireMV3,
15
+ 'valid-mv3-schema': validMV3schema,
16
+ 'no-deprecated-apis': noDeprecatedAPIs,
17
+ 'no-remote-code': noRemoteCode,
18
+ 'valid-service-worker': validServiceWorker,
19
+ 'no-content-style-leak': noContentStyleLeak,
20
+ 'no-invalid-permissions': noInvalidPermissions,
21
+ 'no-missing-permissions': noMissingPermissions,
22
+ 'no-unused-permissions': noUnusedPermissions,
23
+ 'no-insecure-messaging': noInsecureMessaging
24
+ }
25
+ }
26
+
27
+ for (const [ruleId, rule] of Object.entries(plugin.rules))
28
+ rule.meta.docs.url = `https://codeberg.org/adamlui/eslint-plugin-chrome-extensions/#${ruleId}`
29
+
30
+ plugin.configs = {
31
+ recommended: {
32
+ plugins: { 'chrome-extensions': plugin },
33
+ rules: {
34
+ 'chrome-extensions/require-mv3': 'error',
35
+ 'chrome-extensions/valid-mv3-schema': 'error',
36
+ 'chrome-extensions/no-deprecated-apis': 'error',
37
+ 'chrome-extensions/no-remote-code': 'error',
38
+ 'chrome-extensions/valid-service-worker': 'error',
39
+ 'chrome-extensions/no-invalid-permissions': 'error',
40
+ 'chrome-extensions/no-missing-permissions': 'error',
41
+ 'chrome-extensions/no-unused-permissions': 'error',
42
+ 'chrome-extensions/no-insecure-messaging': 'error'
43
+ }
44
+ },
45
+ strict: {
46
+ plugins: { 'chrome-extensions': plugin },
47
+ rules: Object.fromEntries(Object.keys(plugin.rules)
48
+ .map(ruleId => [`chrome-extensions/${ruleId}`, 'error']))
49
+ },
50
+ problem: {
51
+ plugins: { 'chrome-extensions': plugin },
52
+ rules: Object.fromEntries(Object.entries(plugin.rules)
53
+ .filter(([, rule]) => rule.meta.type == 'problem')
54
+ .map(([ruleId]) => [`chrome-extensions/${ruleId}`, 'error']))
55
+ }
56
+ }
57
+
58
+ export { plugin as default, plugin as chromeExtensions }
@@ -0,0 +1,65 @@
1
+ import { readManifest, isContentScriptFile } from '../utils.js'
2
+
3
+ export default {
4
+ meta: {
5
+ type: 'suggestion',
6
+ docs: {
7
+ description: 'Disallow injecting <style> into the host document from a content script',
8
+ category: 'Best Practices',
9
+ recommended: false
10
+ },
11
+ schema: [],
12
+ messages: {
13
+ styleLeak: 'Inject styles via `chrome.scripting.insertCSS()` or `content_scripts.css` instead of appending to the host document.'
14
+ }
15
+ },
16
+
17
+ create(context) {
18
+ const manifest = readManifest(context) ; if (!manifest || !isContentScriptFile(context, manifest)) return {}
19
+ const styleElems = new Set(), docAliases = new Set()
20
+ function isCreateStyle(node) {
21
+ return node?.type == 'CallExpression'
22
+ && node.callee?.property?.name == 'createElement'
23
+ && node.arguments[0]?.value == 'style'
24
+ }
25
+ function isObjectAssign(node) {
26
+ return node?.type == 'CallExpression'
27
+ && node.callee?.type == 'MemberExpression'
28
+ && node.callee.object?.name == 'Object'
29
+ && node.callee.property?.name == 'assign'
30
+ }
31
+ function isDocumentRef(node) {
32
+ return node?.type == 'Identifier'
33
+ && (node.name == 'document' || docAliases.has(node.name))
34
+ }
35
+ function isDocumentMember(node) {
36
+ return node?.type == 'MemberExpression' && isDocumentRef(node.object)
37
+ }
38
+ function isStyleTarget(node) {
39
+ if (isCreateStyle(node)) return true
40
+ if (node?.type == 'Identifier' && styleElems.has(node.name)) return true
41
+ if (isObjectAssign(node) && isCreateStyle(node.arguments[0])) return true
42
+ if (isObjectAssign(node)
43
+ && node.arguments[0]?.type == 'Identifier'
44
+ && styleElems.has(node.arguments[0].name)) return true
45
+ return false
46
+ }
47
+ return {
48
+ VariableDeclarator(node) {
49
+ if (node.id.type != 'Identifier') return
50
+ if (isCreateStyle(node.init)) styleElems.add(node.id.name)
51
+ if (node.init?.type == 'Identifier' && isDocumentRef(node.init))
52
+ docAliases.add(node.id.name)
53
+ },
54
+ CallExpression(node) {
55
+ if (node.callee.type != 'MemberExpression') return
56
+ const method = node.callee.property.name
57
+ if (method != 'append' && method != 'appendChild') return
58
+ if (!isDocumentMember(node.callee.object)) return
59
+ for (const arg of node.arguments)
60
+ if (isStyleTarget(arg))
61
+ context.report({ node: arg, messageId: 'styleLeak' })
62
+ }
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,60 @@
1
+ import { findDeprecated } from '../data/apis.js'
2
+ import { getMemberPath } from '../utils.js'
3
+
4
+ export default {
5
+ meta: {
6
+ type: 'problem',
7
+ docs: {
8
+ description: 'Disallow deprecated chrome.* extension API calls',
9
+ category: 'Possible Errors',
10
+ recommended: true
11
+ },
12
+ defaultOptions: [{ target: 'mv3', allowedAPIs: [] }],
13
+ schema: [{
14
+ type: 'object',
15
+ properties: {
16
+ target: {
17
+ enum: ['mv3', 'mv2'],
18
+ description: 'Chrome extension platform version whose deprecations to validate against'
19
+ },
20
+ allowedAPIs: {
21
+ oneOf: [
22
+ { type: 'string', description: 'Single deprecated API namespace to allow' },
23
+ {
24
+ type: 'array',
25
+ items: { type: 'string', description: 'Deprecated API namespace to allow' },
26
+ description: 'List of deprecated API namespaces to allow'
27
+ }
28
+ ],
29
+ description: 'Additional deprecated API namespaces to allow'
30
+ }
31
+ },
32
+ additionalProperties: false
33
+ }],
34
+ messages: {
35
+ deprecatedAPI: '`{{api}}` is deprecated in {{target}}. Use `{{replacement}}` instead.'
36
+ }
37
+ },
38
+
39
+ create(context) {
40
+ const opts = context.options[0] || {},
41
+ target = opts.target || 'mv3',
42
+ allowedAPIs = Array.isArray(opts.allowedAPIs) ? opts.allowedAPIs
43
+ : opts.allowedAPIs ? [opts.allowedAPIs] : []
44
+ if (target != 'mv3') return {}
45
+ function isAllowed(api) {
46
+ for (const allowed of allowedAPIs)
47
+ if (api == allowed || api.startsWith(`${allowed}.`)) return true
48
+ return false
49
+ }
50
+ return {
51
+ MemberExpression(node) {
52
+ if (node.parent.type == 'MemberExpression' && node.parent.object === node) return
53
+ const api = getMemberPath(node) ; if (!api?.startsWith('chrome.') || isAllowed(api)) return
54
+ const found = findDeprecated(api) ; if (!found) return
55
+ context.report({ node, messageId: 'deprecatedAPI',
56
+ data: { api: found.deprecated, replacement: found.replacement, target }})
57
+ }
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,42 @@
1
+ import { isExternalMessagingListener } from '../data/apis.js'
2
+ import { getMemberPath } from '../utils.js'
3
+
4
+ const RE_SENDER_VALIDATION = /sender\.(?:id|origin)/
5
+
6
+ export default {
7
+ meta: {
8
+ type: 'problem',
9
+ docs: {
10
+ description: 'Require sender validation on external messaging + filtered tab broadcasts',
11
+ category: 'Possible Errors',
12
+ recommended: true
13
+ },
14
+ schema: [],
15
+ messages: {
16
+ unvalidatedExternal: '`{{listener}}` handler must validate `sender.<id|origin>` before processing messages.',
17
+ unfilteredBroadcast: '`chrome.tabs.query({})` with no filter broadcasts to every open tab - add `url` or `currentWindow`.'
18
+ }
19
+ },
20
+
21
+ create(context) {
22
+ return {
23
+ CallExpression(node) {
24
+ if (node.callee.type != 'MemberExpression') return
25
+ const path = getMemberPath(node.callee) ; if (!path) return
26
+ const listenerRoot = path.endsWith('.addListener') ? path.slice(0, -'.addListener'.length) : null
27
+ if (listenerRoot && isExternalMessagingListener(listenerRoot)) {
28
+ const handler = node.arguments[0] ; if (!handler) return
29
+ const body = context.sourceCode.getText(handler)
30
+ if (!RE_SENDER_VALIDATION.test(body))
31
+ context.report({ node, messageId: 'unvalidatedExternal', data: { listener: listenerRoot }})
32
+ }
33
+ if (path == 'chrome.tabs.query') {
34
+ const filter = node.arguments[0],
35
+ isEmpty = !filter || (filter.type == 'ObjectExpression' && !filter.properties.length)
36
+ if (isEmpty)
37
+ context.report({ node, messageId: 'unfilteredBroadcast' })
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,57 @@
1
+ import { isValidPermission } from '../data/permissions.js'
2
+ import { readManifest } from '../utils.js'
3
+
4
+ export default {
5
+ meta: {
6
+ type: 'problem',
7
+ docs: {
8
+ description: 'Disallow invalid or unsupported manifest permission names',
9
+ category: 'Possible Errors',
10
+ recommended: true
11
+ },
12
+ defaultOptions: [{ target: 'mv3', allowedPermissions: [] }],
13
+ schema: [{
14
+ type: 'object',
15
+ properties: {
16
+ target: {
17
+ enum: ['mv3', 'mv2'],
18
+ description: 'Chrome extension platform version whose permissions to validate against'
19
+ },
20
+ allowedPermissions: {
21
+ oneOf: [
22
+ { type: 'string', description: 'Single permission name to allow' },
23
+ {
24
+ type: 'array',
25
+ items: { type: 'string', description: 'Permission name to allow' },
26
+ description: 'List of permission names to allow'
27
+ }
28
+ ],
29
+ description: 'Additional permission names to allow beyond the built-in list'
30
+ }
31
+ },
32
+ additionalProperties: false
33
+ }],
34
+ messages: {
35
+ invalidPermission: '`{{permission}}` is not a valid {{target}} permission.'
36
+ }
37
+ },
38
+
39
+ create(context) {
40
+ const opts = context.options[0] || {},
41
+ target = opts.target || 'mv3',
42
+ allowed = new Set(Array.isArray(opts.allowedPermissions) ? opts.allowedPermissions
43
+ : opts.allowedPermissions ? [opts.allowedPermissions] : []),
44
+ manifest = readManifest(context)
45
+ if (!manifest) return {}
46
+ const all = [...(manifest.permissions || []), ...(manifest.optional_permissions || [])]
47
+ return {
48
+ 'Program:exit'() {
49
+ for (const permission of all) {
50
+ if (allowed.has(permission) || isValidPermission(permission, target)) continue
51
+ context.report({ loc: { line: 1, column: 0 },
52
+ messageId: 'invalidPermission', data: { permission, target }})
53
+ }
54
+ }
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,30 @@
1
+ import { findPermission } from '../data/apis.js'
2
+ import { readManifest, getMemberPath } from '../utils.js'
3
+
4
+ export default {
5
+ meta: {
6
+ type: 'problem',
7
+ docs: {
8
+ description: 'Flag chrome.* API calls used without a matching manifest permission',
9
+ category: 'Possible Errors',
10
+ recommended: true
11
+ },
12
+ schema: [],
13
+ messages: {
14
+ missingPermission: '`{{api}}` requires the "{{permission}}" manifest permission.'
15
+ }
16
+ },
17
+
18
+ create(context) {
19
+ const manifest = readManifest(context) ; if (!manifest) return {}
20
+ const declared = new Set([...(manifest.permissions || []), ...(manifest.optional_permissions || [])])
21
+ return {
22
+ CallExpression(node) {
23
+ if (node.callee.type != 'MemberExpression') return
24
+ const api = getMemberPath(node.callee) ; if (!api) return
25
+ const permission = findPermission(api) ; if (!permission || declared.has(permission)) return
26
+ context.report({ node, messageId: 'missingPermission', data: { api, permission }})
27
+ }
28
+ }
29
+ }
30
+ }