eslint-plugin-lube 0.6.0 → 0.7.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/CHANGELOG.md +23 -0
- package/README.md +4 -1
- package/docs/prefer-type-tag.md +45 -0
- package/package.json +1 -1
- package/src/index.js +11 -2
- package/src/rules/ascii-order.js +163 -71
- package/src/rules/prefer-type-tag.js +165 -0
- package/src/rules/pretty-imports.js +24 -12
- package/src/rules/pretty-jsdoc-casting.js +76 -29
- package/src/rules/pretty-sequence.js +110 -37
- package/src/rules/pretty-ternary.js +42 -14
- package/src/rules/svelte-naming-convention.js +281 -122
- package/src/utils/keep_body_comments.js +53 -0
- package/src/utils/keep_comments.js +6 -2
- package/src/utils/keep_line_breaks.js +3 -1
- package/src/utils/skip_hole_commas.js +3 -1
- package/src/utils/skip_optional_patterns.js +3 -1
- package/src/utils/skip_unchanged_fixes.js +3 -1
- package/src/utils/source_text.js +57 -23
- package/types/index.d.ts.map +1 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const const_regex = /@type\s*\{\s*const\s*\}/
|
|
2
|
+
const directive_regex = /^\s*(?:eslint-disable-next-line\b|@ts-(?:expect-error|ignore)\b)/
|
|
3
|
+
const line_terminator_regex = /\r\n|[\n\r\p{Zl}\p{Zp}]/u
|
|
4
|
+
const space_regex = /^\s*$/
|
|
5
|
+
const statement_lists = new Set(
|
|
6
|
+
[
|
|
7
|
+
"BlockStatement",
|
|
8
|
+
"Program",
|
|
9
|
+
"StaticBlock",
|
|
10
|
+
"SwitchCase",
|
|
11
|
+
"TSModuleBlock"
|
|
12
|
+
]
|
|
13
|
+
)
|
|
14
|
+
const type_regex = /^\*[^]*@type\s*\{[^]*\}/
|
|
15
|
+
const typescript_file_regex = /\.[cm]?tsx?$/
|
|
16
|
+
/** @type {import("eslint").Rule.RuleModule} */
|
|
17
|
+
export default {
|
|
18
|
+
create(context) {
|
|
19
|
+
/** @type {import("../../private.js").TypeServices | undefined} */
|
|
20
|
+
const services = context.sourceCode.parserServices
|
|
21
|
+
const program = services?.program
|
|
22
|
+
if (!services || !program || typescript_file_regex.test(context.filename)) {
|
|
23
|
+
return {}
|
|
24
|
+
}
|
|
25
|
+
const checker = program.getTypeChecker()
|
|
26
|
+
const source_code = context.sourceCode
|
|
27
|
+
const text = source_code.text
|
|
28
|
+
/**
|
|
29
|
+
* @param {number} position
|
|
30
|
+
* @returns {number}
|
|
31
|
+
*/
|
|
32
|
+
function line_start_of(position) {
|
|
33
|
+
let start = position
|
|
34
|
+
while (start > 0 && !line_terminator_regex.test(text.charAt(start - 1))) {
|
|
35
|
+
start--
|
|
36
|
+
}
|
|
37
|
+
return start
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* @param {import("estree").Comment} comment
|
|
41
|
+
* @returns {boolean}
|
|
42
|
+
*/
|
|
43
|
+
function starts_line(comment) {
|
|
44
|
+
const start = /** @type {import("eslint").AST.Range} */(comment.range)/**/[0]
|
|
45
|
+
return space_regex.test(
|
|
46
|
+
text.slice(line_start_of(start), start)
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
VariableDeclaration(node) {
|
|
51
|
+
const [ declarator, other ] = node.declarations
|
|
52
|
+
const value = declarator?.init
|
|
53
|
+
const statement = node.parent.type == "ExportNamedDeclaration" ? node.parent : node
|
|
54
|
+
if (!declarator || !value || other || !statement_lists.has(statement.parent.type)) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
const open = source_code.getTokenBefore(value)
|
|
58
|
+
const close = source_code.getTokenAfter(value)
|
|
59
|
+
const assign = open && source_code.getTokenBefore(open)
|
|
60
|
+
if (open?.value != "(" || close?.value != ")" || assign?.value != "=") {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
const comment = source_code.getCommentsBefore(open).at(-1)
|
|
64
|
+
const comment_range = comment?.range
|
|
65
|
+
if (
|
|
66
|
+
!comment
|
|
67
|
+
|| !comment_range
|
|
68
|
+
|| comment.type != "Block"
|
|
69
|
+
|| !type_regex.test(comment.value)
|
|
70
|
+
|| const_regex.test(comment.value)
|
|
71
|
+
|| !space_regex.test(
|
|
72
|
+
text.slice(comment_range[1], open.range[0])
|
|
73
|
+
)
|
|
74
|
+
) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
const declaration = services.esTreeNodeToTSNodeMap.get(declarator)
|
|
78
|
+
const cast = /** @type {import("typescript").ParenthesizedExpression | undefined} */(declaration?.initializer)/**/
|
|
79
|
+
if (!cast?.expression) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
const leading = source_code.getCommentsBefore(statement)
|
|
83
|
+
const jsdoc = leading.filter(
|
|
84
|
+
item => item.type == "Block" && item.value.startsWith("*")
|
|
85
|
+
)
|
|
86
|
+
if (jsdoc.some(
|
|
87
|
+
item => type_regex.test(item.value)
|
|
88
|
+
)) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
const type = checker.getTypeAtLocation(cast)
|
|
92
|
+
const value_type = checker.getTypeAtLocation(cast.expression)
|
|
93
|
+
if (
|
|
94
|
+
!checker.isTypeAssignableTo(value_type, type)
|
|
95
|
+
|| type.isUnion() && !checker.isTypeAssignableTo(type, value_type)
|
|
96
|
+
) {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
const tag = text.slice(
|
|
100
|
+
comment_range[0],
|
|
101
|
+
comment_range[1]
|
|
102
|
+
)
|
|
103
|
+
let start = /** @type {import("eslint").AST.Range} */(statement.range)/**/[0]
|
|
104
|
+
for (let i = leading.length - 1; i >= 0; i--) {
|
|
105
|
+
const item = /** @type {import("estree").Comment} */(leading[i])/**/
|
|
106
|
+
if (!starts_line(item) || !directive_regex.test(item.value)) {
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
start = /** @type {import("eslint").AST.Range} */(item.range)/**/[0]
|
|
110
|
+
}
|
|
111
|
+
const end = text.startsWith("/**/", close.range[1]) ? close.range[1] + 4 : close.range[1]
|
|
112
|
+
const data = {
|
|
113
|
+
tag,
|
|
114
|
+
value: checker.typeToString(value_type)
|
|
115
|
+
}
|
|
116
|
+
const loc = {
|
|
117
|
+
end: source_code.getLocFromIndex(end),
|
|
118
|
+
start: source_code.getLocFromIndex(comment_range[0])
|
|
119
|
+
}
|
|
120
|
+
if (jsdoc.length) {
|
|
121
|
+
context.report(
|
|
122
|
+
{ data, loc, messageId: "merge" }
|
|
123
|
+
)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
const indent = text.slice(line_start_of(start), start)
|
|
127
|
+
const eol = text.match(line_terminator_regex)?.[0] ?? "\n"
|
|
128
|
+
const inner = text.slice(open.range[1], close.range[0])
|
|
129
|
+
context.report(
|
|
130
|
+
{
|
|
131
|
+
data,
|
|
132
|
+
fix(fixer) {
|
|
133
|
+
return [
|
|
134
|
+
fixer.insertTextBeforeRange(
|
|
135
|
+
[ start, start ],
|
|
136
|
+
space_regex.test(indent) ? tag + eol + indent : tag + " "
|
|
137
|
+
),
|
|
138
|
+
fixer.replaceTextRange(
|
|
139
|
+
[ comment_range[0], end ],
|
|
140
|
+
value.type == "SequenceExpression" ? `(${inner})` : inner
|
|
141
|
+
)
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
loc,
|
|
145
|
+
messageId: "tag"
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
meta: {
|
|
152
|
+
docs: {
|
|
153
|
+
description: "Enforces a JSDoc type tag on a declaration instead of a type cast of its value, wherever the value is assignable to the type.",
|
|
154
|
+
recommended: false,
|
|
155
|
+
url: "https://github.com/artxe/lube-series/blob/master/packages/eslint-plugin-lube/docs/prefer-type-tag.md"
|
|
156
|
+
},
|
|
157
|
+
fixable: "code",
|
|
158
|
+
messages: {
|
|
159
|
+
merge: "Type the declaration with `{{tag}}` instead of casting its value, since its type `{{value}}` is assignable to it; it is not fixed automatically because the declaration already has a JSDoc comment, so add the type to it by hand",
|
|
160
|
+
tag: "Write `{{tag}}` in front of the declaration instead of casting its value, since its type `{{value}}` is assignable to it"
|
|
161
|
+
},
|
|
162
|
+
schema: [],
|
|
163
|
+
type: "suggestion"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -45,12 +45,16 @@ export default {
|
|
|
45
45
|
)
|
|
46
46
|
const first = specifiers[0]
|
|
47
47
|
const last = specifiers[specifiers.length - 1]
|
|
48
|
-
if (first?.type != specifier_type || !last || source_code.getCommentsInside(node).length)
|
|
48
|
+
if (first?.type != specifier_type || !last || source_code.getCommentsInside(node).length) {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
49
51
|
const tokens = source_code.getTokens(node)
|
|
50
52
|
const open_index = tokens.findIndex(
|
|
51
53
|
token => token.range[0] == first.range[0]
|
|
52
54
|
) - 1
|
|
53
|
-
if (tokens[open_index]?.value != "{")
|
|
55
|
+
if (tokens[open_index]?.value != "{") {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
54
58
|
const head = tokens.slice(0, open_index)
|
|
55
59
|
.map(token => token.value)
|
|
56
60
|
.join(" ")
|
|
@@ -94,9 +98,13 @@ export default {
|
|
|
94
98
|
: `${head} ${names}`
|
|
95
99
|
) + tail + (keeps_semicolon ? ";" : "")
|
|
96
100
|
const current = get_text(node.range[0], end)
|
|
97
|
-
if (corrected_text == current)
|
|
101
|
+
if (corrected_text == current) {
|
|
102
|
+
return
|
|
103
|
+
}
|
|
98
104
|
const same_structure = to_structure(current) == to_structure(corrected_text)
|
|
99
|
-
if (!fix_indent && same_structure)
|
|
105
|
+
if (!fix_indent && same_structure) {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
100
108
|
context.report(
|
|
101
109
|
{
|
|
102
110
|
data: {
|
|
@@ -127,18 +135,22 @@ export default {
|
|
|
127
135
|
/** @type {import("../../private.js").RuleListener} */
|
|
128
136
|
const listener = {
|
|
129
137
|
ExportNamedDeclaration: node => {
|
|
130
|
-
if (check_exports)
|
|
138
|
+
if (check_exports) {
|
|
139
|
+
verify(
|
|
131
140
|
/** @type {import("../../private.js").AstNode & import("estree").ExportNamedDeclaration} */(node)/**/,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
141
|
+
"ExportSpecifier",
|
|
142
|
+
"export"
|
|
143
|
+
)
|
|
144
|
+
}
|
|
135
145
|
},
|
|
136
146
|
ImportDeclaration: node => {
|
|
137
|
-
if (check_imports)
|
|
147
|
+
if (check_imports) {
|
|
148
|
+
verify(
|
|
138
149
|
/** @type {import("../../private.js").AstNode & import("estree").ImportDeclaration} */(node)/**/,
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
150
|
+
"ImportSpecifier",
|
|
151
|
+
"import"
|
|
152
|
+
)
|
|
153
|
+
}
|
|
142
154
|
}
|
|
143
155
|
}
|
|
144
156
|
return /** @type {import("eslint").Rule.RuleListener} */(/** @type {unknown} */(listener))/**/
|
|
@@ -8,7 +8,9 @@ const marker_regex = /\/\*\*\//g
|
|
|
8
8
|
/** @type {import("eslint").Rule.RuleModule} */
|
|
9
9
|
export default {
|
|
10
10
|
create(context) {
|
|
11
|
-
if (typescript_file_regex.test(context.filename))
|
|
11
|
+
if (typescript_file_regex.test(context.filename)) {
|
|
12
|
+
return {}
|
|
13
|
+
}
|
|
12
14
|
const source_code = context.sourceCode
|
|
13
15
|
const source = create_source_text(source_code)
|
|
14
16
|
const text = source.text
|
|
@@ -35,9 +37,13 @@ export default {
|
|
|
35
37
|
*/
|
|
36
38
|
function is_arguments(position) {
|
|
37
39
|
const node = /** @type {import("../../private.js").AstNode | null} */(/** @type {unknown} */(source_code.getNodeByRangeIndex(position)))/**/
|
|
38
|
-
if (!node)
|
|
40
|
+
if (!node) {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
39
43
|
const type = node.type
|
|
40
|
-
if (type != "CallExpression" && type != "ImportExpression" && type != "NewExpression")
|
|
44
|
+
if (type != "CallExpression" && type != "ImportExpression" && type != "NewExpression") {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
41
47
|
const before = type == "ImportExpression"
|
|
42
48
|
? source_code.getFirstToken(
|
|
43
49
|
/** @type {import("estree").Node} */(node)/**/
|
|
@@ -57,7 +63,9 @@ export default {
|
|
|
57
63
|
*/
|
|
58
64
|
function push(node) {
|
|
59
65
|
const range = node.range[0] + " " + node.range[1]
|
|
60
|
-
if (node_ranges.has(range))
|
|
66
|
+
if (node_ranges.has(range)) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
61
69
|
node_ranges.add(range)
|
|
62
70
|
expressions.push(node)
|
|
63
71
|
}
|
|
@@ -71,17 +79,27 @@ export default {
|
|
|
71
79
|
let position = start
|
|
72
80
|
let result = ""
|
|
73
81
|
for (const [ removed_start, removed_end ] of removed) {
|
|
74
|
-
if (removed_end <= start || removed_start >= end)
|
|
75
|
-
|
|
82
|
+
if (removed_end <= start || removed_start >= end) {
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
if (source.crosses_edit(removed_start) || source.crosses_edit(removed_end)) {
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
76
88
|
let chunk_end = removed_start
|
|
77
89
|
if (text[removed_start] == ")") {
|
|
78
|
-
while (chunk_end > position && is_space(text.charCodeAt(chunk_end - 1)))
|
|
79
|
-
|
|
90
|
+
while (chunk_end > position && is_space(text.charCodeAt(chunk_end - 1))) {
|
|
91
|
+
chunk_end--
|
|
92
|
+
}
|
|
93
|
+
if (source.comment_by_end.get(chunk_end)?.type == "Line") {
|
|
94
|
+
chunk_end = removed_start
|
|
95
|
+
}
|
|
80
96
|
}
|
|
81
97
|
result += source.render(position, chunk_end)
|
|
82
98
|
position = removed_end
|
|
83
99
|
if (text[removed_start] == "(") {
|
|
84
|
-
while (position < end && is_space(text.charCodeAt(position)))
|
|
100
|
+
while (position < end && is_space(text.charCodeAt(position))) {
|
|
101
|
+
position++
|
|
102
|
+
}
|
|
85
103
|
}
|
|
86
104
|
}
|
|
87
105
|
return result + source.render(position, end)
|
|
@@ -93,7 +111,9 @@ export default {
|
|
|
93
111
|
function verify(node) {
|
|
94
112
|
const node_start = node.range[0]
|
|
95
113
|
const before = source.skip_left(node_start, 0)
|
|
96
|
-
if (!before || text[before - 1] != "(" || is_arguments(before - 1))
|
|
114
|
+
if (!before || text[before - 1] != "(" || is_arguments(before - 1)) {
|
|
115
|
+
return
|
|
116
|
+
}
|
|
97
117
|
/** @type {import("../../private.js").CastingLevel[]} */
|
|
98
118
|
const levels = []
|
|
99
119
|
/** @type {[number, number][]} */
|
|
@@ -110,27 +130,39 @@ export default {
|
|
|
110
130
|
continue
|
|
111
131
|
}
|
|
112
132
|
const comment = source.comment_by_end.get(left)
|
|
113
|
-
if (!comment)
|
|
133
|
+
if (!comment) {
|
|
134
|
+
break
|
|
135
|
+
}
|
|
114
136
|
const level = levels[levels.length - 1]
|
|
115
137
|
const type = comment.type == "Block" && type_regex.test(comment.value)
|
|
116
138
|
? get_type_comment(comment)
|
|
117
139
|
: void 0
|
|
118
140
|
if (type && level && level.type == null) {
|
|
119
141
|
let comment_end = comment.range[1]
|
|
120
|
-
while (comment_end < level.open && is_space(text.charCodeAt(comment_end)))
|
|
142
|
+
while (comment_end < level.open && is_space(text.charCodeAt(comment_end))) {
|
|
143
|
+
comment_end++
|
|
144
|
+
}
|
|
121
145
|
level.type = type
|
|
122
146
|
removed.push(
|
|
123
147
|
[ comment.range[0], comment_end ]
|
|
124
148
|
)
|
|
125
|
-
} else if (type)
|
|
149
|
+
} else if (type) {
|
|
150
|
+
has_inner_type_comment = true
|
|
151
|
+
}
|
|
126
152
|
left = comment.range[0]
|
|
127
153
|
}
|
|
128
|
-
if (!left || text[left - 1] != "(" || is_arguments(left - 1))
|
|
154
|
+
if (!left || text[left - 1] != "(" || is_arguments(left - 1)) {
|
|
155
|
+
break
|
|
156
|
+
}
|
|
129
157
|
const close = source.skip_right(right, text.length)
|
|
130
158
|
right = close
|
|
131
|
-
if (text[close] != ")")
|
|
159
|
+
if (text[close] != ")") {
|
|
160
|
+
break
|
|
161
|
+
}
|
|
132
162
|
if (has_inner_type_comment) {
|
|
133
|
-
if (levels.length)
|
|
163
|
+
if (levels.length) {
|
|
164
|
+
return
|
|
165
|
+
}
|
|
134
166
|
content_end = close
|
|
135
167
|
content_start = left
|
|
136
168
|
has_inner_type_comment = false
|
|
@@ -149,16 +181,21 @@ export default {
|
|
|
149
181
|
}
|
|
150
182
|
const outer = levels[levels.length - 1]
|
|
151
183
|
const first = levels[0]
|
|
152
|
-
if (!outer || !first)
|
|
184
|
+
if (!outer || !first) {
|
|
185
|
+
return
|
|
186
|
+
}
|
|
153
187
|
const restores_outer = outer.type == null
|
|
154
188
|
for (const level of levels) {
|
|
155
|
-
if (level == outer && restores_outer)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
189
|
+
if (level == outer && restores_outer) {
|
|
190
|
+
removed.push(
|
|
191
|
+
[ level.close + 1, level.close_end ]
|
|
192
|
+
)
|
|
193
|
+
} else {
|
|
194
|
+
removed.push(
|
|
195
|
+
[ level.open, level.open + 1 ],
|
|
196
|
+
[ level.close, level.close_end ]
|
|
197
|
+
)
|
|
198
|
+
}
|
|
162
199
|
}
|
|
163
200
|
removed.sort((a, b) => a[0] - b[0])
|
|
164
201
|
if (content_end + 4 <= first.close && source.render(content_end, content_end + 4) == "/**/") {
|
|
@@ -171,12 +208,16 @@ export default {
|
|
|
171
208
|
if (unwrapped_outer) {
|
|
172
209
|
let type_comments = 0
|
|
173
210
|
for (const level of unwrapped) {
|
|
174
|
-
if (level != unwrapped_outer && level.type != null)
|
|
211
|
+
if (level != unwrapped_outer && level.type != null) {
|
|
212
|
+
type_comments++
|
|
213
|
+
}
|
|
175
214
|
}
|
|
176
215
|
if (source.count_comments(
|
|
177
216
|
unwrapped_outer.open,
|
|
178
217
|
content_start
|
|
179
|
-
) > type_comments)
|
|
218
|
+
) > type_comments) {
|
|
219
|
+
return
|
|
220
|
+
}
|
|
180
221
|
}
|
|
181
222
|
const boundaries = [
|
|
182
223
|
left,
|
|
@@ -184,7 +225,9 @@ export default {
|
|
|
184
225
|
content_end,
|
|
185
226
|
right
|
|
186
227
|
]
|
|
187
|
-
if (boundaries.some(source.crosses_edit))
|
|
228
|
+
if (boundaries.some(source.crosses_edit)) {
|
|
229
|
+
return
|
|
230
|
+
}
|
|
188
231
|
let corrected_text = source.render(content_start, content_end)
|
|
189
232
|
for (const [ index, level ] of unwrapped.entries()) {
|
|
190
233
|
corrected_text = level.type
|
|
@@ -202,10 +245,14 @@ export default {
|
|
|
202
245
|
&& restricted_regex.test(
|
|
203
246
|
text.slice(Math.max(0, left - 7), left)
|
|
204
247
|
)
|
|
205
|
-
)
|
|
248
|
+
) {
|
|
249
|
+
return
|
|
250
|
+
}
|
|
206
251
|
corrected_text = prefix + corrected_text + suffix
|
|
207
252
|
const current = source.render(left, right)
|
|
208
|
-
if (current == corrected_text)
|
|
253
|
+
if (current == corrected_text) {
|
|
254
|
+
return
|
|
255
|
+
}
|
|
209
256
|
const message_id = current.replace(marker_regex, "") == corrected_text.replace(marker_regex, "")
|
|
210
257
|
? "marker"
|
|
211
258
|
: "format"
|