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

ota-meshi / eslint-plugin-jsonc / 11769804439

11 Nov 2024 12:22AM UTC coverage: 71.92%. Remained the same
11769804439

Pull #242

github

web-flow
Merge d05b801a6 into 3f8a1b90b
Pull Request #242: chore(deps): update dependency typescript to ~5.6.0

775 of 1276 branches covered (60.74%)

Branch coverage included in aggregate %.

1630 of 2068 relevant lines covered (78.82%)

120.32 hits per line

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

52.07
/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 type { Token } from "../types";
6
import { isCommaToken, isCommentToken } from "@eslint-community/eslint-utils";
1✔
7
import { isTokenOnSameLine } from "../utils/eslint-ast-utils";
1✔
8

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

25
    fixable: "whitespace",
26

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

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

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

100
      const option = providedOption || "always";
22✔
101

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

118
      return { consistent, multiline, minItems };
22✔
119
    }
120

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

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

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

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

149
      const value = normalizeOptionValue(options as BasicConfig);
22✔
150

151
      return { JSONArrayExpression: value, JSONArrayPattern: value };
22✔
152
    }
153

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

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

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

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

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

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

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

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

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

239
      if (!options) return;
22!
240

241
      let elementBreak = false;
22✔
242

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

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

267
        const previousElement = elements[i - 1];
60✔
268

269
        if (i === 0 || element === null || previousElement === null) continue;
60✔
270

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

280
        if (
38✔
281
          !isTokenOnSameLine(
282
            lastTokenOfPreviousElement,
283
            firstTokenOfCurrentElement,
284
          )
285
        )
286
          linebreaksCount++;
26✔
287
      }
288

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

296
      elements.forEach((element, i) => {
22✔
297
        const previousElement = elements[i - 1];
60✔
298

299
        if (i === 0 || element === null || previousElement === null) return;
60✔
300

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

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

331
    return {
36✔
332
      JSONArrayExpression: check,
333
    };
334
  },
335
});
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