eslint-plugin-chrome-extensions 2.4.1 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +101 -23
  2. package/package.json +4 -3
  3. package/src/data/apis.js +166 -115
  4. package/src/data/deprecations.js +7 -7
  5. package/src/data/i18n.js +11 -69
  6. package/src/data/manifest.js +61 -57
  7. package/src/data/permissions.js +29 -29
  8. package/src/data/regex.js +10 -0
  9. package/src/index.js +7 -123
  10. package/src/rules/manifest-key-order.js +8 -8
  11. package/src/rules/manifest-spacing.js +5 -5
  12. package/src/rules/no-content-style-leak.js +6 -4
  13. package/src/rules/no-deprecated-apis.js +4 -4
  14. package/src/rules/no-duplicate-manifest-keys.js +3 -4
  15. package/src/rules/no-insecure-messaging.js +4 -4
  16. package/src/rules/no-invalid-permissions.js +10 -11
  17. package/src/rules/no-missing-default-locale.js +7 -9
  18. package/src/rules/no-missing-i18n-keys.js +22 -20
  19. package/src/rules/no-missing-permissions.js +7 -5
  20. package/src/rules/no-missing-web-accessible-resources.js +7 -5
  21. package/src/rules/no-remote-code.js +12 -12
  22. package/src/rules/no-sensitive-permissions.js +75 -0
  23. package/src/rules/no-unrestricted-host-permissions.js +8 -7
  24. package/src/rules/no-unused-i18n-keys.js +11 -10
  25. package/src/rules/no-unused-permissions.js +22 -23
  26. package/src/rules/require-license-file.js +9 -8
  27. package/src/rules/require-mv3.js +4 -5
  28. package/src/rules/require-short-name-when-useful.js +4 -3
  29. package/src/rules/valid-filenames.js +3 -10
  30. package/src/rules/valid-license-file.js +18 -11
  31. package/src/rules/valid-manifest-icons.js +6 -5
  32. package/src/rules/valid-mv3-schema.js +21 -21
  33. package/src/rules/valid-service-worker.js +36 -13
  34. package/src/rules/valid-url-fields.js +10 -7
  35. package/src/rules/valid-version-field.js +4 -8
  36. package/src/utils/extension.js +51 -0
  37. package/src/utils/i18n.js +55 -0
  38. package/src/utils/init.js +105 -0
  39. package/src/utils/license.js +55 -0
  40. package/src/utils/node.js +23 -0
  41. package/src/utils/pkg.js +21 -0
  42. package/src/utils/rules.js +7 -0
  43. package/src/utils/text.js +13 -0
  44. package/src/data/license.js +0 -27
  45. package/src/data/text-parser.js +0 -11
  46. package/src/utils.js +0 -95
@@ -1,9 +1,10 @@
1
+ import extension from '../utils/extension.js'
1
2
  import fs from 'node:fs'
2
3
  import path from 'node:path'
3
4
 
4
- import { findExtensionRoot, getFilename } from '../utils.js'
5
+ import regex from '../data/regex.js'
5
6
 
6
- const re = { manifestFilename: /manifest\.json$/, svg: /\.svg$/i },
7
+ const re = { svg: /\.svg$/i },
7
8
  sizes = { recommended: ['16', '32', '48', '128'], required: '128' }
8
9
 
9
10
  export default {
@@ -47,12 +48,12 @@ export default {
47
48
  },
48
49
 
49
50
  create(context) {
50
- if (!re.manifestFilename.test(getFilename(context))) return {}
51
+ if (!regex.filename.manifest.test(extension.file.getName(context))) return {}
51
52
  const opts = context.options[0] || {},
52
53
  requireRecommended = opts.requireRecommendedSizes !== false,
53
54
  checkFiles = opts.checkIconExists !== false,
54
- checkSVG = opts.noSVG !== false
55
- const root = checkFiles ? findExtensionRoot(context) : null
55
+ checkSVG = opts.noSVG !== false,
56
+ root = checkFiles ? extension.findRoot(context) : null
56
57
  return {
57
58
  '*'(node) {
58
59
  const keyNode = node.name || node.key ; if (!keyNode) return
@@ -1,9 +1,7 @@
1
- import { mv2OnlyKeys, forbiddenCSPdirectives, ffMV3bgKeys, ffDataCollectionRequired, ffDataCollectionOptional }
2
- from '../data/manifest.js'
1
+ import extension from '../utils/extension.js'
3
2
 
4
- import { getFilename, readManifest } from '../utils.js'
5
-
6
- const re = { manifestFilename: /manifest\.json$/ }
3
+ import manifestData from '../data/manifest.js'
4
+ import regex from '../data/regex.js'
7
5
 
8
6
  export default {
9
7
  meta: {
@@ -24,7 +22,7 @@ export default {
24
22
  },
25
23
 
26
24
  create(context) {
27
- if (re.manifestFilename.test(getFilename(context))) { // lint it directly
25
+ if (regex.filename.manifest.test(extension.file.getName(context))) { // lint it directly
28
26
  const ancestors = [], pendingReports = [], deferredMV2 = []
29
27
  let isMV3 = false, isFF = false
30
28
  function keyOf(node) {
@@ -44,10 +42,10 @@ export default {
44
42
  keyOf(ancestor) == 'data_collection_permissions')
45
43
  if (keyName == 'manifest_version' && node.value?.value == 3) isMV3 = true
46
44
  if (isTopLevel && keyName == 'browser_specific_settings') isFF = true
47
- if (isTopLevel && mv2OnlyKeys.has(keyName))
45
+ if (isTopLevel && manifestData.keys.mv2Only.has(keyName))
48
46
  pendingReports.push({ node, messageId: 'mv2Key',
49
47
  data: { key: keyName, reason: 'MV2-only key' }})
50
- if (inBackground && ffMV3bgKeys.has(keyName))
48
+ if (inBackground && manifestData.keys.ff.mv3BG.has(keyName))
51
49
  deferredMV2.push({ node, messageId: 'mv2Key',
52
50
  data: { key: `background.${keyName}`,
53
51
  reason: 'Use background.service_worker instead' }})
@@ -58,7 +56,7 @@ export default {
58
56
  if (isTopLevel && keyName == 'content_security_policy' && node.value?.type == 'String')
59
57
  pendingReports.push({ node: node.value, messageId: 'wrongCSPType' })
60
58
  if (inCSP && keyName == 'extension_pages' && typeof node.value?.value == 'string')
61
- for (const directive of forbiddenCSPdirectives)
59
+ for (const directive of manifestData.csp.forbiddenDirectives)
62
60
  if (node.value.value.includes(directive))
63
61
  pendingReports.push({ node: node.value, messageId: 'unsafeCSP',
64
62
  data: { directive }})
@@ -67,8 +65,9 @@ export default {
67
65
  ) for (const elem of node.value.elements) {
68
66
  const value = typeof elem.value == 'string' ? elem.value : elem.value?.value
69
67
  if (typeof value == 'string') {
70
- const set = keyName == 'required' ? ffDataCollectionRequired : ffDataCollectionOptional
71
- if (!set.has(value))
68
+ const allowedVals = manifestData.keys.ff[
69
+ `dataCollection${ keyName == 'required' ? 'Required' : 'Optional' }`]
70
+ if (!allowedVals.has(value))
72
71
  pendingReports.push({ node: elem, messageId: 'invalidDataCollectionValue',
73
72
  data: { value, kind: keyName }})
74
73
  }
@@ -86,14 +85,14 @@ export default {
86
85
  }
87
86
  }
88
87
  }
89
- const manifest = readManifest(context) ; if (!manifest || manifest.manifest_version != 3) return {}
88
+ const manifest = extension.manifest.read(context) ; if (!manifest || manifest.manifest_version != 3) return {}
90
89
  const isFF = manifest.browser_specific_settings != null
91
90
  return {
92
91
  Program(node) { /* eslint-disable security/detect-object-injection */
93
92
  for (const key of Object.keys(manifest))
94
- if (mv2OnlyKeys.has(key))
93
+ if (manifestData.keys.mv2Only.has(key))
95
94
  context.report({ node, messageId: 'mv2Key', data: { key, reason: 'MV2-only key' }})
96
- for (const key of ffMV3bgKeys)
95
+ for (const key of manifestData.keys.ff.mv3BG)
97
96
  if (manifest.background?.[key] && !isFF)
98
97
  context.report({ node, messageId: 'mv2Key',
99
98
  data: { key: `background.${key}`, reason: 'Use background.service_worker instead' }})
@@ -105,17 +104,18 @@ export default {
105
104
  context.report({ node, messageId: 'wrongCSPType' })
106
105
  const cspPages = csp?.extension_pages
107
106
  if (typeof cspPages == 'string')
108
- for (const directive of forbiddenCSPdirectives)
107
+ for (const directive of manifestData.csp.forbiddenDirectives)
109
108
  if (cspPages.includes(directive))
110
109
  context.report({ node, messageId: 'unsafeCSP', data: { directive }})
111
110
  if (isFF) { // validate data collection permissions
112
111
  const dcp = manifest.browser_specific_settings?.gecko?.data_collection_permissions
113
- for (const [kind, set] of [['required', ffDataCollectionRequired], ['optional', ffDataCollectionOptional]])
114
- if (Array.isArray(dcp?.[kind]))
115
- for (const value of dcp[kind])
116
- if (!set.has(value))
117
- context.report({ node, messageId: 'invalidDataCollectionValue',
118
- data: { value, kind }})
112
+ for (const [kind, set] of [
113
+ ['required', manifestData.keys.ff.dataCollectionRequired],
114
+ ['optional', manifestData.keys.ff.dataCollectionOptional]
115
+ ]) if (Array.isArray(dcp?.[kind]))
116
+ for (const value of dcp[kind])
117
+ if (!set.has(value))
118
+ context.report({ node, messageId: 'invalidDataCollectionValue', data: { value, kind }})
119
119
  }
120
120
  } /* eslint-enable security/detect-object-injection */
121
121
  }
@@ -1,5 +1,7 @@
1
- import { domGlobals, serviceWorkerKeepaliveMinMS } from '../data/manifest.js'
2
- import { isServiceWorkerFile, isInsideNestedFunction, readManifest } from '../utils.js'
1
+ import extension from '../utils/extension.js'
2
+ import nodeUtils from '../utils/node.js'
3
+
4
+ import manifestData from '../data/manifest.js'
3
5
 
4
6
  export default {
5
7
  meta: {
@@ -9,7 +11,23 @@ export default {
9
11
  category: 'Possible Errors',
10
12
  recommended: true
11
13
  },
12
- schema: [],
14
+ schema: [{
15
+ type: 'object',
16
+ properties: {
17
+ checkListenerTopLevel: { type: 'boolean',
18
+ description: 'Flag `chrome.*.addListener()` calls not registered at module top level' },
19
+ checkKeepaliveTimer: { type: 'boolean',
20
+ description: 'Flag sub-30s `setTimeout`/`setInterval` calls used as a keepalive workaround' },
21
+ checkDOMglobals: { type: 'boolean',
22
+ description: 'Flag DOM globals (`window`, `document`, `localStorage`) inside the service worker' }
23
+ },
24
+ additionalProperties: false
25
+ }],
26
+ defaultOptions: [{
27
+ checkListenerTopLevel: true,
28
+ checkKeepaliveTimer: true,
29
+ checkDOMglobals: true
30
+ }],
13
31
  messages: {
14
32
  listenerNotTopLevel: '`chrome.*.addListener()` must be registered at module top level'
15
33
  + ' so it attaches on service worker wake-up.',
@@ -19,29 +37,34 @@ export default {
19
37
  },
20
38
 
21
39
  create(context) {
22
- const manifest = readManifest(context) ; if (!manifest || !isServiceWorkerFile(context, manifest)) return {}
40
+ const { checkListenerTopLevel, checkKeepaliveTimer, checkDOMglobals } = context.options[0],
41
+ manifest = extension.manifest.read(context)
42
+ if (!manifest || !extension.file.isServiceWorker(context, manifest)) return {}
23
43
  return {
24
44
  CallExpression(node) {
25
- if (node.callee.type == 'MemberExpression'
45
+ if (checkListenerTopLevel
46
+ && node.callee.type == 'MemberExpression'
26
47
  && node.callee.property.name == 'addListener'
27
- && isInsideNestedFunction(node)
48
+ && nodeUtils.isInsideNestedFunction(node)
28
49
  ) context.report({ node, messageId: 'listenerNotTopLevel' })
29
- if (node.callee.type == 'Identifier'
50
+ if (checkKeepaliveTimer
51
+ && node.callee.type == 'Identifier'
30
52
  && ['setTimeout', 'setInterval'].includes(node.callee.name)
31
53
  && node.arguments[1]?.type == 'Literal'
32
54
  && typeof node.arguments[1].value == 'number'
33
- && node.arguments[1].value < serviceWorkerKeepaliveMinMS
34
- && !isInsideNestedFunction(node)
55
+ && node.arguments[1].value < 30000
56
+ && !nodeUtils.isInsideNestedFunction(node)
35
57
  ) context.report({ node, messageId: 'keepaliveTimer', data: { fn: node.callee.name }})
36
58
  },
37
59
  Identifier(node) {
60
+ if (!checkDOMglobals) return
38
61
  if (node.parent?.type == 'MemberExpression' && node.parent.property === node
39
62
  || node.parent?.type == 'Property' && node.parent.key === node && !node.parent.computed
40
- || !domGlobals.has(node.name)
63
+ || !manifestData.globals.dom.has(node.name)
41
64
  ) return
42
- for (let scope = context.sourceCode.getScope(node) ;
43
- scope && scope.type != 'global' ;
44
- scope = scope.upper
65
+ for (let scope = context.sourceCode.getScope(node)
66
+ ; scope && scope.type != 'global'
67
+ ; scope = scope.upper
45
68
  ) if (scope.set.has(node.name)) return // shadowed by a local binding
46
69
  context.report({ node, messageId: 'domInSW', data: { global: node.name }})
47
70
  }
@@ -1,9 +1,12 @@
1
- import { matchPaths, urlPaths } from '../data/manifest.js'
2
- import { getFilename } from '../utils.js'
1
+ import extension from '../utils/extension.js'
2
+
3
+ import manifestData from '../data/manifest.js'
4
+ import regex from '../data/regex.js'
3
5
 
4
6
  const re = {
5
- absoluteURL: /^[a-z][a-z0-9+.-]*:\/\/[^\s/]+/i, manifestFilename: /manifest\.json$/,
6
- match: /^(\*|https?|file|ftp):\/\/([^/]*)(\/.*)$/, template: /\{[^}]+\}/g
7
+ absoluteURL: /^[a-z][a-z0-9+.-]*:\/\/[^\s/]+/i,
8
+ match: /^(\*|https?|file|ftp):\/\/([^/]*)(\/.*)$/,
9
+ template: /\{[^}]+\}/g
7
10
  }
8
11
 
9
12
  function collectStrings(obj, path = [], out = []) {
@@ -67,7 +70,7 @@ export default {
67
70
  },
68
71
 
69
72
  create(context) {
70
- if (!re.manifestFilename.test(getFilename(context))) return {}
73
+ if (!regex.filename.manifest.test(extension.file.getName(context))) return {}
71
74
  const opts = context.options[0] || {},
72
75
  checkURLfields = opts.checkURLfields ?? true,
73
76
  checkMatchPatterns = opts.checkMatchPatterns ?? true,
@@ -79,11 +82,11 @@ export default {
79
82
  let manifest
80
83
  try { manifest = JSON.parse(context.sourceCode.text) } catch { return }
81
84
  for (const { path, value } of collectStrings(manifest)) {
82
- if (checkURLfields && urlPaths.has(path)) {
85
+ if (checkURLfields && manifestData.paths.url.has(path)) {
83
86
  if (!isURL(value))
84
87
  context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidURL',
85
88
  data: { field: path, value }})
86
- } else if (checkMatchPatterns && matchPaths.has(path)) {
89
+ } else if (checkMatchPatterns && manifestData.paths.match.has(path)) {
87
90
  if (value == '<all_urls>') {
88
91
  if (!allowAllURLs)
89
92
  context.report({ loc: { line: 1, column: 1 }, messageId: 'allURLs',
@@ -1,9 +1,9 @@
1
- import { getFilename, readManifest } from '../utils.js'
1
+ import extension from '../utils/extension.js'
2
+ import regex from '../data/regex.js'
2
3
 
3
4
  const re = { /* eslint-disable security/detect-unsafe-regex */
4
5
  chromeParts: /^\d+(?:\.\d+){0,3}$/,
5
6
  date: /^\d{4}\.(?:0?[1-9]|1[0-2])\.(?:0?[1-9]|[12]\d|3[01])(?:\.\d+)?$/,
6
- manifestFilename: /manifest\.json$/,
7
7
  semver: /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/
8
8
  } /* eslint-enable security/detect-unsafe-regex */
9
9
 
@@ -79,7 +79,7 @@ export default {
79
79
  }
80
80
  }
81
81
  }
82
- if (re.manifestFilename.test(getFilename(context))) // lint it directly
82
+ if (regex.filename.manifest.test(extension.file.getName(context))) // lint it directly
83
83
  return {
84
84
  '*'(node) {
85
85
  const keyNode = node.name || node.key ; if (!keyNode) return
@@ -88,10 +88,6 @@ export default {
88
88
  validate(valueNode, valueNode.value)
89
89
  }
90
90
  }
91
- return {
92
- Program(node) {
93
- validate(node, readManifest(context)?.version)
94
- }
95
- }
91
+ return { Program(node) { validate(node, extension.manifest.read(context)?.version) }}
96
92
  }
97
93
  }
@@ -0,0 +1,51 @@
1
+ /* eslint-disable security/detect-non-literal-fs-filename -- lint-time test fixtures only */
2
+
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+
6
+ const manifestCache = new Map()
7
+
8
+ const utils = {
9
+ file: {
10
+ getName(context) { return context.filename || context.getFilename() },
11
+
12
+ isContentScript(context, manifest) {
13
+ const scripts = (manifest?.content_scripts || []).flatMap(cs => cs.js || [])
14
+ if (!scripts.length) return false
15
+ const root = utils.findRoot(context) ; if (!root) return false
16
+ const filename = path.resolve(utils.file.getName(context))
17
+ return scripts.some(script => filename == path.resolve(root, script))
18
+ },
19
+
20
+ isServiceWorker(context, manifest) {
21
+ const worker = manifest?.background?.service_worker ; if (!worker) return false
22
+ const root = utils.findRoot(context) ; if (!root) return false
23
+ return path.resolve(utils.file.getName(context)) == path.resolve(root, worker)
24
+ }
25
+ },
26
+
27
+ findRoot(context) {
28
+ let dir = path.dirname(utils.file.getName(context))
29
+ while (dir) {
30
+ if (fs.existsSync(path.join(dir, 'manifest.json'))) return dir
31
+ const parent = path.dirname(dir)
32
+ if (parent == dir) break // reached fs root
33
+ dir = parent
34
+ }
35
+ return null
36
+ },
37
+
38
+ manifest: {
39
+ read(context) {
40
+ const root = utils.findRoot(context) ; if (!root) return null
41
+ if (manifestCache.has(root)) return manifestCache.get(root)
42
+ let parsed
43
+ try { parsed = JSON.parse(fs.readFileSync(path.join(root, 'manifest.json'), 'utf8')) }
44
+ catch { parsed = null }
45
+ manifestCache.set(root, parsed)
46
+ return parsed
47
+ }
48
+ }
49
+ }
50
+
51
+ export default utils
@@ -0,0 +1,55 @@
1
+ /* eslint-disable security/detect-non-literal-fs-filename -- lint-time test fixtures only */
2
+
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+
6
+ import i18n from '../data/i18n.js'
7
+
8
+ function templateToPattern(body) {
9
+ const parts = body.split(i18n.re.templateExpr).map(part => part.replace(i18n.re.specialChars, '\\$&'))
10
+ return new RegExp(`^${parts.join('.*')}$`) // eslint-disable-line security/detect-non-literal-regexp
11
+ }
12
+
13
+ export default {
14
+ collectUsedKeys(root, scanExtensions) {
15
+ const used = new Set(),
16
+ patterns = [],
17
+ exts = new Set(scanExtensions),
18
+ stack = [root]
19
+ while (stack.length) {
20
+ const dir = stack.pop() ; let entries
21
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }) } catch { continue }
22
+ for (const entry of entries) {
23
+ if (entry.name.startsWith('.') || entry.name == 'node_modules') continue
24
+ const fullPath = path.join(dir, entry.name)
25
+ if (entry.isDirectory()) { stack.push(fullPath) ; continue }
26
+ const ext = path.extname(entry.name).slice(1).toLowerCase()
27
+ if (!exts.has(ext)) continue
28
+ let text
29
+ try { text = fs.readFileSync(fullPath, 'utf8') } catch { continue }
30
+ for (const match of text.matchAll(i18n.re.msgPlaceholder))
31
+ if (!this.isPredefined(match[1])) used.add(match[1])
32
+ for (const match of text.matchAll(i18n.re.getMsg.literal))
33
+ used.add(match[1])
34
+ for (const match of text.matchAll(i18n.re.getMsg.template)) {
35
+ const body = match[1]
36
+ if (body.includes('${')) patterns.push(templateToPattern(body))
37
+ else used.add(body)
38
+ }
39
+ }
40
+ }
41
+ return { used, patterns }
42
+ },
43
+
44
+ isPredefined(key) { return key.startsWith('@@') },
45
+
46
+ readDefaultLocaleMessages(manifest, root) {
47
+ const locale = manifest?.default_locale
48
+ if (typeof locale != 'string') return null
49
+ const filePath = path.join(root, '_locales', locale, 'messages.json')
50
+ let messages
51
+ try { messages = JSON.parse(fs.readFileSync(filePath, 'utf8')) } catch { return null }
52
+ if (!messages || typeof messages != 'object' || Array.isArray(messages)) return null
53
+ return { locale, messages }
54
+ }
55
+ }
@@ -0,0 +1,105 @@
1
+ import css from '@eslint/css'
2
+ import html from '@html-eslint/eslint-plugin'
3
+ import json from '@eslint/json'
4
+ import rulesUtils from './rules.js'
5
+ import text from './text.js'
6
+
7
+ import manifestKeyOrder from '../rules/manifest-key-order.js'
8
+ import manifestSpacing from '../rules/manifest-spacing.js'
9
+ import noContentStyleLeak from '../rules/no-content-style-leak.js'
10
+ import noDeprecatedAPIs from '../rules/no-deprecated-apis.js'
11
+ import noDuplicateManifestKeys from '../rules/no-duplicate-manifest-keys.js'
12
+ import noInsecureFetch from '../rules/no-insecure-fetch.js'
13
+ import noInsecureMessaging from '../rules/no-insecure-messaging.js'
14
+ import noInvalidPermissions from '../rules/no-invalid-permissions.js'
15
+ import noMissingDefaultLocale from '../rules/no-missing-default-locale.js'
16
+ import noMissingI18nKeys from '../rules/no-missing-i18n-keys.js'
17
+ import noMissingPermissions from '../rules/no-missing-permissions.js'
18
+ import noMissingWebAccessibleResources from '../rules/no-missing-web-accessible-resources.js'
19
+ import noRemoteCode from '../rules/no-remote-code.js'
20
+ import noSensitivePermissions from '../rules/no-sensitive-permissions.js'
21
+ import noUnrestrictedHostPermissions from '../rules/no-unrestricted-host-permissions.js'
22
+ import noUnusedI18nKeys from '../rules/no-unused-i18n-keys.js'
23
+ import noUnusedPermissions from '../rules/no-unused-permissions.js'
24
+ import requireLicenseFile from '../rules/require-license-file.js'
25
+ import requireMV3 from '../rules/require-mv3.js'
26
+ import requireShortNameWhenUseful from '../rules/require-short-name-when-useful.js'
27
+ import validFilenames from '../rules/valid-filenames.js'
28
+ import validLicenseFile from '../rules/valid-license-file.js'
29
+ import validManifestIcons from '../rules/valid-manifest-icons.js'
30
+ import validMV3schema from '../rules/valid-mv3-schema.js'
31
+ import validServiceWorker from '../rules/valid-service-worker.js'
32
+ import validURLfields from '../rules/valid-url-fields.js'
33
+ import validVersionField from '../rules/valid-version-field.js'
34
+
35
+ const namespace = 'chrome-extensions'
36
+
37
+ export default {
38
+ bundleDeprecation(plugin, [ruleId, bundleNames], pluginName) { /* eslint-disable security/detect-object-injection */
39
+ for (const bundleName of bundleNames)
40
+ if (plugin.configs[bundleName])
41
+ plugin.configs[bundleName].rules[`${pluginName}/${ruleId}`] = 'error'
42
+ }, /* eslint-enable security/detect-object-injection */
43
+
44
+ configs(plugin) {
45
+ const plugins = { [namespace]: plugin },
46
+ rules = plugin.rules,
47
+ createConfig = predicate => ({ plugins, rules: rulesUtils.bundle(rules, predicate, namespace) })
48
+ plugin.configs = {
49
+ recommended: createConfig(rule => rule.meta.docs.recommended),
50
+ strict: createConfig(() => true),
51
+ problem: createConfig(rule => rule.meta.type == 'problem'),
52
+ security: createConfig(rule => rule.meta.docs.security),
53
+ manifest: {
54
+ ...createConfig(rule => rule.meta.docs.manifest), language: 'json/json', plugins: { ...plugins, json }},
55
+ license: {
56
+ ...createConfig(rule => rule.meta.docs.license), languageOptions: { parser: text.parser }},
57
+ css: {
58
+ ...createConfig(rule => rule.meta.docs.css), language: 'css/css', plugins: { ...plugins, css }},
59
+ html: {
60
+ ...createConfig(rule => rule.meta.docs.html), language: 'html/html', plugins: { ...plugins, html }},
61
+ json: {
62
+ ...createConfig(rule => rule.meta.docs.json), language: 'json/json', plugins: { ...plugins, json }},
63
+ json5: {
64
+ ...createConfig(rule => rule.meta.docs.json), language: 'json/json5', plugins: { ...plugins, json }},
65
+ jsonc: {
66
+ ...createConfig(rule => rule.meta.docs.json), language: 'json/jsonc', plugins: { ...plugins, json }}
67
+ }
68
+ },
69
+
70
+ plugin() { return { rules: {}, configs: {}}},
71
+
72
+ rules(plugin) {
73
+ plugin.rules = {
74
+ 'manifest-key-order': manifestKeyOrder,
75
+ 'manifest-spacing': manifestSpacing,
76
+ 'no-content-style-leak': noContentStyleLeak,
77
+ 'no-deprecated-apis': noDeprecatedAPIs,
78
+ 'no-duplicate-manifest-keys': noDuplicateManifestKeys,
79
+ 'no-insecure-fetch': noInsecureFetch,
80
+ 'no-insecure-messaging': noInsecureMessaging,
81
+ 'no-invalid-permissions': noInvalidPermissions,
82
+ 'no-missing-default-locale': noMissingDefaultLocale,
83
+ 'no-missing-i18n-keys': noMissingI18nKeys,
84
+ 'no-missing-permissions': noMissingPermissions,
85
+ 'no-missing-web-accessible-resources': noMissingWebAccessibleResources,
86
+ 'no-remote-code': noRemoteCode,
87
+ 'no-sensitive-permissions': noSensitivePermissions,
88
+ 'no-unrestricted-host-permissions': noUnrestrictedHostPermissions,
89
+ 'no-unused-i18n-keys': noUnusedI18nKeys,
90
+ 'no-unused-permissions': noUnusedPermissions,
91
+ 'require-mv3': requireMV3,
92
+ 'require-license-file': requireLicenseFile,
93
+ 'require-short-name-when-useful': requireShortNameWhenUseful,
94
+ 'valid-filenames': validFilenames,
95
+ 'valid-license-file': validLicenseFile,
96
+ 'valid-manifest-icons': validManifestIcons,
97
+ 'valid-mv3-schema': validMV3schema,
98
+ 'valid-service-worker': validServiceWorker,
99
+ 'valid-url-fields': validURLfields,
100
+ 'valid-version-field': validVersionField
101
+ }
102
+ for (const [ruleId, rule] of Object.entries(plugin.rules))
103
+ rule.meta.docs.url = `https://codeberg.org/adamlui/eslint-plugin-${namespace}/#${ruleId}`
104
+ }
105
+ }
@@ -0,0 +1,55 @@
1
+ /* eslint-disable security/detect-non-literal-fs-filename -- lint-time test fixtures only */
2
+
3
+ import { createRequire } from 'node:module'
4
+ import fs from 'node:fs'
5
+ import path from 'node:path'
6
+
7
+ import regex from '../data/regex.js'
8
+
9
+ const re = { spdxTag: /SPDX-License-Identifier:\s*([\w.-]+)/i },
10
+ require = createRequire(import.meta.url),
11
+ skipDirs = new Set(['node_modules']),
12
+ spdxIDs = new Set(require('spdx-license-ids')),
13
+ spdxNames = Object.entries(require('spdx-license-list'))
14
+ .map(([id, { name }]) => ({ id, name }))
15
+ .sort((a, b) => b.name.length - a.name.length)
16
+
17
+ function tryReaddir(dir) {
18
+ try { return fs.readdirSync(dir, { withFileTypes: true }) } catch { return null }}
19
+
20
+ export default {
21
+ find(root) {
22
+ const queue = [root]
23
+ while (queue.length) {
24
+ const dir = queue.shift(), entries = tryReaddir(dir)
25
+ if (!entries) continue
26
+ for (const entry of entries) {
27
+ if (entry.name.startsWith('.') || skipDirs.has(entry.name)) continue
28
+ if (entry.isFile() && regex.filename.license.test(entry.name)) return path.join(dir, entry.name)
29
+ if (entry.isDirectory()) queue.push(path.join(dir, entry.name))
30
+ }
31
+ }
32
+ return null
33
+ },
34
+
35
+ spdx: {
36
+ appendTag(fixer, text, id) {
37
+ const nl = text.includes('\r\n') ? '\r\n' : '\n',
38
+ trailer = `${text.endsWith(nl) ? '' : nl}${nl}SPDX-License-Identifier: ${id}${nl}`
39
+ return fixer.insertTextAfterRange([0, text.length], trailer)
40
+ },
41
+ find(text) {
42
+ const normalizedTxt = text.replace(/\s+/g, ' ')
43
+ let best = null
44
+ for (const { id, name } of spdxNames) {
45
+ const idx = normalizedTxt.indexOf(name)
46
+ if (idx == -1) continue
47
+ if (!best || idx < best.idx || (idx == best.idx && name.length > best.name.length))
48
+ best = { id, idx, name }
49
+ }
50
+ return best?.id ?? null
51
+ },
52
+ isValid(id) { return spdxIDs.has(id) },
53
+ parse(text) { return text.match(re.spdxTag)?.[1] ?? null }
54
+ }
55
+ }
@@ -0,0 +1,23 @@
1
+ export default {
2
+ getMemberPath(node) {
3
+ const parts = [] ; let currentNode = node
4
+ while (currentNode?.type == 'MemberExpression') {
5
+ if (currentNode.computed || currentNode.property.type != 'Identifier') return null
6
+ parts.unshift(currentNode.property.name)
7
+ currentNode = currentNode.object
8
+ }
9
+ if (currentNode?.type != 'Identifier') return null
10
+ parts.unshift(currentNode.name)
11
+ return parts.join('.')
12
+ },
13
+
14
+ isInsideNestedFunction(node) {
15
+ let ancestor = node.parent
16
+ while (ancestor) {
17
+ if (['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'].includes(ancestor.type))
18
+ return true
19
+ ancestor = ancestor.parent
20
+ }
21
+ return false
22
+ }
23
+ }
@@ -0,0 +1,21 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ export default {
5
+ findRoot(dir) {
6
+ let current = path.resolve(dir)
7
+ while (true) {
8
+ if (current.split(path.sep).includes('node_modules')) {
9
+ const parent = path.dirname(current)
10
+ if (parent === current) return null
11
+ current = parent
12
+ continue
13
+ }
14
+ if ( // eslint-disable-next-line security/detect-non-literal-fs-filename
15
+ fs.existsSync(path.join(current, 'package.json'))) return current
16
+ const parent = path.dirname(current)
17
+ if (parent === current) return null
18
+ current = parent
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,7 @@
1
+ export default {
2
+ bundle(rules, predicate, pluginName) {
3
+ return Object.fromEntries(Object.entries(rules)
4
+ .filter(([, rule]) => predicate(rule))
5
+ .map(([ruleId]) => [`${pluginName}/${ruleId}`, 'error']))
6
+ }
7
+ }
@@ -0,0 +1,13 @@
1
+ export default {
2
+ parser: {
3
+ parse: text => ({
4
+ type: 'Program',
5
+ body: [],
6
+ sourceType: 'module',
7
+ range: [0, text.length],
8
+ loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 0 }},
9
+ tokens: [],
10
+ comments: []
11
+ })
12
+ }
13
+ }
@@ -1,27 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
-
4
- export const re = {
5
- licenseFilename: // e.g. LICENSE.md, LICENSE-MIT, MIT-LICENSE.md, COPYING.LESSER, LICENSE-2.0.txt
6
- /^(?:[\w.-]*-)?(?:LICEN[SC]E|COPYING)(?:[-.][\w.-]+)?$/i, // eslint-disable-line security/detect-unsafe-regex
7
- spdxTag: /SPDX-License-Identifier:\s*([\w.+-]+)/i
8
- }
9
-
10
- export const skipDirs = new Set(['node_modules'])
11
-
12
- export function findLicenseFile(root) {
13
- const queue = [root]
14
- while (queue.length) {
15
- const dir = queue.shift(), entries = tryReaddir(dir)
16
- if (!entries) continue
17
- for (const entry of entries) {
18
- if (entry.name.startsWith('.') || skipDirs.has(entry.name)) continue
19
- if (entry.isFile() && re.licenseFilename.test(entry.name)) return path.join(dir, entry.name)
20
- if (entry.isDirectory()) queue.push(path.join(dir, entry.name))
21
- }
22
- }
23
- return null
24
- }
25
-
26
- function tryReaddir(dir) { // eslint-disable-next-line security/detect-non-literal-fs-filename
27
- try { return fs.readdirSync(dir, { withFileTypes: true }) } catch { return null }}