• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ota-meshi / eslint-plugin-jsonc / 7681074173

27 Jan 2024 08:46PM UTC coverage: 72.066%. Remained the same
7681074173

push

github

renovate[bot]
chore(deps): update dependency esbuild to ^0.20.0

999 of 1590 branches covered (0.0%)

Branch coverage included in aggregate %.

1648 of 2083 relevant lines covered (79.12%)

57.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

52.94
/lib/rules/array-element-newline.ts
1
// Most source code was copied from ESLint v8.
2
// MIT License. Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
3
import type { AST } from "jsonc-eslint-parser";
4
import { createRule } from "../utils";
1✔
5
import { getSourceCode } from "eslint-compat-utils";
1✔
6
import type { Token } from "../types";
7
import { isCommaToken, isCommentToken } from "@eslint-community/eslint-utils";
1✔
8
import { isTokenOnSameLine } from "../utils/eslint-ast-utils";
1✔
9

10
type BasicConfig =
11
  | ("always" | "never" | "consistent")
12
  | {
13
      multiline?: boolean;
14
      minItems?: number | null;
15
    };
16
export default createRule("array-element-newline", {
1✔
17
  meta: {
18
    docs: {
19
      description: "enforce line breaks between array elements",
20
      recommended: null,
21
      extensionRule: true,
22
      layout: true,
23
    },
24
    type: "layout",
25

26
    fixable: "whitespace",
27

28
    schema: {
29
      definitions: {
30
        basicConfig: {
31
          oneOf: [
32
            {
33
              type: "string",
34
              enum: ["always", "never", "consistent"],
35
            },
36
            {
37
              type: "object",
38
              properties: {
39
                multiline: {
40
                  type: "boolean",
41
                },
42
                minItems: {
43
                  type: ["integer", "null"],
44
                  minimum: 0,
45
                },
46
              },
47
              additionalProperties: false,
48
            },
49
          ],
50
        },
51
      },
52
      type: "array",
53
      items: [
54
        {
55
          oneOf: [
56
            {
57
              $ref: "#/definitions/basicConfig",
58
            },
59
            {
60
              type: "object",
61
              properties: {
62
                ArrayExpression: {
63
                  $ref: "#/definitions/basicConfig",
64
                },
65
                JSONArrayExpression: {
66
                  $ref: "#/definitions/basicConfig",
67
                },
68
                ArrayPattern: {
69
                  $ref: "#/definitions/basicConfig",
70
                },
71
              },
72
              additionalProperties: false,
73
              minProperties: 1,
74
            },
75
          ],
76
        },
77
      ],
78
    },
79

80
    messages: {
81
      unexpectedLineBreak: "There should be no linebreak here.",
82
      missingLineBreak: "There should be a linebreak after this element.",
83
    },
84
  },
85
  create(context) {
86
    const sourceCode = getSourceCode(context);
16✔
87
    if (!sourceCode.parserServices.isJSON) {
16!
88
      return {};
×
89
    }
90

91
    /**
92
     * Normalizes a given option value.
93
     * @param providedOption An option value to parse.
94
     * @returns Normalized option object.
95
     */
96
    function normalizeOptionValue(providedOption: BasicConfig) {
97
      let consistent = false;
11✔
98
      let multiline = false;
11✔
99
      let minItems: number;
100

101
      const option = providedOption || "always";
11✔
102

103
      if (
11!
104
        !option ||
22!
105
        option === "always" ||
106
        (typeof option === "object" && option.minItems === 0)
107
      ) {
108
        minItems = 0;
11✔
109
      } else if (option === "never") {
×
110
        minItems = Number.POSITIVE_INFINITY;
×
111
      } else if (option === "consistent") {
×
112
        consistent = true;
×
113
        minItems = Number.POSITIVE_INFINITY;
×
114
      } else {
115
        multiline = Boolean(option.multiline);
×
116
        minItems = option.minItems || Number.POSITIVE_INFINITY;
×
117
      }
118

119
      return { consistent, multiline, minItems };
11✔
120
    }
121

122
    /**
123
     * Normalizes a given option value.
124
     * @param options An option value to parse.
125
     * @returns Normalized option object.
126
     */
127
    function normalizeOptions(options: any) {
128
      if (
11!
129
        options &&
11!
130
        (options.ArrayExpression ||
131
          options.JSONArrayExpression ||
132
          options.ArrayPattern)
133
      ) {
134
        let expressionOptions, patternOptions;
135

136
        if (options.ArrayExpression || options.JSONArrayExpression)
×
137
          expressionOptions = normalizeOptionValue(
×
138
            options.ArrayExpression || options.JSONArrayExpression,
×
139
          );
140

141
        if (options.ArrayPattern)
×
142
          patternOptions = normalizeOptionValue(options.ArrayPattern);
×
143

144
        return {
×
145
          JSONArrayExpression: expressionOptions,
146
          JSONArrayPattern: patternOptions,
147
        };
148
      }
149

150
      const value = normalizeOptionValue(options as BasicConfig);
11✔
151

152
      return { JSONArrayExpression: value, JSONArrayPattern: value };
11✔
153
    }
154

155
    /**
156
     * Reports that there shouldn't be a line break after the first token
157
     * @param token The token to use for the report.
158
     */
159
    function reportNoLineBreak(token: Token): void {
160
      const tokenBefore = sourceCode.getTokenBefore(token, {
×
161
        includeComments: true,
162
      })!;
163

164
      context.report({
×
165
        loc: {
166
          start: tokenBefore.loc!.end,
167
          end: token.loc.start,
168
        },
169
        messageId: "unexpectedLineBreak",
170
        fix(fixer) {
171
          if (isCommentToken(tokenBefore)) return null;
×
172

173
          if (!isTokenOnSameLine(tokenBefore, token))
×
174
            return fixer.replaceTextRange(
×
175
              [tokenBefore.range[1], token.range[0]],
176
              " ",
177
            );
178

179
          /**
180
           * This will check if the comma is on the same line as the next element
181
           * Following array:
182
           * [
183
           *     1
184
           *     , 2
185
           *     , 3
186
           * ]
187
           *
188
           * will be fixed to:
189
           * [
190
           *     1, 2, 3
191
           * ]
192
           */
193
          const twoTokensBefore = sourceCode.getTokenBefore(tokenBefore, {
×
194
            includeComments: true,
195
          })!;
196

197
          if (isCommentToken(twoTokensBefore)) return null;
×
198

199
          return fixer.replaceTextRange(
×
200
            [twoTokensBefore.range[1], tokenBefore.range[0]],
201
            "",
202
          );
203
        },
204
      });
205
    }
206

207
    /**
208
     * Reports that there should be a line break after the first token
209
     * @param token The token to use for the report.
210
     */
211
    function reportRequiredLineBreak(token: Token): void {
212
      const tokenBefore = sourceCode.getTokenBefore(token, {
8✔
213
        includeComments: true,
214
      })!;
215

216
      context.report({
8✔
217
        loc: {
218
          start: tokenBefore.loc!.end,
219
          end: token.loc.start,
220
        },
221
        messageId: "missingLineBreak",
222
        fix(fixer) {
223
          return fixer.replaceTextRange(
8✔
224
            [tokenBefore.range![1], token.range[0]],
225
            "\n",
226
          );
227
        },
228
      });
229
    }
230

231
    /**
232
     * Reports a given node if it violated this rule.
233
     * @param node A node to check. This is an ObjectExpression node or an ObjectPattern node.
234
     */
235
    function check(node: AST.JSONArrayExpression): void {
236
      const elements = node.elements;
11✔
237
      const normalizedOptions = normalizeOptions(context.options[0]);
11✔
238
      const options = normalizedOptions[node.type];
11✔
239

240
      if (!options) return;
11!
241

242
      let elementBreak = false;
11✔
243

244
      /**
245
       * MULTILINE: true
246
       * loop through every element and check
247
       * if at least one element has linebreaks inside
248
       * this ensures that following is not valid (due to elements are on the same line):
249
       *
250
       * [
251
       *      1,
252
       *      2,
253
       *      3
254
       * ]
255
       */
256
      if (options.multiline) {
11!
257
        elementBreak = elements
×
258
          .filter((element: any) => element !== null)
×
259
          .some(
260
            (element: any) => element.loc.start.line !== element.loc.end.line,
×
261
          );
262
      }
263

264
      let linebreaksCount = 0;
11✔
265
      for (let i = 0; i < node.elements.length; i++) {
11✔
266
        const element = node.elements[i];
31✔
267

268
        const previousElement = elements[i - 1];
31✔
269

270
        if (i === 0 || element === null || previousElement === null) continue;
31✔
271

272
        const commaToken = sourceCode.getFirstTokenBetween(
20✔
273
          previousElement as any,
274
          element as any,
275
          isCommaToken,
276
        )!;
277
        const lastTokenOfPreviousElement =
278
          sourceCode.getTokenBefore(commaToken);
20✔
279
        const firstTokenOfCurrentElement = sourceCode.getTokenAfter(commaToken);
20✔
280

281
        if (
20✔
282
          !isTokenOnSameLine(
283
            lastTokenOfPreviousElement,
284
            firstTokenOfCurrentElement,
285
          )
286
        )
287
          linebreaksCount++;
12✔
288
      }
289

290
      const needsLinebreaks =
291
        elements.length >= options.minItems ||
11!
292
        (options.multiline && elementBreak) ||
293
        (options.consistent &&
294
          linebreaksCount > 0 &&
295
          linebreaksCount < node.elements.length);
296

297
      elements.forEach((element, i) => {
11✔
298
        const previousElement = elements[i - 1];
31✔
299

300
        if (i === 0 || element === null || previousElement === null) return;
31✔
301

302
        const commaToken = sourceCode.getFirstTokenBetween(
20✔
303
          previousElement as any,
304
          element as any,
305
          isCommaToken,
306
        )!;
307
        const lastTokenOfPreviousElement =
308
          sourceCode.getTokenBefore(commaToken);
20✔
309
        const firstTokenOfCurrentElement =
310
          sourceCode.getTokenAfter(commaToken)!;
20✔
311

312
        if (needsLinebreaks) {
20!
313
          if (
20✔
314
            isTokenOnSameLine(
315
              lastTokenOfPreviousElement,
316
              firstTokenOfCurrentElement,
317
            )
318
          )
319
            reportRequiredLineBreak(firstTokenOfCurrentElement);
8✔
320
        } else {
321
          if (
×
322
            !isTokenOnSameLine(
323
              lastTokenOfPreviousElement,
324
              firstTokenOfCurrentElement,
325
            )
326
          )
327
            reportNoLineBreak(firstTokenOfCurrentElement);
×
328
        }
329
      });
330
    }
331

332
    return {
16✔
333
      JSONArrayExpression: check,
334
    };
335
  },
336
});
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc