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.
@@ -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) return
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 != "{") return
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) return
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) return
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) verify(
138
+ if (check_exports) {
139
+ verify(
131
140
  /** @type {import("../../private.js").AstNode & import("estree").ExportNamedDeclaration} */(node)/**/,
132
- "ExportSpecifier",
133
- "export"
134
- )
141
+ "ExportSpecifier",
142
+ "export"
143
+ )
144
+ }
135
145
  },
136
146
  ImportDeclaration: node => {
137
- if (check_imports) verify(
147
+ if (check_imports) {
148
+ verify(
138
149
  /** @type {import("../../private.js").AstNode & import("estree").ImportDeclaration} */(node)/**/,
139
- "ImportSpecifier",
140
- "import"
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)) return {}
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) return false
40
+ if (!node) {
41
+ return false
42
+ }
39
43
  const type = node.type
40
- if (type != "CallExpression" && type != "ImportExpression" && type != "NewExpression") return false
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)) return
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) continue
75
- if (source.crosses_edit(removed_start) || source.crosses_edit(removed_end)) return null
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))) chunk_end--
79
- if (source.comment_by_end.get(chunk_end)?.type == "Line") chunk_end = removed_start
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))) 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)) return
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) break
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))) 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) has_inner_type_comment = true
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)) break
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] != ")") break
159
+ if (text[close] != ")") {
160
+ break
161
+ }
132
162
  if (has_inner_type_comment) {
133
- if (levels.length) return
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) return
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) removed.push(
156
- [ level.close + 1, level.close_end ]
157
- )
158
- else removed.push(
159
- [ level.open, level.open + 1 ],
160
- [ level.close, level.close_end ]
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) type_comments++
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) return
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)) return
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
- ) return
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) return
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"