eslint-soml 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/index.d.ts +33 -0
  2. package/index.js +47 -0
  3. package/language/soml-language.js +72 -0
  4. package/language/soml-source-code.js +277 -0
  5. package/license +9 -0
  6. package/package.json +83 -0
  7. package/readme.md +175 -0
  8. package/rules/format.js +134 -0
  9. package/rules/index.js +17 -0
  10. package/rules/key-name-casing.js +94 -0
  11. package/rules/no-block-string-trailing-whitespace.js +64 -0
  12. package/rules/no-empty-keys.js +43 -0
  13. package/rules/no-invisible-characters.js +90 -0
  14. package/rules/no-loss-of-precision.js +96 -0
  15. package/rules/no-mixed-number-types.js +81 -0
  16. package/rules/no-numeric-dotted-keys.js +81 -0
  17. package/rules/no-unnormalized-keys.js +62 -0
  18. package/rules/numeric-separators-style.js +136 -0
  19. package/rules/prefer-bare-keys.js +63 -0
  20. package/rules/prefer-literal-strings.js +85 -0
  21. package/rules/prefer-utc-instants.js +53 -0
  22. package/rules/rule/index.js +7 -0
  23. package/rules/rule/soml-context.js +47 -0
  24. package/rules/rule/soml-listeners.js +63 -0
  25. package/rules/rule/to-eslint-create.js +34 -0
  26. package/rules/rule/to-eslint-listener.js +42 -0
  27. package/rules/rule/to-eslint-problem.js +53 -0
  28. package/rules/rule/to-eslint-rule-fixer.js +56 -0
  29. package/rules/rule/to-eslint-rule.js +36 -0
  30. package/rules/rule/to-eslint-rules.js +9 -0
  31. package/rules/rule/utilities.js +29 -0
  32. package/rules/sort-keys.js +187 -0
  33. package/rules/spaced-comment.js +83 -0
  34. package/rules/utils/escape-string.js +30 -0
  35. package/rules/utils/get-documentation-url.js +15 -0
  36. package/rules/utils/index.js +2 -0
  37. package/rules/utils/invisible-characters.js +26 -0
@@ -0,0 +1,62 @@
1
+ /**
2
+ @import {SomlContext} from './rule/soml-context.js';
3
+ @import {SomlRule} from './rule/to-eslint-rule.js';
4
+ */
5
+
6
+ const MESSAGE_ID = 'no-unnormalized-keys';
7
+ const messages = {
8
+ [MESSAGE_ID]: 'The key `{{key}}` is not in {{form}}, so it can look the same as a different key.',
9
+ };
10
+
11
+ /**
12
+ @param {SomlContext} context
13
+ */
14
+ const create = context => {
15
+ const [{form}] = context.options;
16
+
17
+ context.on('KeySegment', node => {
18
+ if (node.value.normalize(form) === node.value) {
19
+ return;
20
+ }
21
+
22
+ return {
23
+ node,
24
+ messageId: MESSAGE_ID,
25
+ data: {
26
+ key: node.value,
27
+ form,
28
+ },
29
+ };
30
+ });
31
+ };
32
+
33
+ /**
34
+ @type {SomlRule}
35
+ */
36
+ const config = {
37
+ create,
38
+ meta: {
39
+ type: 'problem',
40
+ docs: {
41
+ description: 'Disallow keys that are not in a Unicode normalization form.',
42
+ recommended: 'unopinionated',
43
+ },
44
+ schema: [
45
+ {
46
+ type: 'object',
47
+ additionalProperties: false,
48
+ properties: {
49
+ form: {
50
+ enum: ['NFC', 'NFD', 'NFKC', 'NFKD'],
51
+ description: 'The Unicode normalization form keys must be in.',
52
+ },
53
+ },
54
+ },
55
+ ],
56
+ defaultOptions: [{form: 'NFC'}],
57
+ messages,
58
+ languages: ['soml/soml'],
59
+ },
60
+ };
61
+
62
+ export default config;
@@ -0,0 +1,136 @@
1
+ /**
2
+ @import {SomlContext} from './rule/soml-context.js';
3
+ @import {SomlRule} from './rule/to-eslint-rule.js';
4
+ */
5
+
6
+ const MESSAGE_ID = 'numeric-separators-style';
7
+ const messages = {
8
+ [MESSAGE_ID]: 'Group the digits as `{{formatted}}`.',
9
+ };
10
+
11
+ const DECIMAL_NUMBER = /^(?<sign>-?)(?<integer>[\d_]+)(?:\.(?<fraction>[\d_]+))?(?:e(?<exponentSign>-?)(?<exponent>[\d_]+))?$/v;
12
+
13
+ /*
14
+ Groups `digits` with `_`, counting from the right, or from the left for a fraction. Fewer than `minimumDigits` digits, or no `groupLength`, means no grouping.
15
+ */
16
+ function group(digits, {minimumDigits, groupLength, isFromLeft = false}) {
17
+ if (groupLength === undefined || digits.length < minimumDigits) {
18
+ return digits;
19
+ }
20
+
21
+ const groups = [];
22
+
23
+ if (isFromLeft) {
24
+ for (let start = 0; start < digits.length; start += groupLength) {
25
+ groups.push(digits.slice(start, start + groupLength));
26
+ }
27
+ } else {
28
+ for (let end = digits.length; end > 0; end -= groupLength) {
29
+ groups.unshift(digits.slice(Math.max(end - groupLength, 0), end));
30
+ }
31
+ }
32
+
33
+ return groups.join('_');
34
+ }
35
+
36
+ function format(text, options) {
37
+ const {sign, integer, fraction, exponentSign, exponent} = DECIMAL_NUMBER.exec(text).groups;
38
+ const strip = digits => digits.replaceAll('_', '');
39
+ let formatted = sign + group(strip(integer), options);
40
+
41
+ if (fraction !== undefined) {
42
+ formatted += `.${group(strip(fraction), {...options, groupLength: options.fractionGroupLength, isFromLeft: true})}`;
43
+ }
44
+
45
+ if (exponent !== undefined) {
46
+ formatted += `e${exponentSign}${group(strip(exponent), options)}`;
47
+ }
48
+
49
+ return formatted;
50
+ }
51
+
52
+ /**
53
+ @param {SomlContext} context
54
+ */
55
+ const create = context => {
56
+ const [options] = context.options;
57
+
58
+ context.on(['Integer', 'Float'], node => {
59
+ // A hexadecimal, octal, or binary int is grouped by what its digits mean, such as bytes or bits, so it is left alone.
60
+ if (node.type === 'Integer' ? node.radix !== 10 : !Number.isFinite(node.value)) {
61
+ return;
62
+ }
63
+
64
+ const text = context.sourceCode.getText(node);
65
+
66
+ if (options.onlyIfContainsSeparator && !text.includes('_')) {
67
+ return;
68
+ }
69
+
70
+ const formatted = format(text, options);
71
+
72
+ if (formatted === text) {
73
+ return;
74
+ }
75
+
76
+ return {
77
+ node,
78
+ messageId: MESSAGE_ID,
79
+ data: {formatted},
80
+ fix: fixer => fixer.replaceText(node, formatted),
81
+ };
82
+ });
83
+ };
84
+
85
+ /**
86
+ @type {SomlRule}
87
+ */
88
+ const config = {
89
+ create,
90
+ meta: {
91
+ type: 'suggestion',
92
+ docs: {
93
+ description: 'Enforce the grouping of digits with `_` in decimal numbers.',
94
+ recommended: true,
95
+ },
96
+ fixable: 'code',
97
+ schema: [
98
+ {
99
+ type: 'object',
100
+ additionalProperties: false,
101
+ properties: {
102
+ minimumDigits: {
103
+ type: 'integer',
104
+ minimum: 0,
105
+ description: 'The number of digits from which separators are required.',
106
+ },
107
+ groupLength: {
108
+ type: 'integer',
109
+ minimum: 1,
110
+ description: 'The number of digits in each group.',
111
+ },
112
+ fractionGroupLength: {
113
+ type: 'integer',
114
+ minimum: 1,
115
+ description: 'The number of digits in each group after the decimal point. By default, the digits after the decimal point are not grouped.',
116
+ },
117
+ onlyIfContainsSeparator: {
118
+ type: 'boolean',
119
+ description: 'Whether to only check numbers that already contain a separator.',
120
+ },
121
+ },
122
+ },
123
+ ],
124
+ defaultOptions: [
125
+ {
126
+ minimumDigits: 5,
127
+ groupLength: 3,
128
+ onlyIfContainsSeparator: false,
129
+ },
130
+ ],
131
+ messages,
132
+ languages: ['soml/soml'],
133
+ },
134
+ };
135
+
136
+ export default config;
@@ -0,0 +1,63 @@
1
+ /**
2
+ @import {SomlContext} from './rule/soml-context.js';
3
+ @import {SomlRule} from './rule/to-eslint-rule.js';
4
+ */
5
+
6
+ const MESSAGE_ID = 'prefer-bare-keys';
7
+ const messages = {
8
+ [MESSAGE_ID]: 'The key `{{key}}` does not need quotes.',
9
+ };
10
+
11
+ /*
12
+ A bare key is ASCII letters, digits, `_`, and `-`.
13
+ */
14
+ const BARE_KEY = /^[\w\-]+$/v;
15
+
16
+ const isDigit = character => character >= '0' && character <= '9';
17
+
18
+ /**
19
+ @param {SomlContext} context
20
+ */
21
+ const create = context => {
22
+ context.on('Key', function * (key) {
23
+ for (const [index, node] of key.segments.entries()) {
24
+ if (node.style === 'bare' || !BARE_KEY.test(node.value)) {
25
+ continue;
26
+ }
27
+
28
+ // Without quotes, a digit on both sides of a `.` reads as a number, as in `1.5`, which `no-numeric-dotted-keys` reports.
29
+ const previous = key.segments[index - 1]?.value ?? '';
30
+ const next = key.segments[index + 1]?.value ?? '';
31
+
32
+ if ((isDigit(previous.at(-1)) && isDigit(node.value[0])) || (isDigit(node.value.at(-1)) && isDigit(next[0]))) {
33
+ continue;
34
+ }
35
+
36
+ yield {
37
+ node,
38
+ messageId: MESSAGE_ID,
39
+ data: {key: node.value},
40
+ fix: fixer => fixer.replaceText(node, node.value),
41
+ };
42
+ }
43
+ });
44
+ };
45
+
46
+ /**
47
+ @type {SomlRule}
48
+ */
49
+ const config = {
50
+ create,
51
+ meta: {
52
+ type: 'suggestion',
53
+ docs: {
54
+ description: 'Prefer bare keys when a key does not need quotes.',
55
+ recommended: true,
56
+ },
57
+ fixable: 'code',
58
+ messages,
59
+ languages: ['soml/soml'],
60
+ },
61
+ };
62
+
63
+ export default config;
@@ -0,0 +1,85 @@
1
+ import {hasInvisibleCharacter} from './utils/index.js';
2
+
3
+ /**
4
+ @import {SomlContext} from './rule/soml-context.js';
5
+ @import {SomlRule} from './rule/to-eslint-rule.js';
6
+ @import {StringNode, KeySegmentNode} from 'soml-lang';
7
+ */
8
+
9
+ const MESSAGE_ID = 'prefer-literal-strings';
10
+ const messages = {
11
+ [MESSAGE_ID]: 'Prefer a literal string. A `"…"` string is only needed for escapes.',
12
+ };
13
+
14
+ /*
15
+ What a `'...'` string cannot hold: `'`, which would end it, and a line feed. A tab and every other control character stay escaped too, because they are invisible. That is also how canonical form decides.
16
+ */
17
+ const NOT_LITERAL = /[\p{Control}']/v;
18
+
19
+ /**
20
+ The literal spelling of a string or key with the same value, or `undefined` when there is none.
21
+
22
+ @param {StringNode | KeySegmentNode} node
23
+ @param {string} text The source text of the node.
24
+ @returns {string | undefined}
25
+ */
26
+ function getLiteral(node, text) {
27
+ // A block string keeps its layout, so it only changes when its text already means the same without escapes. With no `'` in it, no line can close the new delimiter early.
28
+ if (node.block) {
29
+ if (text.includes('\\') || text.includes('\'') || hasInvisibleCharacter(text)) {
30
+ return;
31
+ }
32
+
33
+ const delimiter = '\''.repeat(text.indexOf('\n'));
34
+ return delimiter + text.slice(delimiter.length, -delimiter.length) + delimiter;
35
+ }
36
+
37
+ // Invisible characters stay escaped, as `no-invisible-characters` asks for.
38
+ if (NOT_LITERAL.test(node.value) || hasInvisibleCharacter(node.value)) {
39
+ return;
40
+ }
41
+
42
+ return `'${node.value}'`;
43
+ }
44
+
45
+ /**
46
+ @param {SomlContext} context
47
+ */
48
+ const create = context => {
49
+ context.on(['String', 'KeySegment'], node => {
50
+ if (node.style !== 'escaped') {
51
+ return;
52
+ }
53
+
54
+ const literal = getLiteral(node, context.sourceCode.getText(node));
55
+
56
+ if (literal === undefined) {
57
+ return;
58
+ }
59
+
60
+ return {
61
+ node,
62
+ messageId: MESSAGE_ID,
63
+ fix: fixer => fixer.replaceText(node, literal),
64
+ };
65
+ });
66
+ };
67
+
68
+ /**
69
+ @type {SomlRule}
70
+ */
71
+ const config = {
72
+ create,
73
+ meta: {
74
+ type: 'suggestion',
75
+ docs: {
76
+ description: 'Prefer literal strings, `\'…\'`, when a string has no escapes.',
77
+ recommended: true,
78
+ },
79
+ fixable: 'code',
80
+ messages,
81
+ languages: ['soml/soml'],
82
+ },
83
+ };
84
+
85
+ export default config;
@@ -0,0 +1,53 @@
1
+ /**
2
+ @import {SomlContext} from './rule/soml-context.js';
3
+ @import {SomlRule} from './rule/to-eslint-rule.js';
4
+ */
5
+
6
+ const MESSAGE_ID = 'prefer-utc-instants';
7
+ const messages = {
8
+ [MESSAGE_ID]: 'Write the instant in UTC, as `{{utc}}`.',
9
+ };
10
+
11
+ /**
12
+ @param {SomlContext} context
13
+ */
14
+ const create = context => {
15
+ context.on('Instant', node => {
16
+ const text = context.sourceCode.getText(node);
17
+
18
+ if (text.endsWith('Z')) {
19
+ return;
20
+ }
21
+
22
+ // Keep as many fractional digits as were written, so `.500` stays `.500`.
23
+ const fractionalSecondDigits = /\.(?<fraction>\d+)/v.exec(text)?.groups.fraction.length ?? 0;
24
+ const utc = node.value.toString({fractionalSecondDigits});
25
+
26
+ return {
27
+ node,
28
+ messageId: MESSAGE_ID,
29
+ data: {utc},
30
+ // The offset is not part of the value, so this keeps it.
31
+ fix: fixer => fixer.replaceText(node, utc),
32
+ };
33
+ });
34
+ };
35
+
36
+ /**
37
+ @type {SomlRule}
38
+ */
39
+ const config = {
40
+ create,
41
+ meta: {
42
+ type: 'suggestion',
43
+ docs: {
44
+ description: 'Prefer instants in UTC.',
45
+ recommended: false,
46
+ },
47
+ fixable: 'code',
48
+ messages,
49
+ languages: ['soml/soml'],
50
+ },
51
+ };
52
+
53
+ export default config;
@@ -0,0 +1,7 @@
1
+ /**
2
+ @import {SomlCreate} from './to-eslint-create.js';
3
+ @import {SomlRule} from './to-eslint-rule.js';
4
+ @import {SomlContext} from './soml-context.js';
5
+ */
6
+
7
+ export {default as toEslintRules} from './to-eslint-rules.js';
@@ -0,0 +1,47 @@
1
+ /**
2
+ @import {RuleContext} from '@eslint/core';
3
+ @import {Node} from 'soml-lang';
4
+ @import SomlListeners from './soml-listeners.js';
5
+ @import {SomlProblems} from './to-eslint-problem.js';
6
+ */
7
+
8
+ /**
9
+ @typedef {RuleContext} SomlRuleContext
10
+ */
11
+
12
+ /**
13
+ @template {Node['type']} [NodeType=Node['type']]
14
+ @typedef {(node: Extract<Node, {type: NodeType}>, parent: Node | undefined) => SomlProblems} SomlRuleListener
15
+ */
16
+
17
+ /**
18
+ @typedef {<NodeType extends Node['type']>(type: NodeType | NodeType[], listener: SomlRuleListener<NodeType>) => void} SomlRuleListen
19
+ @typedef {SomlRuleContext & {
20
+ on: SomlRuleListen
21
+ onExit: SomlRuleListen
22
+ }} SomlContext
23
+ */
24
+
25
+ /**
26
+ Create a better `Context` object with `on` and `onExit` method to add listeners
27
+
28
+ @param {SomlRuleContext} eslintContext
29
+ @param {SomlListeners} listeners
30
+ @returns {SomlContext}
31
+ */
32
+ export default function createSomlContext(eslintContext, listeners) {
33
+ /**
34
+ @type {SomlContext}
35
+ */
36
+ const context = new Proxy(/** @type {SomlContext} */ (eslintContext), {
37
+ get(target, property, receiver) {
38
+ if (property === 'on' || property === 'onExit') {
39
+ return listeners[property].bind(listeners);
40
+ }
41
+
42
+ return Reflect.get(target, property, receiver);
43
+ },
44
+ });
45
+
46
+ return context;
47
+ }
@@ -0,0 +1,63 @@
1
+ import toEslintListener from './to-eslint-listener.js';
2
+
3
+ /**
4
+ @import {RuleVisitor} from '@eslint/core';
5
+ @import {Node} from 'soml-lang';
6
+ @import {SomlRuleContext, SomlRuleListener} from './soml-context.js';
7
+ */
8
+
9
+ export default class SomlListeners {
10
+ #context;
11
+ #listeners = new Map();
12
+
13
+ /**
14
+ @param {SomlRuleContext} context
15
+ */
16
+ constructor(context) {
17
+ this.#context = context;
18
+ }
19
+
20
+ #addEventListener(selectors, listener) {
21
+ const listeners = this.#listeners;
22
+ for (const selector of selectors) {
23
+ if (listeners.has(selector)) {
24
+ listeners.get(selector).push(listener);
25
+ } else {
26
+ listeners.set(selector, [listener]);
27
+ }
28
+ }
29
+ }
30
+
31
+ /**
32
+ @template {Node['type']} NodeType
33
+ @param {NodeType | NodeType[]} selectorOrSelectors
34
+ @param {SomlRuleListener<NodeType>} listener
35
+ */
36
+ on(selectorOrSelectors, listener) {
37
+ const selectors = Array.isArray(selectorOrSelectors) ? selectorOrSelectors : [selectorOrSelectors];
38
+ this.#addEventListener(selectors, listener);
39
+ }
40
+
41
+ /**
42
+ @template {Node['type']} NodeType
43
+ @param {NodeType | NodeType[]} selectorOrSelectors
44
+ @param {SomlRuleListener<NodeType>} listener
45
+ */
46
+ onExit(selectorOrSelectors, listener) {
47
+ const selectors = Array.isArray(selectorOrSelectors) ? selectorOrSelectors : [selectorOrSelectors];
48
+ this.#addEventListener(selectors.map(selector => `${selector}:exit`), listener);
49
+ }
50
+
51
+ /**
52
+ @returns {RuleVisitor}
53
+ */
54
+ toEslintListeners() {
55
+ const eslintListeners = {};
56
+
57
+ for (const [selector, listeners] of this.#listeners) {
58
+ eslintListeners[selector] = toEslintListener(this.#context, listeners);
59
+ }
60
+
61
+ return eslintListeners;
62
+ }
63
+ }
@@ -0,0 +1,34 @@
1
+ import assert from 'node:assert/strict';
2
+ import createSomlContext from './soml-context.js';
3
+ import SomlListeners from './soml-listeners.js';
4
+
5
+ /**
6
+ @import {RuleDefinition} from '@eslint/core';
7
+ @import {SomlContext} from './soml-context.js';
8
+ */
9
+
10
+ /**
11
+ @typedef {RuleDefinition['create']} EslintCreate
12
+ @typedef {(context: SomlContext) => void} SomlCreate
13
+ */
14
+
15
+ /**
16
+ Convert Soml style of `create` to ESLint style
17
+
18
+ @param {SomlCreate} somlCreate
19
+ @returns {EslintCreate}
20
+ */
21
+ export default function toEslintCreate(somlCreate) {
22
+ return eslintContext => {
23
+ const somlListeners = new SomlListeners(eslintContext);
24
+ const somlContext = createSomlContext(eslintContext, somlListeners);
25
+
26
+ const result = somlCreate(somlContext);
27
+
28
+ assert.equal(result, undefined, `[${eslintContext.id}] Rule \`create\` function should return \`undefined\`, please use \`context.on()\` instead of return listeners.`);
29
+
30
+ const eslintListeners = somlListeners.toEslintListeners();
31
+
32
+ return eslintListeners;
33
+ };
34
+ }
@@ -0,0 +1,42 @@
1
+ import {forEachFixOrProblem} from './utilities.js';
2
+ import toEslintProblem from './to-eslint-problem.js';
3
+
4
+ /**
5
+ @import {RuleVisitor} from '@eslint/core';
6
+ @import {Node} from 'soml-lang';
7
+ @import {SomlRuleContext, SomlRuleListener} from './soml-context.js';
8
+ */
9
+
10
+ /**
11
+ @typedef {(node: Node, parent: Node | undefined) => void} EslintListener
12
+ */
13
+
14
+ /**
15
+ @param {SomlRuleContext} context
16
+ @param {SomlRuleListener[]} listeners
17
+ @returns {EslintListener}
18
+ */
19
+ export default function toEslintListener(context, listeners) {
20
+ const reportProblem = somlProblem => {
21
+ if (somlProblem) {
22
+ context.report(toEslintProblem(somlProblem));
23
+ }
24
+ };
25
+
26
+ /*
27
+ Declared with fixed arity rather than rest arguments, since this runs for every node visit of every rule and a per-call rest array is measurable.
28
+ Listeners receive the node and its parent.
29
+ */
30
+ return (node, parent) => {
31
+ for (const listener of listeners) {
32
+ const somlProblems = listener(node, parent);
33
+
34
+ // Listeners report nothing on the vast majority of nodes, so keep that path free of iterator allocation.
35
+ if (!somlProblems) {
36
+ continue;
37
+ }
38
+
39
+ forEachFixOrProblem(somlProblems, reportProblem);
40
+ }
41
+ };
42
+ }
@@ -0,0 +1,53 @@
1
+ import toEslintFixer from './to-eslint-rule-fixer.js';
2
+
3
+ /**
4
+ @import {SomlRuleContext} from './soml-context.js';
5
+ @import {SomlRuleFixer} from './to-eslint-rule-fixer.js';
6
+ */
7
+
8
+ /**
9
+ @typedef {Parameters<SomlRuleContext['report']>[0]} EslintProblem
10
+ */
11
+
12
+ /**
13
+ @template {NonNullable<EslintProblem['suggest']>[number]} [Suggestion=NonNullable<EslintProblem['suggest']>[number]]
14
+ @typedef {Suggestion extends unknown ? Omit<Suggestion, 'fix'> & {fix: SomlRuleFixer} : never} SomlSuggestion
15
+ */
16
+
17
+ /**
18
+ @template {EslintProblem} [Problem=EslintProblem]
19
+ @typedef {Problem extends unknown ? Omit<Problem, 'fix' | 'suggest'> & {fix?: SomlRuleFixer, suggest?: SomlSuggestion[]} : never} SomlProblem
20
+ */
21
+
22
+ /**
23
+ @typedef {SomlProblem | void | Iterable<SomlProblem | void>} SomlProblems
24
+ */
25
+
26
+ /**
27
+ @param {SomlProblem} somlProblem
28
+ @returns {EslintProblem}
29
+ */
30
+ export default function toEslintProblem(somlProblem) {
31
+ const {fix, suggest, ...problem} = somlProblem;
32
+ /**
33
+ @type {EslintProblem}
34
+ */
35
+ const eslintProblem = problem;
36
+
37
+ if (fix) {
38
+ eslintProblem.fix = toEslintFixer(fix);
39
+ }
40
+
41
+ if (Array.isArray(suggest)) {
42
+ eslintProblem.suggest = suggest.map(somlSuggest => ({
43
+ ...somlSuggest,
44
+ fix: toEslintFixer(somlSuggest.fix),
45
+ data: {
46
+ ...somlProblem.data,
47
+ ...somlSuggest.data,
48
+ },
49
+ }));
50
+ }
51
+
52
+ return eslintProblem;
53
+ }