eslint-plugin-greasemonkey 1.0.0 → 1.2.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.
package/src/index.js CHANGED
@@ -1,33 +1,42 @@
1
- import metaSpacing from './rules/meta-spacing.js'
2
- import noUnusedGrants from './rules/no-unused-grants.js'
3
- import noUnusedResources from './rules/no-unused-resources.js'
4
- import requireMatchingLocalizedFields from './rules/require-matching-localized-fields.js'
5
- import requireRequiredFields from './rules/require-required-fields.js'
6
- import validFieldValues from './rules/valid-field-values.js'
7
-
8
- const plugin = {
9
- rules: {
10
- 'meta-spacing': metaSpacing,
11
- 'require-matching-localized-fields': requireMatchingLocalizedFields,
12
- 'require-required-fields': requireRequiredFields,
13
- 'valid-field-values': validFieldValues,
14
- 'no-unused-resources': noUnusedResources,
15
- 'no-unused-grants': noUnusedGrants
16
- }
17
- }
18
-
19
- plugin.configs = {
20
- recommended: {
21
- plugins: { greasemonkey: plugin },
22
- rules: {
23
- 'greasemonkey/meta-spacing': 'error',
24
- 'greasemonkey/require-matching-localized-fields': 'error',
25
- 'greasemonkey/require-required-fields': 'error',
26
- 'greasemonkey/valid-field-values': 'error',
27
- 'greasemonkey/no-unused-resources': 'error',
28
- 'greasemonkey/no-unused-grants': 'error'
29
- }
30
- }
31
- }
32
-
33
- export default plugin
1
+ import metaSpacing from './rules/meta-spacing.js'
2
+ import noDuplicateKeys from './rules/no-duplicate-keys.js'
3
+ import noUnusedGrants from './rules/no-unused-grants.js'
4
+ import noUnusedResources from './rules/no-unused-resources.js'
5
+ import requireMatchingLocalizedFields from './rules/require-matching-localized-fields.js'
6
+ import requireRequiredFields from './rules/require-required-fields.js'
7
+ import requireUserJsFilename from './rules/require-user-js-filename.js'
8
+ import validFieldValues from './rules/valid-field-values.js'
9
+ import validGrants from './rules/valid-grants.js'
10
+
11
+ const plugin = {
12
+ rules: {
13
+ 'meta-spacing': metaSpacing,
14
+ 'no-duplicate-keys': noDuplicateKeys,
15
+ 'no-unused-grants': noUnusedGrants,
16
+ 'no-unused-resources': noUnusedResources,
17
+ 'require-matching-localized-fields': requireMatchingLocalizedFields,
18
+ 'require-required-fields': requireRequiredFields,
19
+ 'require-user-js-filename': requireUserJsFilename,
20
+ 'valid-field-values': validFieldValues,
21
+ 'valid-grants': validGrants
22
+ }
23
+ }
24
+
25
+ plugin.configs = {
26
+ recommended: {
27
+ plugins: { greasemonkey: plugin },
28
+ rules: {
29
+ 'greasemonkey/meta-spacing': 'error',
30
+ 'greasemonkey/no-duplicate-keys': 'error',
31
+ 'greasemonkey/no-unused-grants': 'error',
32
+ 'greasemonkey/no-unused-resources': 'error',
33
+ 'greasemonkey/require-matching-localized-fields': 'error',
34
+ 'greasemonkey/require-required-fields': 'error',
35
+ 'greasemonkey/require-user-js-filename': 'error',
36
+ 'greasemonkey/valid-field-values': 'error',
37
+ 'greasemonkey/valid-grants': 'error'
38
+ }
39
+ }
40
+ }
41
+
42
+ export default plugin
@@ -1,47 +1,51 @@
1
- import { extractMetadata } from '../utils.js'
2
-
3
- const reLeadingWhitespace = /^\s*/
4
-
5
- export default {
6
- meta: {
7
- type: 'layout',
8
- docs: {
9
- description: 'Align metadata values to the column after the longest key',
10
- category: 'Stylistic Issues',
11
- recommended: true
12
- },
13
- fixable: 'whitespace',
14
- schema: [{ type: 'number', description: 'Minimum spaces after the longest key (default 3)' }],
15
- defaultOptions: [3],
16
- messages: { misaligned: 'Metadata values should be aligned at column {{column}}' }
17
- },
18
-
19
- create(context) {
20
- const minSpaces = context.options[0] ?? 3
21
- return {
22
- Program() {
23
- const sourceCode = context.sourceCode,
24
- metaEntries = extractMetadata(sourceCode)
25
- if (!metaEntries.length) return
26
- const longestKey = metaEntries.reduce((max, entry) => Math.max(max, entry.key.length), 0),
27
- targetColumn = longestKey + minSpaces +4
28
- for (const entry of metaEntries) {
29
- const keyLen = entry.key.length,
30
- commentText = sourceCode.getText(entry.comment),
31
- actualSpaces = commentText.split(entry.key)[1].match(reLeadingWhitespace)[0].length,
32
- desiredSpaces = targetColumn - keyLen -4
33
- if (actualSpaces != desiredSpaces)
34
- context.report({
35
- node: entry.comment, messageId: 'misaligned', data: { column: targetColumn },
36
- fix(fixer) {
37
- const keyIdx = commentText.indexOf(entry.key) + entry.key.length,
38
- rest = commentText.slice(keyIdx).replace(reLeadingWhitespace, ''),
39
- fixedText = commentText.slice(0, keyIdx) + ' '.repeat(desiredSpaces) + rest
40
- return fixer.replaceText(entry.comment, fixedText)
41
- }
42
- })
43
- }
44
- }
45
- }
46
- }
47
- }
1
+ import { extractMetadata } from '../utils.js'
2
+
3
+ const re = { leadingWhitespace: /^\s*/, whitespace: /\s+/ }
4
+
5
+ export default {
6
+ meta: {
7
+ type: 'layout',
8
+ docs: {
9
+ description: 'Align metadata values to the column after the longest key',
10
+ category: 'Stylistic Issues',
11
+ recommended: true
12
+ },
13
+ fixable: 'whitespace',
14
+ schema: [{ type: 'number', description: 'Minimum spaces after the longest key (default 3)' }],
15
+ defaultOptions: [3],
16
+ messages: { misaligned: 'Metadata values should be aligned at column {{column}}' }
17
+ },
18
+
19
+ create(context) {
20
+ const minSpaces = context.options[0] ?? 3
21
+ return {
22
+ Program() {
23
+ const sourceCode = context.sourceCode,
24
+ metaEntries = extractMetadata(sourceCode)
25
+ if (!metaEntries.length) return
26
+ let targetCol = metaEntries.reduce((max, entry) => Math.max(max, entry.key.length), 0) + minSpaces +4
27
+ for (const entry of metaEntries)
28
+ if (entry.key == 'resource') {
29
+ const name = entry.value.trim().split(re.whitespace)[0],
30
+ requiredCol = 4 + entry.key.length +1 + name.length + minSpaces
31
+ if (requiredCol > targetCol) targetCol = requiredCol
32
+ }
33
+ for (const entry of metaEntries) {
34
+ const commentText = sourceCode.getText(entry.comment),
35
+ keyIdx = commentText.indexOf(entry.key) + entry.key.length,
36
+ parts = entry.value.trim().split(re.whitespace),
37
+ named = entry.key == 'resource' && parts.length >= 2,
38
+ prefix = named ? ` ${parts[0]} ` : '',
39
+ suffix = named ? parts.slice(1).join(' ') : entry.value.trim(),
40
+ fixedText = commentText.slice(0, keyIdx) + prefix
41
+ + ' '.repeat(targetCol - keyIdx - prefix.length) + suffix
42
+ if (commentText !== fixedText)
43
+ context.report({
44
+ node: entry.comment, messageId: 'misaligned', data: { column: targetCol },
45
+ fix(fixer) { return fixer.replaceText(entry.comment, fixedText) }
46
+ })
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,35 @@
1
+ import { extractMetadata } from '../utils.js'
2
+
3
+ const MULTI_VALUE_KEYS = new Set([
4
+ 'compatible', 'connect', 'description', 'exclude', 'exclude-match', 'grant',
5
+ 'homepageURL', 'include', 'license', 'match', 'require', 'resource', 'supportURL'
6
+ ])
7
+
8
+ export default {
9
+ meta: {
10
+ type: 'problem',
11
+ docs: {
12
+ description: 'Disallow duplicate metadata keys',
13
+ category: 'Possible Errors',
14
+ recommended: true
15
+ },
16
+ schema: [],
17
+ messages: { dupeKey: 'Duplicate @{{key}}' }
18
+ },
19
+
20
+ create(context) {
21
+ return {
22
+ Program() {
23
+ const metaEntries = extractMetadata(context.sourceCode),
24
+ seen = new Set()
25
+ for (const entry of metaEntries) {
26
+ const baseKey = entry.key.split(':')[0]
27
+ if (!entry.key.includes(':') && MULTI_VALUE_KEYS.has(baseKey)) continue
28
+ if (seen.has(entry.key))
29
+ context.report({ node: entry.comment, messageId: 'dupeKey', data: { key: entry.key }})
30
+ else seen.add(entry.key)
31
+ }
32
+ }
33
+ }
34
+ }
35
+ }
@@ -1,36 +1,33 @@
1
- import { extractMetadata } from '../utils.js'
2
-
3
- export default {
4
- meta: {
5
- type: 'problem',
6
- docs: {
7
- description: 'Flag @grant entries whose APIs are never used',
8
- category: 'Possible Errors',
9
- recommended: true
10
- },
11
- schema: [],
12
- messages: {
13
- unusedGrant: 'Unused @grant: "{{grant}}". If used indirectly,'
14
- + ' add // eslint-disable-line greasemonkey/no-unused-grants'
15
- }
16
- },
17
-
18
- create(context) {
19
- const usedIdentifiers = new Set()
20
- return {
21
- Identifier(node) { usedIdentifiers.add(node.name) },
22
- 'Program:exit'() {
23
- const sourceCode = context.sourceCode,
24
- metaEntries = extractMetadata(sourceCode),
25
- grants = metaEntries.filter(e => e.key == 'grant')
26
- if (!grants.length) return
27
- for (const grant of grants) {
28
- const grantName = grant.value.trim()
29
- if (['none', 'unsafeWindow'].includes(grantName)) continue
30
- if (!usedIdentifiers.has(grantName))
31
- context.report({ node: grant.comment, messageId: 'unusedGrant', data: { grant: grantName }})
32
- }
33
- }
34
- }
35
- }
36
- }
1
+ import { extractMetadata } from '../utils.js'
2
+
3
+ export default {
4
+ meta: {
5
+ type: 'problem',
6
+ docs: {
7
+ description: 'Flag @grant entries whose APIs are never used',
8
+ category: 'Possible Errors',
9
+ recommended: true
10
+ },
11
+ schema: [],
12
+ messages: { unusedGrant: 'Unused @grant: "{{grant}}".' }
13
+ },
14
+
15
+ create(context) {
16
+ const usedIdentifiers = new Set()
17
+ return {
18
+ Identifier(node) { usedIdentifiers.add(node.name) },
19
+ 'Program:exit'() {
20
+ const sourceCode = context.sourceCode,
21
+ metaEntries = extractMetadata(sourceCode),
22
+ grants = metaEntries.filter(e => e.key == 'grant')
23
+ if (!grants.length) return
24
+ for (const grant of grants) {
25
+ const grantName = grant.value.trim()
26
+ if (['none', 'unsafeWindow'].includes(grantName)) continue
27
+ if (!usedIdentifiers.has(grantName))
28
+ context.report({ node: grant.comment, messageId: 'unusedGrant', data: { grant: grantName }})
29
+ }
30
+ }
31
+ }
32
+ }
33
+ }
@@ -1,40 +1,36 @@
1
- import { extractMetadata } from '../utils.js'
2
-
3
- const reWhitespace = /\s+/
4
-
5
- export default {
6
- meta: {
7
- type: 'problem',
8
- docs: {
9
- description: 'Flag @resource entries that are never referenced directly',
10
- category: 'Possible Errors',
11
- recommended: true
12
- },
13
- schema: [],
14
- messages: {
15
- unusedResource: 'Unused @resource: "{{name}}". If used indirectly,'
16
- + ' add // eslint-disable-line greasemonkey/no-unused-resources'
17
- }
18
- },
19
-
20
- create(context) {
21
- return {
22
- Program() {
23
- const sourceCode = context.sourceCode,
24
- metaEntries = extractMetadata(sourceCode),
25
- resources = metaEntries.filter(e => e.key == 'resource')
26
- if (!resources.length) return
27
- const text = sourceCode.getText(),
28
- usedResources = new Set(),
29
- regex = /GM_getResource(?:Text|URL)\s*\(\s*['"]([^'"]+)['"]\s*\)/g
30
- let match
31
- while ((match = regex.exec(text)) !== null) usedResources.add(match[1])
32
- for (const res of resources) {
33
- const name = res.value.split(reWhitespace)[0]
34
- if (!usedResources.has(name))
35
- context.report({ node: res.comment, messageId: 'unusedResource', data: { name }})
36
- }
37
- }
38
- }
39
- }
40
- }
1
+ import { extractMetadata } from '../utils.js'
2
+
3
+ const re = { resourceGetter: /GM_getResource(?:Text|URL)\s*\(\s*['"]([^'"]+)['"]\s*\)/g, whitespace: /\s+/ }
4
+
5
+ export default {
6
+ meta: {
7
+ type: 'problem',
8
+ docs: {
9
+ description: 'Flag @resource entries that are never referenced directly',
10
+ category: 'Possible Errors',
11
+ recommended: true
12
+ },
13
+ schema: [],
14
+ messages: { unusedResource: 'Unused @resource: "{{name}}".' }
15
+ },
16
+
17
+ create(context) {
18
+ return {
19
+ Program() {
20
+ const sourceCode = context.sourceCode,
21
+ metaEntries = extractMetadata(sourceCode),
22
+ resources = metaEntries.filter(e => e.key == 'resource')
23
+ if (!resources.length) return
24
+ const text = sourceCode.getText(),
25
+ usedResources = new Set()
26
+ let match
27
+ while ((match = re.resourceGetter.exec(text)) !== null) usedResources.add(match[1])
28
+ for (const res of resources) {
29
+ const name = res.value.split(re.whitespace)[0]
30
+ if (!usedResources.has(name))
31
+ context.report({ node: res.comment, messageId: 'unusedResource', data: { name }})
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
@@ -1,53 +1,53 @@
1
- import { extractMetadata } from '../utils.js'
2
-
3
- export default {
4
- meta: {
5
- type: 'problem',
6
- docs: {
7
- description: 'Require matching localized name and description fields',
8
- category: 'Possible Errors',
9
- recommended: true
10
- },
11
- schema: [{
12
- type: 'object',
13
- properties: {
14
- ignoreLanguages: {
15
- type: 'array',
16
- items: { type: 'string' },
17
- description: 'Language codes to ignore (e.g., ["fr", "de"])'
18
- }
19
- },
20
- additionalProperties: false
21
- }],
22
- defaultOptions: [{}],
23
- messages: { missingPair: 'Missing matching @{{missing}} for @{{present}}' }
24
- },
25
-
26
- create(context) {
27
- const options = context.options[0] || {},
28
- ignoreLanguages = options.ignoreLanguages || []
29
- return {
30
- Program() {
31
- const metaEntries = extractMetadata(context.sourceCode),
32
- nameEntries = metaEntries.filter(e => e.key.startsWith('name:')),
33
- descEntries = metaEntries.filter(e => e.key.startsWith('description:')),
34
- nameLangs = new Set(nameEntries.map(e => e.key.split(':')[1])),
35
- descLangs = new Set(descEntries.map(e => e.key.split(':')[1]))
36
- for (const lang of nameLangs)
37
- if (!ignoreLanguages.includes(lang) && !descLangs.has(lang))
38
- context.report({
39
- node: nameEntries.find(entry => entry.key == `name:${lang}`).comment,
40
- messageId: 'missingPair',
41
- data: { missing: `description:${lang}`, present: `name:${lang}` }
42
- })
43
- for (const lang of descLangs)
44
- if (!ignoreLanguages.includes(lang) && !nameLangs.has(lang))
45
- context.report({
46
- node: descEntries.find(e => e.key == `description:${lang}`).comment,
47
- messageId: 'missingPair',
48
- data: { missing: `name:${lang}`, present: `description:${lang}` }
49
- })
50
- }
51
- }
52
- }
53
- }
1
+ import { extractMetadata } from '../utils.js'
2
+
3
+ export default {
4
+ meta: {
5
+ type: 'problem',
6
+ docs: {
7
+ description: 'Require matching localized name and description fields',
8
+ category: 'Possible Errors',
9
+ recommended: true
10
+ },
11
+ schema: [{
12
+ type: 'object',
13
+ properties: {
14
+ ignoreLanguages: {
15
+ type: 'array',
16
+ items: { type: 'string' },
17
+ description: 'Language codes to ignore (e.g., ["fr", "de"])'
18
+ }
19
+ },
20
+ additionalProperties: false
21
+ }],
22
+ defaultOptions: [{}],
23
+ messages: { missingPair: 'Missing matching @{{missing}} for @{{present}}' }
24
+ },
25
+
26
+ create(context) {
27
+ const options = context.options[0] || {},
28
+ ignoreLanguages = options.ignoreLanguages || []
29
+ return {
30
+ Program() {
31
+ const metaEntries = extractMetadata(context.sourceCode),
32
+ nameEntries = metaEntries.filter(e => e.key.startsWith('name:')),
33
+ descEntries = metaEntries.filter(e => e.key.startsWith('description:')),
34
+ nameLangs = new Set(nameEntries.map(e => e.key.split(':')[1])),
35
+ descLangs = new Set(descEntries.map(e => e.key.split(':')[1]))
36
+ for (const lang of nameLangs)
37
+ if (!ignoreLanguages.includes(lang) && !descLangs.has(lang))
38
+ context.report({
39
+ node: nameEntries.find(entry => entry.key == `name:${lang}`).comment,
40
+ messageId: 'missingPair',
41
+ data: { missing: `description:${lang}`, present: `name:${lang}` }
42
+ })
43
+ for (const lang of descLangs)
44
+ if (!ignoreLanguages.includes(lang) && !nameLangs.has(lang))
45
+ context.report({
46
+ node: descEntries.find(e => e.key == `description:${lang}`).comment,
47
+ messageId: 'missingPair',
48
+ data: { missing: `name:${lang}`, present: `description:${lang}` }
49
+ })
50
+ }
51
+ }
52
+ }
53
+ }
@@ -1,41 +1,41 @@
1
- import { extractMetadata } from '../utils.js'
2
-
3
- export default {
4
- meta: {
5
- type: 'problem',
6
- docs: {
7
- description: 'Require essential metadata fields',
8
- category: 'Possible Errors',
9
- recommended: true
10
- },
11
- schema: [{
12
- type: 'object',
13
- properties: {
14
- fields: {
15
- type: 'array',
16
- items: { type: 'string' },
17
- description: 'Required metadata fields (default: name, namespace, version)'
18
- }
19
- },
20
- additionalProperties: false
21
- }],
22
- defaultOptions: [{}],
23
- messages: { missingField: 'Missing required metadata field: @{{field}}' }
24
- },
25
-
26
- create(context) {
27
- const options = context.options[0] || {},
28
- requiredFields = options.fields || ['name', 'namespace', 'version']
29
- return {
30
- Program(node) {
31
- const metaEntries = extractMetadata(context.sourceCode),
32
- keys = new Set(metaEntries.map(e => e.key))
33
- for (const field of requiredFields)
34
- if (!keys.has(field))
35
- context.report({ node, messageId: 'missingField', data: { field }})
36
- if (!metaEntries.some(entry => ['include', 'match'].includes(entry.key)))
37
- context.report({ node, messageId: 'missingField', data: { field: 'match or include' }})
38
- }
39
- }
40
- }
41
- }
1
+ import { extractMetadata } from '../utils.js'
2
+
3
+ export default {
4
+ meta: {
5
+ type: 'problem',
6
+ docs: {
7
+ description: 'Require essential metadata fields',
8
+ category: 'Possible Errors',
9
+ recommended: true
10
+ },
11
+ schema: [{
12
+ type: 'object',
13
+ properties: {
14
+ fields: {
15
+ type: 'array',
16
+ items: { type: 'string' },
17
+ description: 'Required metadata fields (default: name, namespace, version)'
18
+ }
19
+ },
20
+ additionalProperties: false
21
+ }],
22
+ defaultOptions: [{}],
23
+ messages: { missingField: 'Missing required metadata field: @{{field}}' }
24
+ },
25
+
26
+ create(context) {
27
+ const options = context.options[0] || {},
28
+ requiredFields = options.fields || ['name', 'namespace', 'version']
29
+ return {
30
+ Program(node) {
31
+ const metaEntries = extractMetadata(context.sourceCode),
32
+ keys = new Set(metaEntries.map(e => e.key))
33
+ for (const field of requiredFields)
34
+ if (!keys.has(field))
35
+ context.report({ node, messageId: 'missingField', data: { field }})
36
+ if (!metaEntries.some(entry => ['include', 'match'].includes(entry.key)))
37
+ context.report({ node, messageId: 'missingField', data: { field: 'match or include' }})
38
+ }
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,25 @@
1
+ import { extractMetadata } from '../utils.js'
2
+
3
+ export default {
4
+ meta: {
5
+ type: 'problem',
6
+ docs: {
7
+ description: 'Require that userscript filenames end in ".user.js"',
8
+ category: 'Possible Errors',
9
+ recommended: true
10
+ },
11
+ schema: [],
12
+ messages: { requireUserJs: 'Userscript filename should end in ".user.js"' }
13
+ },
14
+
15
+ create(context) {
16
+ return {
17
+ Program(node) {
18
+ const metaEntries = extractMetadata(context.sourceCode)
19
+ if (!metaEntries.length) return
20
+ if (!context.filename.endsWith('.user.js'))
21
+ context.report({ node, messageId: 'requireUserJs' })
22
+ }
23
+ }
24
+ }
25
+ }