]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Rule to check empty newline between class members | |
3 | * @author 薛定谔的猫<hh_2013@foxmail.com> | |
4 | */ | |
5 | "use strict"; | |
6 | ||
609c276f TL |
7 | //------------------------------------------------------------------------------ |
8 | // Requirements | |
9 | //------------------------------------------------------------------------------ | |
10 | ||
eb39fafa DC |
11 | const astUtils = require("./utils/ast-utils"); |
12 | ||
13 | //------------------------------------------------------------------------------ | |
14 | // Rule Definition | |
15 | //------------------------------------------------------------------------------ | |
16 | ||
17 | module.exports = { | |
18 | meta: { | |
19 | type: "layout", | |
20 | ||
21 | docs: { | |
22 | description: "require or disallow an empty line between class members", | |
eb39fafa DC |
23 | recommended: false, |
24 | url: "https://eslint.org/docs/rules/lines-between-class-members" | |
25 | }, | |
26 | ||
27 | fixable: "whitespace", | |
28 | ||
29 | schema: [ | |
30 | { | |
31 | enum: ["always", "never"] | |
32 | }, | |
33 | { | |
34 | type: "object", | |
35 | properties: { | |
36 | exceptAfterSingleLine: { | |
37 | type: "boolean", | |
38 | default: false | |
39 | } | |
40 | }, | |
41 | additionalProperties: false | |
42 | } | |
43 | ], | |
44 | messages: { | |
45 | never: "Unexpected blank line between class members.", | |
46 | always: "Expected blank line between class members." | |
47 | } | |
48 | }, | |
49 | ||
50 | create(context) { | |
51 | ||
52 | const options = []; | |
53 | ||
54 | options[0] = context.options[0] || "always"; | |
55 | options[1] = context.options[1] || { exceptAfterSingleLine: false }; | |
56 | ||
57 | const sourceCode = context.getSourceCode(); | |
58 | ||
609c276f TL |
59 | /** |
60 | * Gets a pair of tokens that should be used to check lines between two class member nodes. | |
61 | * | |
62 | * In most cases, this returns the very last token of the current node and | |
63 | * the very first token of the next node. | |
64 | * For example: | |
65 | * | |
66 | * class C { | |
67 | * x = 1; // curLast: `;` nextFirst: `in` | |
68 | * in = 2 | |
69 | * } | |
70 | * | |
71 | * There is only one exception. If the given node ends with a semicolon, and it looks like | |
72 | * a semicolon-less style's semicolon - one that is not on the same line as the preceding | |
73 | * token, but is on the line where the next class member starts - this returns the preceding | |
74 | * token and the semicolon as boundary tokens. | |
75 | * For example: | |
76 | * | |
77 | * class C { | |
78 | * x = 1 // curLast: `1` nextFirst: `;` | |
79 | * ;in = 2 | |
80 | * } | |
81 | * When determining the desired layout of the code, we should treat this semicolon as | |
82 | * a part of the next class member node instead of the one it technically belongs to. | |
83 | * @param {ASTNode} curNode Current class member node. | |
84 | * @param {ASTNode} nextNode Next class member node. | |
85 | * @returns {Token} The actual last token of `node`. | |
86 | * @private | |
87 | */ | |
88 | function getBoundaryTokens(curNode, nextNode) { | |
89 | const lastToken = sourceCode.getLastToken(curNode); | |
90 | const prevToken = sourceCode.getTokenBefore(lastToken); | |
91 | const nextToken = sourceCode.getFirstToken(nextNode); // skip possible lone `;` between nodes | |
92 | ||
93 | const isSemicolonLessStyle = ( | |
94 | astUtils.isSemicolonToken(lastToken) && | |
95 | !astUtils.isTokenOnSameLine(prevToken, lastToken) && | |
96 | astUtils.isTokenOnSameLine(lastToken, nextToken) | |
97 | ); | |
98 | ||
99 | return isSemicolonLessStyle | |
100 | ? { curLast: prevToken, nextFirst: lastToken } | |
101 | : { curLast: lastToken, nextFirst: nextToken }; | |
102 | } | |
103 | ||
eb39fafa DC |
104 | /** |
105 | * Return the last token among the consecutive tokens that have no exceed max line difference in between, before the first token in the next member. | |
106 | * @param {Token} prevLastToken The last token in the previous member node. | |
107 | * @param {Token} nextFirstToken The first token in the next member node. | |
108 | * @param {number} maxLine The maximum number of allowed line difference between consecutive tokens. | |
109 | * @returns {Token} The last token among the consecutive tokens. | |
110 | */ | |
111 | function findLastConsecutiveTokenAfter(prevLastToken, nextFirstToken, maxLine) { | |
112 | const after = sourceCode.getTokenAfter(prevLastToken, { includeComments: true }); | |
113 | ||
114 | if (after !== nextFirstToken && after.loc.start.line - prevLastToken.loc.end.line <= maxLine) { | |
115 | return findLastConsecutiveTokenAfter(after, nextFirstToken, maxLine); | |
116 | } | |
117 | return prevLastToken; | |
118 | } | |
119 | ||
120 | /** | |
121 | * Return the first token among the consecutive tokens that have no exceed max line difference in between, after the last token in the previous member. | |
122 | * @param {Token} nextFirstToken The first token in the next member node. | |
123 | * @param {Token} prevLastToken The last token in the previous member node. | |
124 | * @param {number} maxLine The maximum number of allowed line difference between consecutive tokens. | |
125 | * @returns {Token} The first token among the consecutive tokens. | |
126 | */ | |
127 | function findFirstConsecutiveTokenBefore(nextFirstToken, prevLastToken, maxLine) { | |
128 | const before = sourceCode.getTokenBefore(nextFirstToken, { includeComments: true }); | |
129 | ||
130 | if (before !== prevLastToken && nextFirstToken.loc.start.line - before.loc.end.line <= maxLine) { | |
131 | return findFirstConsecutiveTokenBefore(before, prevLastToken, maxLine); | |
132 | } | |
133 | return nextFirstToken; | |
134 | } | |
135 | ||
136 | /** | |
137 | * Checks if there is a token or comment between two tokens. | |
138 | * @param {Token} before The token before. | |
139 | * @param {Token} after The token after. | |
140 | * @returns {boolean} True if there is a token or comment between two tokens. | |
141 | */ | |
142 | function hasTokenOrCommentBetween(before, after) { | |
143 | return sourceCode.getTokensBetween(before, after, { includeComments: true }).length !== 0; | |
144 | } | |
145 | ||
146 | return { | |
147 | ClassBody(node) { | |
148 | const body = node.body; | |
149 | ||
150 | for (let i = 0; i < body.length - 1; i++) { | |
151 | const curFirst = sourceCode.getFirstToken(body[i]); | |
609c276f | 152 | const { curLast, nextFirst } = getBoundaryTokens(body[i], body[i + 1]); |
eb39fafa DC |
153 | const isMulti = !astUtils.isTokenOnSameLine(curFirst, curLast); |
154 | const skip = !isMulti && options[1].exceptAfterSingleLine; | |
155 | const beforePadding = findLastConsecutiveTokenAfter(curLast, nextFirst, 1); | |
156 | const afterPadding = findFirstConsecutiveTokenBefore(nextFirst, curLast, 1); | |
157 | const isPadded = afterPadding.loc.start.line - beforePadding.loc.end.line > 1; | |
158 | const hasTokenInPadding = hasTokenOrCommentBetween(beforePadding, afterPadding); | |
159 | const curLineLastToken = findLastConsecutiveTokenAfter(curLast, nextFirst, 0); | |
160 | ||
161 | if ((options[0] === "always" && !skip && !isPadded) || | |
162 | (options[0] === "never" && isPadded)) { | |
163 | context.report({ | |
164 | node: body[i + 1], | |
165 | messageId: isPadded ? "never" : "always", | |
166 | fix(fixer) { | |
167 | if (hasTokenInPadding) { | |
168 | return null; | |
169 | } | |
170 | return isPadded | |
171 | ? fixer.replaceTextRange([beforePadding.range[1], afterPadding.range[0]], "\n") | |
172 | : fixer.insertTextAfter(curLineLastToken, "\n"); | |
173 | } | |
174 | }); | |
175 | } | |
176 | } | |
177 | } | |
178 | }; | |
179 | } | |
180 | }; |