@ttsc/lint 0.24.0 → 0.26.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.
@@ -10,9 +10,8 @@ import (
10
10
  // formatParameterProperties breaks a constructor's parameter list onto
11
11
  // one-parameter-per-line when it declares parameter properties, matching
12
12
  // Prettier 3. Prettier forces the break whenever a constructor has more
13
- // than one parameter and at least one carries an accessibility or
14
- // `readonly` modifier (a parameter property), regardless of whether the
15
- // flat form fits printWidth:
13
+ // than one parameter and at least one carries a parameter-property
14
+ // modifier, regardless of whether the flat form fits printWidth:
16
15
  //
17
16
  // constructor(
18
17
  // private readonly repo: Repository,
@@ -155,26 +154,15 @@ func (formatParameterProperties) Check(ctx *Context, node *shimast.Node) {
155
154
  )
156
155
  }
157
156
 
158
- // anyParameterProperty reports whether any parameter carries an
159
- // accessibility (`public`/`private`/`protected`) or `readonly` modifier,
160
- // which makes it a parameter property.
157
+ // anyParameterProperty reports whether any parameter is a parameter property.
158
+ // It defers to the package's shared `isParameterProperty` so one definition
159
+ // answers the question for every rule that asks it; restating the modifier set
160
+ // here is what left `override` out and made this rule abstain on a legal
161
+ // parameter property Prettier breaks.
161
162
  func anyParameterProperty(params []*shimast.Node) bool {
162
163
  for _, p := range params {
163
- if p == nil {
164
- continue
165
- }
166
- mods := p.Modifiers()
167
- if mods == nil {
168
- continue
169
- }
170
- for _, m := range mods.Nodes {
171
- switch m.Kind {
172
- case shimast.KindPublicKeyword,
173
- shimast.KindPrivateKeyword,
174
- shimast.KindProtectedKeyword,
175
- shimast.KindReadonlyKeyword:
176
- return true
177
- }
164
+ if isParameterProperty(p) {
165
+ return true
178
166
  }
179
167
  }
180
168
  return false
@@ -311,7 +311,11 @@ func collectTemplateRanges(file *shimast.SourceFile, src string) []byteRange {
311
311
  return
312
312
  }
313
313
  switch node.Kind {
314
- case shimast.KindNoSubstitutionTemplateLiteral, shimast.KindTemplateExpression:
314
+ case shimast.KindNoSubstitutionTemplateLiteral,
315
+ shimast.KindTemplateExpression,
316
+ // A template literal TYPE spans head plus spans like the expression form
317
+ // and its newlines are equally part of the declared type's text.
318
+ shimast.KindTemplateLiteralType:
315
319
  pos := shimscanner.SkipTrivia(src, node.Pos())
316
320
  end := node.End()
317
321
  if pos >= 0 && end <= len(src) && end > pos {
@@ -143,6 +143,34 @@ type functionalNoTryOptions struct {
143
143
  AllowFinally bool `json:"allowFinally"`
144
144
  }
145
145
 
146
+ // functionalNoMixedTypesOptions gates the two container kinds the rule visits.
147
+ // Both default to true (an absent key leaves the container checked), so the
148
+ // pointers distinguish "not configured" from an explicit false.
149
+ type functionalNoMixedTypesOptions struct {
150
+ CheckInterfaces *bool `json:"checkInterfaces"`
151
+ CheckTypeLiterals *bool `json:"checkTypeLiterals"`
152
+ }
153
+
154
+ // functionalNoReturnVoidOptions narrows what counts as a rejected return.
155
+ //
156
+ // `allowNull` and `allowUndefined` default to true, matching the rule's
157
+ // existing behavior: only a declared `void` return type is rejected. Setting
158
+ // either to false extends the rejection to that declared return type.
159
+ // `ignoreInferredTypes` defaults to false and, when true, spares a bare
160
+ // `return;` whose enclosing function declares no return type, which is the one
161
+ // place the rule reports a void-ness it inferred rather than read.
162
+ type functionalNoReturnVoidOptions struct {
163
+ AllowNull *bool `json:"allowNull"`
164
+ AllowUndefined *bool `json:"allowUndefined"`
165
+ IgnoreInferredTypes bool `json:"ignoreInferredTypes"`
166
+ }
167
+
168
+ // functionalPreferTacitOptions gates the member-expression callee shape.
169
+ // Defaults to true, keeping `x => service.handler(x)` reported as before.
170
+ type functionalPreferTacitOptions struct {
171
+ CheckMemberExpressions *bool `json:"checkMemberExpressions"`
172
+ }
173
+
146
174
  type functionalImmutableDataOptions struct {
147
175
  functionalPatternOptions
148
176
  IgnoreMapsAndSets bool `json:"ignoreMapsAndSets"`
@@ -159,8 +187,15 @@ type functionalPreferImmutableTypesOptions struct {
159
187
  functionalPatternOptions
160
188
  }
161
189
 
190
+ // functionalPreferReadonlyTypeOptions narrows which positions the rule polices.
191
+ // Both gates default to off, so an absent key leaves every visited position
192
+ // checked exactly as before.
162
193
  type functionalPreferReadonlyTypeOptions struct {
163
194
  functionalPatternOptions
195
+ AllowMutableReturnType bool `json:"allowMutableReturnType"`
196
+ IgnoreClass interface{} `json:"ignoreClass"`
197
+ IgnoreCollections bool `json:"ignoreCollections"`
198
+ IgnoreInterface bool `json:"ignoreInterface"`
164
199
  }
165
200
 
166
201
  type functionalReadonlyTypeOptions struct {
@@ -332,6 +367,18 @@ func (functionalNoLoopStatements) Check(ctx *Context, node *shimast.Node) {
332
367
  }
333
368
 
334
369
  func (functionalNoMixedTypes) Check(ctx *Context, node *shimast.Node) {
370
+ var opts functionalNoMixedTypesOptions
371
+ _ = ctx.DecodeOptions(&opts)
372
+ switch node.Kind {
373
+ case shimast.KindInterfaceDeclaration:
374
+ if opts.CheckInterfaces != nil && !*opts.CheckInterfaces {
375
+ return
376
+ }
377
+ case shimast.KindTypeLiteral:
378
+ if opts.CheckTypeLiterals != nil && !*opts.CheckTypeLiterals {
379
+ return
380
+ }
381
+ }
335
382
  members := containerMembers(node)
336
383
  if len(members) < 2 {
337
384
  return
@@ -361,16 +408,51 @@ func (functionalNoPromiseReject) Check(ctx *Context, node *shimast.Node) {
361
408
  }
362
409
 
363
410
  func (functionalNoReturnVoid) Check(ctx *Context, node *shimast.Node) {
411
+ var opts functionalNoReturnVoidOptions
412
+ _ = ctx.DecodeOptions(&opts)
364
413
  if node.Kind == shimast.KindReturnStatement {
365
414
  ret := node.AsReturnStatement()
366
- if ret != nil && ret.Expression == nil {
367
- ctx.Report(node, "Function must return a value.")
415
+ if ret == nil || ret.Expression != nil {
416
+ return
417
+ }
418
+ if opts.IgnoreInferredTypes && functionalEnclosingReturnTypeText(ctx, node) == "" {
419
+ return
368
420
  }
421
+ ctx.Report(node, "Function must return a value.")
369
422
  return
370
423
  }
371
- if functionalReturnTypeText(ctx, node) == "void" {
424
+ switch functionalReturnTypeText(ctx, node) {
425
+ case "void":
372
426
  ctx.Report(node, "Function must return a value.")
427
+ case "null":
428
+ if opts.AllowNull != nil && !*opts.AllowNull {
429
+ ctx.Report(node, "Function must return a value.")
430
+ }
431
+ case "undefined":
432
+ if opts.AllowUndefined != nil && !*opts.AllowUndefined {
433
+ ctx.Report(node, "Function must return a value.")
434
+ }
435
+ }
436
+ }
437
+
438
+ // functionalEnclosingReturnTypeText returns the declared return type text of the
439
+ // nearest function-like ancestor of `node`, or "" when that function declares
440
+ // none. A bare `return;` there is the only place the rule reports a void-ness it
441
+ // inferred instead of reading, which is what `ignoreInferredTypes` spares.
442
+ //
443
+ // The walk stops at the nearest function-like of ANY kind. Skipping a
444
+ // constructor or a set accessor, neither of which may annotate a return type,
445
+ // would attribute an enclosing function's annotation to a `return;` that has
446
+ // nothing to do with it. A get accessor may annotate one, and
447
+ // functionalReturnTypeText reads it.
448
+ func functionalEnclosingReturnTypeText(ctx *Context, node *shimast.Node) string {
449
+ for parent := node.Parent; parent != nil; parent = parent.Parent {
450
+ if !isFunctionLikeKind(parent) {
451
+ continue
452
+ }
453
+ return functionalReturnTypeText(ctx, parent)
373
454
  }
455
+ return ""
374
456
  }
375
457
 
376
458
  func (functionalNoThisExpressions) Check(ctx *Context, node *shimast.Node) {
@@ -418,6 +500,20 @@ func (functionalPreferPropertySignatures) Check(ctx *Context, node *shimast.Node
418
500
  func (functionalPreferReadonlyType) Check(ctx *Context, node *shimast.Node) {
419
501
  var opts functionalPreferReadonlyTypeOptions
420
502
  _ = ctx.DecodeOptions(&opts)
503
+ if opts.IgnoreInterface && hasAncestor(node, func(ancestor *shimast.Node) bool {
504
+ return ancestor.Kind == shimast.KindInterfaceDeclaration
505
+ }) {
506
+ return
507
+ }
508
+ if functionalIgnoreClassSkips(opts.IgnoreClass, node) {
509
+ return
510
+ }
511
+ if opts.AllowMutableReturnType && isFunctionLikeReturnTypePosition(node) {
512
+ return
513
+ }
514
+ if opts.IgnoreCollections && isFunctionalCollectionTypeNode(node) {
515
+ return
516
+ }
421
517
  switch node.Kind {
422
518
  case shimast.KindArrayType:
423
519
  if !isReadonlyTypeNode(node) {
@@ -451,10 +547,33 @@ func (functionalPreferReadonlyType) Check(ctx *Context, node *shimast.Node) {
451
547
  }
452
548
 
453
549
  func (functionalPreferTacit) Check(ctx *Context, node *shimast.Node) {
550
+ var opts functionalPreferTacitOptions
551
+ _ = ctx.DecodeOptions(&opts)
454
552
  text := compactFunctionalWhitespace(nodeText(ctx.File, node))
455
- if isTacitWrapperText(text) {
456
- ctx.Report(node, "Potentially unnecessary function wrapper.")
553
+ if !isTacitWrapperText(text) {
554
+ return
555
+ }
556
+ if opts.CheckMemberExpressions != nil && !*opts.CheckMemberExpressions &&
557
+ isTacitWrapperMemberCallee(text) {
558
+ return
457
559
  }
560
+ ctx.Report(node, "Potentially unnecessary function wrapper.")
561
+ }
562
+
563
+ // isTacitWrapperMemberCallee reports whether the wrapper's callee is a member
564
+ // expression (`service.handler`) rather than a bare identifier. `text` has
565
+ // already satisfied isTacitWrapperText, so the split and the `(` are present.
566
+ func isTacitWrapperMemberCallee(text string) bool {
567
+ parts := strings.Split(text, "=>")
568
+ if len(parts) != 2 {
569
+ return false
570
+ }
571
+ call := parts[1]
572
+ open := strings.LastIndex(call, "(")
573
+ if open <= 0 {
574
+ return false
575
+ }
576
+ return strings.Contains(call[:open], ".")
458
577
  }
459
578
 
460
579
  func (functionalReadonlyType) Check(ctx *Context, node *shimast.Node) {
@@ -595,27 +714,12 @@ func isDeclarationName(node *shimast.Node) bool {
595
714
  }
596
715
  }
597
716
 
717
+ // functionalReturnTypeText returns the declared return-type text of a
718
+ // signature-bearing node, or "" when it declares none. It reads the same
719
+ // signature table isFunctionLikeReturnTypePosition uses, so a get accessor is
720
+ // not mistaken for a declaration that cannot annotate its return type.
598
721
  func functionalReturnTypeText(ctx *Context, node *shimast.Node) string {
599
- var typeNode *shimast.Node
600
- switch node.Kind {
601
- case shimast.KindFunctionDeclaration:
602
- if decl := node.AsFunctionDeclaration(); decl != nil {
603
- typeNode = decl.Type
604
- }
605
- case shimast.KindFunctionExpression:
606
- if decl := node.AsFunctionExpression(); decl != nil {
607
- typeNode = decl.Type
608
- }
609
- case shimast.KindArrowFunction:
610
- if decl := node.AsArrowFunction(); decl != nil {
611
- typeNode = decl.Type
612
- }
613
- case shimast.KindMethodDeclaration:
614
- if decl := node.AsMethodDeclaration(); decl != nil {
615
- typeNode = decl.Type
616
- }
617
- }
618
- return strings.TrimSpace(nodeText(ctx.File, typeNode))
722
+ return strings.TrimSpace(nodeText(ctx.File, signatureReturnTypeNode(node)))
619
723
  }
620
724
 
621
725
  func functionalFunctionLikeName(node *shimast.Node) string {
@@ -757,6 +861,125 @@ func nilSafeFile(node *shimast.Node) *shimast.SourceFile {
757
861
  return nil
758
862
  }
759
863
 
864
+ // isFunctionLikeReturnTypePosition reports whether `node` is the return-type
865
+ // annotation of any signature-bearing declaration, or sits inside one. The rule
866
+ // visits type nodes, so a mutable array nested in the return type is the same
867
+ // position as the return type itself.
868
+ //
869
+ // Every kind that can carry a return-type annotation is listed: leaving call
870
+ // signatures, construct signatures, constructor types, or a get accessor out
871
+ // would let the same option answer differently for the same position depending
872
+ // on how the signature was spelled.
873
+ func isFunctionLikeReturnTypePosition(node *shimast.Node) bool {
874
+ for current := node; current != nil && current.Parent != nil; current = current.Parent {
875
+ typeNode := signatureReturnTypeNode(current.Parent)
876
+ if typeNode != nil && typeNode == current {
877
+ return true
878
+ }
879
+ }
880
+ return false
881
+ }
882
+
883
+ // signatureReturnTypeNode returns the declared return-type node of `node`, or
884
+ // nil when `node` carries no signature or declares no return type.
885
+ //
886
+ // Do not replace this table with typescript-go's own FunctionLikeData().Type:
887
+ // an index signature embeds the same base, so that accessor also returns the
888
+ // value type of `{ [k: string]: string[] }`, which is not a return type. The
889
+ // three kinds this table omits, a constructor, a set accessor, and an index
890
+ // signature, are exactly the ones whose Type field means something else.
891
+ func signatureReturnTypeNode(node *shimast.Node) *shimast.Node {
892
+ switch node.Kind {
893
+ case shimast.KindFunctionDeclaration:
894
+ if decl := node.AsFunctionDeclaration(); decl != nil {
895
+ return decl.Type
896
+ }
897
+ case shimast.KindFunctionExpression:
898
+ if decl := node.AsFunctionExpression(); decl != nil {
899
+ return decl.Type
900
+ }
901
+ case shimast.KindArrowFunction:
902
+ if decl := node.AsArrowFunction(); decl != nil {
903
+ return decl.Type
904
+ }
905
+ case shimast.KindMethodDeclaration:
906
+ if decl := node.AsMethodDeclaration(); decl != nil {
907
+ return decl.Type
908
+ }
909
+ case shimast.KindMethodSignature:
910
+ if decl := node.AsMethodSignatureDeclaration(); decl != nil {
911
+ return decl.Type
912
+ }
913
+ case shimast.KindGetAccessor:
914
+ if decl := node.AsGetAccessorDeclaration(); decl != nil {
915
+ return decl.Type
916
+ }
917
+ case shimast.KindFunctionType:
918
+ if decl := node.AsFunctionTypeNode(); decl != nil {
919
+ return decl.Type
920
+ }
921
+ case shimast.KindConstructorType:
922
+ if decl := node.AsConstructorTypeNode(); decl != nil {
923
+ return decl.Type
924
+ }
925
+ case shimast.KindCallSignature:
926
+ if decl := node.AsCallSignatureDeclaration(); decl != nil {
927
+ return decl.Type
928
+ }
929
+ case shimast.KindConstructSignature:
930
+ if decl := node.AsConstructSignatureDeclaration(); decl != nil {
931
+ return decl.Type
932
+ }
933
+ }
934
+ return nil
935
+ }
936
+
937
+ // isFunctionalClassMemberPosition reports whether `node` sits inside a class,
938
+ // and whether that position is a field. `ignoreClass: true` skips anything
939
+ // under a class, its heritage clause and type parameters included, the same
940
+ // ancestor test `ignoreInterface` applies. `"fieldsOnly"` narrows that to field
941
+ // declarations, leaving methods, accessors, and constructor parameters checked.
942
+ func isFunctionalClassMemberPosition(node *shimast.Node) (inClass bool, inField bool) {
943
+ for parent := node.Parent; parent != nil; parent = parent.Parent {
944
+ switch parent.Kind {
945
+ case shimast.KindPropertyDeclaration:
946
+ inField = true
947
+ case shimast.KindClassDeclaration, shimast.KindClassExpression:
948
+ return true, inField
949
+ }
950
+ }
951
+ return false, false
952
+ }
953
+
954
+ // functionalIgnoreClassSkips resolves the `boolean | "fieldsOnly"` option
955
+ // against a visited node's class position.
956
+ func functionalIgnoreClassSkips(option interface{}, node *shimast.Node) bool {
957
+ if option == nil || option == false {
958
+ return false
959
+ }
960
+ inClass, inField := isFunctionalClassMemberPosition(node)
961
+ if !inClass {
962
+ return false
963
+ }
964
+ if option == "fieldsOnly" {
965
+ return inField
966
+ }
967
+ return option == true
968
+ }
969
+
970
+ // isFunctionalCollectionTypeNode reports whether `node` is an array, tuple, or
971
+ // mutable built-in collection reference, the set `ignoreCollections` spares.
972
+ func isFunctionalCollectionTypeNode(node *shimast.Node) bool {
973
+ switch node.Kind {
974
+ case shimast.KindArrayType, shimast.KindTupleType:
975
+ return true
976
+ case shimast.KindTypeReference:
977
+ return isMutableTypeReference(node)
978
+ default:
979
+ return false
980
+ }
981
+ }
982
+
760
983
  func isMutableTypeReference(node *shimast.Node) bool {
761
984
  ref := node.AsTypeReferenceNode()
762
985
  if ref == nil || ref.TypeName == nil {
@@ -317,12 +317,14 @@ func parameterPropertyName(param *shimast.Node) (string, bool) {
317
317
  return name, name != ""
318
318
  }
319
319
 
320
+ // isParameterProperty is the package's shared parameter-property predicate.
321
+ // The compiler's own mask is the source: a restated keyword list is what left
322
+ // `override` out of format/parameter-properties (#1131), and this predicate
323
+ // feeds six rules, so a restatement here would be five more chances at the same
324
+ // drift.
320
325
  func isParameterProperty(param *shimast.Node) bool {
321
- return hasModifier(param, shimast.KindPublicKeyword) ||
322
- hasModifier(param, shimast.KindPrivateKeyword) ||
323
- hasModifier(param, shimast.KindProtectedKeyword) ||
324
- hasModifier(param, shimast.KindReadonlyKeyword) ||
325
- hasModifier(param, shimast.KindOverrideKeyword)
326
+ return param != nil &&
327
+ param.ModifierFlags()&shimast.ModifierFlagsParameterPropertyModifier != 0
326
328
  }
327
329
 
328
330
  func thisPropertyAssignment(stmt *shimast.Node) (property string, value string, ok bool) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/lint",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "Reference ttsc plugin: ESLint-style lint rules over the TypeScript-Go Program used by the type-check pass.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -37,7 +37,7 @@
37
37
  "@types/node": "^25.3.0",
38
38
  "rimraf": "^6.1.2",
39
39
  "typescript": "^7.0.2",
40
- "ttsc": "0.24.0"
40
+ "ttsc": "0.26.0"
41
41
  },
42
42
  "repository": {
43
43
  "type": "git",
package/rule/rule.go CHANGED
@@ -262,15 +262,27 @@ type RelatedInformation struct {
262
262
  // be replaced as a whole.
263
263
  //
264
264
  // Application policy: a rule may emit several `TextEdit`s in one
265
- // `ReportFix` / `ReportRangeFix` call, in any order. The host treats the
266
- // per-pass edit set as a candidate list. Within a single fix pass, edits
267
- // must not overlap each other; when two edits cover overlapping ranges
268
- // (either from one rule emitting multiple edits in one call, or from two
269
- // different rules in the same pass), the host applies the earliest-starting
270
- // / shortest edit and silently drops the rest. There is no diagnostic for
271
- // dropped edits, and the host does not currently report when a comment
272
- // falls inside a deletion range. Design fixes so each finding emits one
273
- // contiguous TextEdit covering the entire replacement region.
265
+ // `ReportFix` / `ReportRangeFix` call, in any order. The unit the host
266
+ // resolves conflicts on is the FINDING, not the individual edit. Within one
267
+ // fix pass the host considers each finding's edits as one group, earliest
268
+ // group first, and accepts a group only when every member coexists with the
269
+ // edits already accepted. If any member would be dropped, the whole group is
270
+ // skipped, so a multi-edit fix never half-applies. The skipped finding is not
271
+ // lost: the next cascade pass re-runs the rule against the rewritten source
272
+ // and the fix applies then, or the cascade converges without it.
273
+ //
274
+ // A finding's own edits must therefore not overlap each other either, or the
275
+ // finding can never apply. Exact duplicates within one finding are collapsed
276
+ // rather than treated as a conflict, so repeating an identical edit is
277
+ // harmless. Nothing diagnoses a skipped group, and the host does not report
278
+ // when a comment falls inside a deletion range.
279
+ //
280
+ // Emit the narrowest edits that express the rewrite. Several small
281
+ // non-overlapping edits contend for less source than one wide replacement and
282
+ // are the shape the atomic applier exists to support. Built-ins that ship
283
+ // multi-edit fixes include `typescript/no-import-type-side-effects`,
284
+ // `format/whitespace`, `format/indent`, `unicorn/prevent-abbreviations`, and
285
+ // `unicorn/template-indent`.
274
286
  type TextEdit struct {
275
287
  Pos int
276
288
  End int