@ttsc/lint 0.19.3 → 0.20.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.
Files changed (63) hide show
  1. package/README.md +1 -1
  2. package/lib/index.js +26 -4
  3. package/lib/index.js.map +1 -1
  4. package/lib/structures/format/ITtscLintFormat.d.ts +14 -10
  5. package/lib/structures/rules/ITtscLintRegexpRules.d.ts +24 -9
  6. package/lib/structures/rules/ITtscLintSecurityRules.d.ts +18 -0
  7. package/lib/structures/rules/ITtscLintSolidRules.d.ts +22 -2
  8. package/lib/structures/rules/ITtscLintStorybookRules.d.ts +21 -0
  9. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +10 -1
  10. package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +13 -2
  11. package/linthost/compile.go +9 -4
  12. package/linthost/config.go +18 -14
  13. package/linthost/contrib_adapter.go +57 -0
  14. package/linthost/dispatch.go +5 -1
  15. package/linthost/display_width.go +179 -41
  16. package/linthost/engine.go +120 -4
  17. package/linthost/flags_gen.go +4 -4
  18. package/linthost/hints.go +207 -0
  19. package/linthost/host.go +68 -0
  20. package/linthost/lsp.go +148 -29
  21. package/linthost/print_dispatch.go +24 -0
  22. package/linthost/print_nodes_array.go +88 -15
  23. package/linthost/print_nodes_call.go +38 -31
  24. package/linthost/print_nodes_control_flow.go +319 -0
  25. package/linthost/print_nodes_function.go +140 -13
  26. package/linthost/print_nodes_list.go +22 -10
  27. package/linthost/project_engine.go +18 -1
  28. package/linthost/project_rules.go +47 -0
  29. package/linthost/rule_docs.go +114 -0
  30. package/linthost/rules_boundaries.go +80 -2
  31. package/linthost/rules_format_bracket_spacing.go +18 -6
  32. package/linthost/rules_format_clause_join.go +11 -8
  33. package/linthost/rules_format_indent.go +155 -4
  34. package/linthost/rules_format_print_width.go +12 -35
  35. package/linthost/rules_format_quote_props.go +88 -27
  36. package/linthost/rules_format_statement_split.go +81 -0
  37. package/linthost/rules_format_trailing_comma.go +62 -94
  38. package/linthost/rules_gap.go +26 -17
  39. package/linthost/rules_jsdoc.go +54 -0
  40. package/linthost/rules_logic.go +30 -15
  41. package/linthost/rules_no_redeclare.go +12 -3
  42. package/linthost/rules_promise.go +37 -15
  43. package/linthost/rules_regexp.go +443 -49
  44. package/linthost/rules_security.go +243 -4
  45. package/linthost/rules_solid.go +430 -10
  46. package/linthost/rules_storybook.go +255 -10
  47. package/linthost/rules_suggestions.go +60 -39
  48. package/linthost/rules_ts.go +7 -0
  49. package/linthost/rules_ts_async.go +80 -2
  50. package/linthost/rules_ts_extra.go +21 -7
  51. package/linthost/serve.go +318 -0
  52. package/linthost/width_tables_gen.go +219 -0
  53. package/package.json +2 -2
  54. package/rule/hint.go +142 -0
  55. package/rule/rule.go +197 -1
  56. package/src/index.ts +35 -4
  57. package/src/structures/format/ITtscLintFormat.ts +14 -10
  58. package/src/structures/rules/ITtscLintRegexpRules.ts +24 -9
  59. package/src/structures/rules/ITtscLintSecurityRules.ts +18 -0
  60. package/src/structures/rules/ITtscLintSolidRules.ts +22 -2
  61. package/src/structures/rules/ITtscLintStorybookRules.ts +21 -0
  62. package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +10 -1
  63. package/src/structures/rules/ITtscLintTypeScriptRules.ts +13 -2
@@ -4,8 +4,9 @@ import (
4
4
  shimast "github.com/microsoft/typescript-go/shim/ast"
5
5
  )
6
6
 
7
- // formatTrailingComma adds trailing commas to multi-line lists. Mirrors
8
- // prettier's `trailingComma: "all"` default, *not* a tunable.
7
+ // formatTrailingComma normalizes trailing commas on governed lists. It adds a
8
+ // comma to a multi-line list when the configured mode permits one and removes
9
+ // an existing comma when that mode forbids one.
9
10
  //
10
11
  // Scope (intentionally narrower than the closing-brace surface of TS):
11
12
  //
@@ -14,6 +15,7 @@ import (
14
15
  // - CallExpression / NewExpression `foo(a, b)` / `new Foo(a, b)`
15
16
  // - NamedImports / NamedExports `import { a, b } from "x"`
16
17
  // - TupleType `[A, B]` at the type level
18
+ // - EnumDeclaration `enum E { A, B }`
17
19
  // - Function parameter lists `function foo(a, b) {}` etc.,
18
20
  // including interface call/construct/method signatures and
19
21
  // `(a, b) => …` / `new (a, b) => …` function/constructor type
@@ -29,11 +31,11 @@ import (
29
31
  // - JSX attribute lists. Prettier does not apply trailing commas there
30
32
  // either.
31
33
  //
32
- // Rest parameters (`...rest`) are explicitly skipped: ECMAScript forbids
33
- // a trailing comma after a rest element (TS1013), so the rule must not
34
- // insert one even when the rest parameter is the multi-line list's last
35
- // element. The same restriction applies to rest binding patterns, which
36
- // the rule does not visit at all.
34
+ // Rest parameters (`...rest`) are explicitly skipped only when adding a
35
+ // comma: ECMAScript forbids a trailing comma after a rest element (TS1013).
36
+ // A legacy comma is not syntactically valid, so there is nothing for the
37
+ // removal path to preserve. The same restriction applies to rest binding
38
+ // patterns, which the rule does not visit at all.
37
39
  //
38
40
  // Destructuring assignment TARGETS are the one array/object-literal shape
39
41
  // where the same rest restriction bites: `({ a, ...rest } = obj)` and
@@ -69,6 +71,7 @@ func (formatTrailingComma) Visits() []shimast.Kind {
69
71
  shimast.KindNamedImports,
70
72
  shimast.KindNamedExports,
71
73
  shimast.KindTupleType,
74
+ shimast.KindEnumDeclaration,
72
75
  shimast.KindFunctionDeclaration,
73
76
  shimast.KindFunctionExpression,
74
77
  shimast.KindArrowFunction,
@@ -95,9 +98,6 @@ func (formatTrailingComma) Check(ctx *Context, node *shimast.Node) {
95
98
  if mode == "" {
96
99
  mode = "all"
97
100
  }
98
- if mode == "none" {
99
- return
100
- }
101
101
  switch node.Kind {
102
102
  case shimast.KindArrayLiteralExpression:
103
103
  arr := node.AsArrayLiteralExpression()
@@ -107,7 +107,7 @@ func (formatTrailingComma) Check(ctx *Context, node *shimast.Node) {
107
107
  if isRestAssignmentTargetLiteral(node) {
108
108
  return
109
109
  }
110
- considerTrailingComma(ctx, arr.Elements, node.End()-1)
110
+ normalizeTrailingComma(ctx, arr.Elements, node.End()-1, mode != "none")
111
111
  case shimast.KindObjectLiteralExpression:
112
112
  obj := node.AsObjectLiteralExpression()
113
113
  if obj == nil {
@@ -116,11 +116,8 @@ func (formatTrailingComma) Check(ctx *Context, node *shimast.Node) {
116
116
  if isRestAssignmentTargetLiteral(node) {
117
117
  return
118
118
  }
119
- considerTrailingComma(ctx, obj.Properties, node.End()-1)
119
+ normalizeTrailingComma(ctx, obj.Properties, node.End()-1, mode != "none")
120
120
  case shimast.KindCallExpression:
121
- if mode == "es5" {
122
- return // prettier's es5 mode skips call arguments
123
- }
124
121
  call := node.AsCallExpression()
125
122
  if call == nil {
126
123
  return
@@ -133,149 +130,110 @@ func (formatTrailingComma) Check(ctx *Context, node *shimast.Node) {
133
130
  // same exception so the two rules agree on the reflowed shape.
134
131
  return
135
132
  }
136
- considerTrailingComma(ctx, call.Arguments, node.End()-1)
133
+ normalizeTrailingComma(ctx, call.Arguments, node.End()-1, mode == "all")
137
134
  case shimast.KindNewExpression:
138
- if mode == "es5" {
139
- return
140
- }
141
135
  ne := node.AsNewExpression()
142
136
  if ne == nil || ne.Arguments == nil {
143
137
  return
144
138
  }
145
- considerTrailingComma(ctx, ne.Arguments, node.End()-1)
139
+ normalizeTrailingComma(ctx, ne.Arguments, node.End()-1, mode == "all")
146
140
  case shimast.KindNamedImports:
147
141
  named := node.AsNamedImports()
148
142
  if named == nil {
149
143
  return
150
144
  }
151
- considerTrailingComma(ctx, named.Elements, node.End()-1)
145
+ normalizeTrailingComma(ctx, named.Elements, node.End()-1, mode != "none")
152
146
  case shimast.KindNamedExports:
153
147
  named := node.AsNamedExports()
154
148
  if named == nil {
155
149
  return
156
150
  }
157
- considerTrailingComma(ctx, named.Elements, node.End()-1)
151
+ normalizeTrailingComma(ctx, named.Elements, node.End()-1, mode != "none")
158
152
  case shimast.KindTupleType:
159
- if mode == "es5" {
160
- return // tuple types are type-level; ES5 mode is runtime-only
161
- }
162
153
  tup := node.AsTupleTypeNode()
163
154
  if tup == nil {
164
155
  return
165
156
  }
166
- considerTrailingComma(ctx, tup.Elements, node.End()-1)
167
- case shimast.KindFunctionDeclaration:
168
- if mode == "es5" {
157
+ normalizeTrailingComma(ctx, tup.Elements, node.End()-1, mode != "none")
158
+ case shimast.KindEnumDeclaration:
159
+ decl := node.AsEnumDeclaration()
160
+ if decl == nil {
169
161
  return
170
162
  }
163
+ normalizeTrailingComma(ctx, decl.Members, node.End()-1, mode != "none")
164
+ case shimast.KindFunctionDeclaration:
171
165
  fn := node.AsFunctionDeclaration()
172
166
  if fn == nil {
173
167
  return
174
168
  }
175
- considerFunctionParameterComma(ctx, fn.Parameters)
169
+ normalizeFunctionParameterComma(ctx, fn.Parameters, mode == "all")
176
170
  case shimast.KindFunctionExpression:
177
- if mode == "es5" {
178
- return
179
- }
180
171
  fn := node.AsFunctionExpression()
181
172
  if fn == nil {
182
173
  return
183
174
  }
184
- considerFunctionParameterComma(ctx, fn.Parameters)
175
+ normalizeFunctionParameterComma(ctx, fn.Parameters, mode == "all")
185
176
  case shimast.KindArrowFunction:
186
- if mode == "es5" {
187
- return
188
- }
189
177
  fn := node.AsArrowFunction()
190
178
  if fn == nil {
191
179
  return
192
180
  }
193
- considerFunctionParameterComma(ctx, fn.Parameters)
181
+ normalizeFunctionParameterComma(ctx, fn.Parameters, mode == "all")
194
182
  case shimast.KindMethodDeclaration:
195
- if mode == "es5" {
196
- return
197
- }
198
183
  fn := node.AsMethodDeclaration()
199
184
  if fn == nil {
200
185
  return
201
186
  }
202
- considerFunctionParameterComma(ctx, fn.Parameters)
187
+ normalizeFunctionParameterComma(ctx, fn.Parameters, mode == "all")
203
188
  case shimast.KindConstructor:
204
- if mode == "es5" {
205
- return
206
- }
207
189
  fn := node.AsConstructorDeclaration()
208
190
  if fn == nil {
209
191
  return
210
192
  }
211
- considerFunctionParameterComma(ctx, fn.Parameters)
193
+ normalizeFunctionParameterComma(ctx, fn.Parameters, mode == "all")
212
194
  case shimast.KindGetAccessor:
213
- if mode == "es5" {
214
- return
215
- }
216
195
  fn := node.AsGetAccessorDeclaration()
217
196
  if fn == nil {
218
197
  return
219
198
  }
220
- considerFunctionParameterComma(ctx, fn.Parameters)
199
+ normalizeFunctionParameterComma(ctx, fn.Parameters, mode == "all")
221
200
  case shimast.KindSetAccessor:
222
- if mode == "es5" {
223
- return
224
- }
225
201
  fn := node.AsSetAccessorDeclaration()
226
202
  if fn == nil {
227
203
  return
228
204
  }
229
- considerFunctionParameterComma(ctx, fn.Parameters)
205
+ normalizeFunctionParameterComma(ctx, fn.Parameters, mode == "all")
230
206
  case shimast.KindMethodSignature:
231
- if mode == "es5" {
232
- return
233
- }
234
207
  sig := node.AsMethodSignatureDeclaration()
235
208
  if sig == nil {
236
209
  return
237
210
  }
238
- considerFunctionParameterComma(ctx, sig.Parameters)
211
+ normalizeFunctionParameterComma(ctx, sig.Parameters, mode == "all")
239
212
  case shimast.KindCallSignature:
240
- if mode == "es5" {
241
- return
242
- }
243
213
  sig := node.AsCallSignatureDeclaration()
244
214
  if sig == nil {
245
215
  return
246
216
  }
247
- considerFunctionParameterComma(ctx, sig.Parameters)
217
+ normalizeFunctionParameterComma(ctx, sig.Parameters, mode == "all")
248
218
  case shimast.KindConstructSignature:
249
- if mode == "es5" {
250
- return
251
- }
252
219
  sig := node.AsConstructSignatureDeclaration()
253
220
  if sig == nil {
254
221
  return
255
222
  }
256
- considerFunctionParameterComma(ctx, sig.Parameters)
223
+ normalizeFunctionParameterComma(ctx, sig.Parameters, mode == "all")
257
224
  case shimast.KindFunctionType:
258
- if mode == "es5" {
259
- return
260
- }
261
225
  ft := node.AsFunctionTypeNode()
262
226
  if ft == nil {
263
227
  return
264
228
  }
265
- considerFunctionParameterComma(ctx, ft.Parameters)
229
+ normalizeFunctionParameterComma(ctx, ft.Parameters, mode == "all")
266
230
  case shimast.KindConstructorType:
267
- if mode == "es5" {
268
- return
269
- }
270
231
  ct := node.AsConstructorTypeNode()
271
232
  if ct == nil {
272
233
  return
273
234
  }
274
- considerFunctionParameterComma(ctx, ct.Parameters)
235
+ normalizeFunctionParameterComma(ctx, ct.Parameters, mode == "all")
275
236
  case shimast.KindTypeParameter:
276
- if mode == "es5" {
277
- return // type parameters postdate ES5; prettier es5 skips them
278
- }
279
237
  if node.Parent == nil {
280
238
  return
281
239
  }
@@ -288,7 +246,7 @@ func (formatTrailingComma) Check(ctx *Context, node *shimast.Node) {
288
246
  if closePos < 0 {
289
247
  return
290
248
  }
291
- considerTrailingComma(ctx, list, closePos)
249
+ normalizeTrailingComma(ctx, list, closePos, mode != "none")
292
250
  }
293
251
  }
294
252
 
@@ -303,9 +261,10 @@ func isDynamicImportCall(call *shimast.CallExpression) bool {
303
261
  call.Expression.Kind == shimast.KindImportKeyword
304
262
  }
305
263
 
306
- // considerTrailingComma reports a fix when the bracket-delimited list is
307
- // multi-line and missing its trailing comma. `closeBracketPos` points at
308
- // the closing punctuation byte itself (e.g. the `]` of an array literal).
264
+ // normalizeTrailingComma aligns one bracket-delimited list with the configured
265
+ // policy. `closeBracketPos` points at its closing punctuation (for example the
266
+ // `]` of an array literal). Addition stays limited to multi-line lists, as
267
+ // Prettier does, while removal also handles a pre-existing single-line comma.
309
268
  //
310
269
  // "Multi-line" means the close bracket sits on a different line from the
311
270
  // last element's end, not just "the list contains a newline somewhere".
@@ -316,7 +275,7 @@ func isDynamicImportCall(call *shimast.CallExpression) bool {
316
275
  // but `}` and `)` collapse onto one line; inserting `,)` there would be
317
276
  // stylistically wrong and, for parameter rest-paths nearby, can shift
318
277
  // later diagnostics.
319
- func considerTrailingComma(ctx *Context, list *shimast.NodeList, closeBracketPos int) {
278
+ func normalizeTrailingComma(ctx *Context, list *shimast.NodeList, closeBracketPos int, want bool) {
320
279
  if list == nil || len(list.Nodes) == 0 {
321
280
  return
322
281
  }
@@ -328,10 +287,20 @@ func considerTrailingComma(ctx *Context, list *shimast.NodeList, closeBracketPos
328
287
  if closeBracketPos < 0 || closeBracketPos >= len(src) {
329
288
  return
330
289
  }
331
- if !rangeSpansMultipleLines(src, last.End(), closeBracketPos) {
290
+ comma := trailingCommaPos(src, last.End(), closeBracketPos)
291
+ if comma >= 0 {
292
+ if want {
293
+ return
294
+ }
295
+ ctx.ReportRangeFix(
296
+ comma,
297
+ comma+1,
298
+ "Trailing comma is not allowed by this trailingComma mode.",
299
+ TextEdit{Pos: comma, End: comma + 1, Text: ""},
300
+ )
332
301
  return
333
302
  }
334
- if rangeHasTrailingComma(src, last.End(), closeBracketPos) {
303
+ if !want || !rangeSpansMultipleLines(src, last.End(), closeBracketPos) {
335
304
  return
336
305
  }
337
306
  ctx.ReportRangeFix(
@@ -352,11 +321,11 @@ func considerTrailingComma(ctx *Context, list *shimast.NodeList, closeBracketPos
352
321
  // a TS1013 syntax error. The rule peeks the last element's
353
322
  // DotDotDotToken and bails when set, since the rest must remain the
354
323
  // terminal element with no following comma.
355
- func considerFunctionParameterComma(ctx *Context, list *shimast.NodeList) {
324
+ func normalizeFunctionParameterComma(ctx *Context, list *shimast.NodeList, want bool) {
356
325
  if list == nil || len(list.Nodes) == 0 {
357
326
  return
358
327
  }
359
- if lastParameterIsRest(list) {
328
+ if want && lastParameterIsRest(list) {
360
329
  return
361
330
  }
362
331
  src := ctx.File.Text()
@@ -364,7 +333,7 @@ func considerFunctionParameterComma(ctx *Context, list *shimast.NodeList) {
364
333
  if closePos < 0 {
365
334
  return
366
335
  }
367
- considerTrailingComma(ctx, list, closePos)
336
+ normalizeTrailingComma(ctx, list, closePos, want)
368
337
  }
369
338
 
370
339
  // isRestAssignmentTargetLiteral reports whether node is an object- or
@@ -494,11 +463,10 @@ func rangeSpansMultipleLines(src string, a, b int) bool {
494
463
  return false
495
464
  }
496
465
 
497
- // rangeHasTrailingComma scans the source between the last item's end and
498
- // the close bracket. Returns true if a `,` is the first non-whitespace,
499
- // non-comment byte. Comments after the trailing comma still count as
500
- // "comma present".
501
- func rangeHasTrailingComma(src string, start, end int) bool {
466
+ // trailingCommaPos returns the offset of the comma immediately following a
467
+ // list's final item, skipping whitespace and comments. It deliberately does
468
+ // not scan through another token, so it cannot remove an item separator.
469
+ func trailingCommaPos(src string, start, end int) int {
502
470
  if start < 0 {
503
471
  start = 0
504
472
  }
@@ -508,7 +476,7 @@ func rangeHasTrailingComma(src string, start, end int) bool {
508
476
  for i := start; i < end; {
509
477
  c := src[i]
510
478
  if c == ',' {
511
- return true
479
+ return i
512
480
  }
513
481
  if c == ' ' || c == '\t' || c == '\r' || c == '\n' {
514
482
  i++
@@ -532,9 +500,9 @@ func rangeHasTrailingComma(src string, start, end int) bool {
532
500
  continue
533
501
  }
534
502
  }
535
- return false
503
+ return -1
536
504
  }
537
- return false
505
+ return -1
538
506
  }
539
507
 
540
508
  func init() {
@@ -540,13 +540,6 @@ func (dotNotation) Check(ctx *Context, node *shimast.Node) {
540
540
  return
541
541
  }
542
542
  message := "Use dot notation instead of a string literal property access."
543
- // Conservative: keep reserved words as bracket access. Modern JS accepts
544
- // `obj.class`/`obj.if`/etc. syntactically but mixing them with member
545
- // expression syntax is jarring and can confuse minifiers and older runtimes.
546
- if isReservedWord(key) {
547
- ctx.Report(node, message)
548
- return
549
- }
550
543
  src := ctx.File.Text()
551
544
  // Determine where the bracket access begins. With an optional chain
552
545
  // (`obj?.["foo"]`) the `?.` token already separates object from access, so
@@ -575,18 +568,34 @@ func (dotNotation) Check(ctx *Context, node *shimast.Node) {
575
568
  ctx.Report(node, message)
576
569
  return
577
570
  }
571
+ edit := TextEdit{Pos: replaceFrom, End: node.End(), Text: replacement}
572
+ switch {
578
573
  // A comment inside the replaced `[…]` tail (`p1 /* keep */ ["foo"]`) would be
579
- // silently deleted by the splice. Decline to a report-only diagnostic,
580
- // matching ESLint dot-notation's `commentsExistBetween` guard.
581
- if hasCommentBetween(src, replaceFrom, node.End()) {
582
- ctx.Report(node, message)
583
- return
574
+ // silently deleted by the splice, so it is never imposed — matching ESLint
575
+ // dot-notation's `commentsExistBetween` guard. The rewrite itself is still
576
+ // correct, so it is offered as an opt-in suggestion that names the loss.
577
+ case hasCommentBetween(src, replaceFrom, node.End()):
578
+ ctx.ReportSuggestion(
579
+ node,
580
+ message,
581
+ "Use dot notation, discarding the comment inside the brackets.",
582
+ edit,
583
+ )
584
+ // Conservative: keep reserved words as bracket access. Modern JS accepts
585
+ // `obj.class`/`obj.if`/etc. syntactically but mixing them with member
586
+ // expression syntax is jarring and can confuse minifiers and older runtimes.
587
+ // ESLint's default `allowKeywords: true` does rewrite them, so the edit is
588
+ // offered as an opt-in suggestion rather than withheld outright.
589
+ case isReservedWord(key):
590
+ ctx.ReportSuggestion(
591
+ node,
592
+ message,
593
+ "Use dot notation for the reserved word `"+key+"`.",
594
+ edit,
595
+ )
596
+ default:
597
+ ctx.ReportFix(node, message, edit)
584
598
  }
585
- ctx.ReportFix(
586
- node,
587
- message,
588
- TextEdit{Pos: replaceFrom, End: node.End(), Text: replacement},
589
- )
590
599
  }
591
600
 
592
601
  // isDecimalIntegerLiteralText reports whether raw is a numeric literal written
@@ -1,9 +1,12 @@
1
1
  package linthost
2
2
 
3
3
  import (
4
+ "sort"
4
5
  "strings"
5
6
 
6
7
  shimast "github.com/microsoft/typescript-go/shim/ast"
8
+
9
+ publicrule "github.com/samchon/ttsc/packages/lint/rule"
7
10
  )
8
11
 
9
12
  type jsdocLintRule struct {
@@ -11,6 +14,56 @@ type jsdocLintRule struct {
11
14
  check func(*Context, string, parsedJSDocBlock)
12
15
  }
13
16
 
17
+ type jsdocCheckTagNamesHintRule struct{}
18
+
19
+ type jsdocCheckTagNamesHintState struct{}
20
+
21
+ func (jsdocCheckTagNamesHintRule) Name() string { return "jsdoc/check-tag-names" }
22
+ func (jsdocCheckTagNamesHintRule) Check(ctx *publicrule.ProjectContext) {
23
+ ctx.SetState(jsdocCheckTagNamesHintState{})
24
+ }
25
+ func (jsdocCheckTagNamesHintRule) AcceptsTtscLintOptions() bool { return false }
26
+ func (jsdocCheckTagNamesHintRule) NeedsTypeChecker() bool { return false }
27
+ func (jsdocCheckTagNamesHintRule) Hints(ctx *publicrule.HintContext) []publicrule.Hint {
28
+ if ctx == nil {
29
+ return nil
30
+ }
31
+ if _, ok := ctx.State.(jsdocCheckTagNamesHintState); !ok {
32
+ return nil
33
+ }
34
+ tags := make([]string, 0, len(knownJSDocTags))
35
+ for tag := range knownJSDocTags {
36
+ tags = append(tags, tag)
37
+ }
38
+ sort.Strings(tags)
39
+ hints := make([]publicrule.Hint, 0, len(tags))
40
+ for _, tag := range tags {
41
+ hints = append(hints, publicrule.Hint{
42
+ Insert: tag,
43
+ Detail: jsdocTagHintDetail(tag),
44
+ Trigger: publicrule.HintTrigger{
45
+ Scope: publicrule.HintScopeJSDoc,
46
+ After: "@",
47
+ },
48
+ })
49
+ }
50
+ return hints
51
+ }
52
+
53
+ func jsdocTagHintDetail(tag string) string {
54
+ normalized := strings.ToLower(tag)
55
+ if canonical, alias := jsdocTagSynonyms[normalized]; alias {
56
+ return "alias for @" + canonical
57
+ }
58
+ if _, typed := jsdocTagsWithType[normalized]; typed {
59
+ return "accepts a type"
60
+ }
61
+ if _, empty := emptyJSDocTags[normalized]; empty {
62
+ return "no content"
63
+ }
64
+ return "JSDoc tag"
65
+ }
66
+
14
67
  func (r jsdocLintRule) Name() string { return r.name }
15
68
  func (r jsdocLintRule) Visits() []shimast.Kind {
16
69
  return []shimast.Kind{shimast.KindSourceFile}
@@ -488,6 +541,7 @@ func checkJSDocEmptyTags(ctx *Context, _ string, block parsedJSDocBlock) {
488
541
 
489
542
  func init() {
490
543
  Register(jsdocLintRule{name: "jsdoc/check-tag-names", check: checkJSDocTagNames})
544
+ registerBuiltInProjectCompanion(jsdocCheckTagNamesHintRule{})
491
545
  Register(jsdocLintRule{name: "jsdoc/check-values", check: checkJSDocCheckValues})
492
546
  Register(jsdocLintRule{name: "jsdoc/empty-tags", check: checkJSDocEmptyTags})
493
547
  Register(jsdocLintRule{name: "jsdoc/no-types", check: checkJSDocNoTypes})
@@ -86,8 +86,9 @@ func (noExtraBooleanCast) Check(ctx *Context, node *shimast.Node) {
86
86
  // result, as do explicit parentheses around the cast itself.
87
87
  // - Comments. The splice keeps only the inner expression's text, so a
88
88
  // comment anywhere else in the replaced span (`!Boolean(/* why */ ok)`)
89
- // would be silently deleted. The fix is declined report only — when
90
- // the replaced span contains a comment outside the kept text.
89
+ // would be silently deleted. Imposing that loss is unacceptable, so the
90
+ // same edit is offered as an opt-in suggestion instead: the author sees
91
+ // the title, chooses it, and reviews the result.
91
92
  func reportBooleanCastFix(ctx *Context, node, inner *shimast.Node, message string) {
92
93
  if inner == nil {
93
94
  ctx.Report(node, message)
@@ -105,17 +106,23 @@ func reportBooleanCastFix(ctx *Context, node, inner *shimast.Node, message strin
105
106
  ctx.Report(node, message)
106
107
  return
107
108
  }
108
- if hasCommentBetween(src, editPos, keepStart) ||
109
- hasCommentBetween(src, keepEnd, node.End()) {
110
- ctx.Report(node, message)
111
- return
112
- }
113
109
  text := src[keepStart:keepEnd]
114
110
  if floor, bounded := booleanContextPrecedenceFloor(node); bounded &&
115
111
  shimast.GetExpressionPrecedence(inner) <= floor {
116
112
  text = "(" + text + ")"
117
113
  }
118
- ctx.ReportFix(node, message, TextEdit{Pos: editPos, End: node.End(), Text: text})
114
+ edit := TextEdit{Pos: editPos, End: node.End(), Text: text}
115
+ if hasCommentBetween(src, editPos, keepStart) ||
116
+ hasCommentBetween(src, keepEnd, node.End()) {
117
+ ctx.ReportSuggestion(
118
+ node,
119
+ message,
120
+ "Remove the redundant cast, discarding the comment inside it.",
121
+ edit,
122
+ )
123
+ return
124
+ }
125
+ ctx.ReportFix(node, message, edit)
119
126
  }
120
127
 
121
128
  // booleanContextPrecedenceFloor returns the precedence at or below which a
@@ -239,21 +246,29 @@ func (eqeqeq) Check(ctx *Context, node *shimast.Node) {
239
246
  }
240
247
  }
241
248
 
249
+ // reportEqeqeq reports the loose operator and rewrites it to `replacement`.
250
+ // The rewrite is imposed only when `isEqeqeqAutoFixSafe` proves both operands
251
+ // share a type, because tightening a genuinely mixed-type comparison changes
252
+ // what the program computes. That is a judgement only the author can make, so
253
+ // the same edit is still offered as an opt-in suggestion whose title names the
254
+ // behavior change rather than being discarded.
242
255
  func reportEqeqeq(ctx *Context, operator *shimast.Node, expr *shimast.BinaryExpression, message, replacement string) {
243
256
  pos, end := tokenRange(ctx.File, operator)
244
257
  if pos < 0 {
245
258
  pos, end = operator.Pos(), operator.End()
246
259
  }
260
+ edit := TextEdit{Pos: pos, End: end, Text: replacement}
247
261
  if isEqeqeqAutoFixSafe(expr) {
248
- ctx.ReportRangeFix(
249
- pos,
250
- end,
251
- message,
252
- TextEdit{Pos: pos, End: end, Text: replacement},
253
- )
262
+ ctx.ReportRangeFix(pos, end, message, edit)
254
263
  return
255
264
  }
256
- ctx.ReportRange(pos, end, message)
265
+ ctx.ReportRangeSuggestion(
266
+ pos,
267
+ end,
268
+ message,
269
+ "Replace with `"+replacement+"`, which changes the result when the operands differ in type.",
270
+ edit,
271
+ )
257
272
  }
258
273
 
259
274
  func isEqeqeqAutoFixSafe(expr *shimast.BinaryExpression) bool {
@@ -1,6 +1,10 @@
1
1
  package linthost
2
2
 
3
- import shimast "github.com/microsoft/typescript-go/shim/ast"
3
+ import (
4
+ shimast "github.com/microsoft/typescript-go/shim/ast"
5
+
6
+ publicrule "github.com/samchon/ttsc/packages/lint/rule"
7
+ )
4
8
 
5
9
  // noRedeclare: reject declaring the same binding more than once in the
6
10
  // same scope. The second declaration silently overwrites the first;
@@ -32,8 +36,13 @@ func (noRedeclare) Check(ctx *Context, node *shimast.Node) {
32
36
 
33
37
  report := func(name string, declNode *shimast.Node) {
34
38
  if prior, exists := seen[name]; exists {
35
- _ = prior
36
- ctx.Report(declNode, "'"+name+"' is already defined.")
39
+ priorPos, priorEnd := ctx.nodeFindingRange(prior)
40
+ ctx.ReportRelated(declNode, "'"+name+"' is already defined.",
41
+ publicrule.RelatedInformation{
42
+ Pos: priorPos,
43
+ End: priorEnd,
44
+ Message: "'" + name + "' was first defined here.",
45
+ })
37
46
  return
38
47
  }
39
48
  seen[name] = declNode