@ttsc/lint 0.25.0 → 0.26.1

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.25.0",
3
+ "version": "0.26.1",
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.25.0"
40
+ "ttsc": "0.26.1"
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
package/src/index.ts CHANGED
@@ -1789,8 +1789,9 @@ function evaluateTtsxConfigPlugins(
1789
1789
  "--no-plugins",
1790
1790
  loaderPath,
1791
1791
  ];
1792
- if (process.env.TTSC_TSGO_BINARY) {
1793
- args.unshift("--binary", process.env.TTSC_TSGO_BINARY);
1792
+ const tsgoBinary = resolveConfigTsgo(configPath, context);
1793
+ if (tsgoBinary) {
1794
+ args.unshift("--binary", tsgoBinary);
1794
1795
  }
1795
1796
  const env = {
1796
1797
  ...nodeConfigLoaderEnv(configPath),
@@ -2453,6 +2454,73 @@ function nodeConfigLoaderEnv(configPath: string): NodeJS.ProcessEnv {
2453
2454
  * and is not for a host that loaded this descriptor in process. The bare name
2454
2455
  * remains the last resort for an installation none of them reach.
2455
2456
  */
2457
+ /**
2458
+ * The native TypeScript compiler the config evaluator must run.
2459
+ *
2460
+ * The evaluator builds the loader in an ephemeral directory, so the child it
2461
+ * spawns cannot discover `typescript` the way an ordinary invocation does: the
2462
+ * directory is not in the project, and `linkNearestNodeModules` is the only
2463
+ * thing that puts the project's modules within reach of it. Leaving the child
2464
+ * to re-derive the compiler from there made this work by inheritance —
2465
+ * `TTSC_TSGO_BINARY` is exported by `ttsx` to its own descendants, so a build
2466
+ * launched under `ttsx` passed a binary down and a build launched any other way
2467
+ * did not. A published consumer exports no such variable, and neither does a
2468
+ * process that deliberately sheds the launcher's runtime state, and for those
2469
+ * the evaluator failed with `ttsc: typescript is required` before it read a
2470
+ * line of the config.
2471
+ *
2472
+ * The host has already resolved a compiler for the project it is linting, so
2473
+ * the answer is asked of the project rather than of the environment. The config
2474
+ * comes first and the descriptor's own location second, the same order and for
2475
+ * the same reason as {@link resolveTtsxLauncher}. An explicit `TTSC_TSGO_BINARY`
2476
+ * still wins, so an embedder that pins a compiler keeps pinning it.
2477
+ *
2478
+ * Returning `undefined` leaves the child to resolve for itself, which is what
2479
+ * it did before: a project that cannot answer here could not answer there
2480
+ * either, and the child's own diagnostic is the one that names the missing
2481
+ * package.
2482
+ */
2483
+ function resolveConfigTsgo(
2484
+ configPath: string,
2485
+ context: TtscPluginFactoryContext<ITtscLintPluginConfig>,
2486
+ ): string | undefined {
2487
+ const explicit = process.env.TTSC_TSGO_BINARY?.trim();
2488
+ if (explicit) return explicit;
2489
+ const anchors = [
2490
+ configPath,
2491
+ path.join(context.projectRoot, "package.json"),
2492
+ context.dirname,
2493
+ ];
2494
+ for (const anchor of anchors) {
2495
+ const binary = tsgoBinaryFrom(anchor);
2496
+ if (binary !== undefined) return binary;
2497
+ }
2498
+ return undefined;
2499
+ }
2500
+
2501
+ /**
2502
+ * The platform compiler binary of the `typescript` install `anchor` can see, or
2503
+ * `undefined` when this anchor reaches neither the package nor its platform
2504
+ * dependency. Mirrors ttsc's own resolution order so both name one file.
2505
+ */
2506
+ function tsgoBinaryFrom(anchor: string): string | undefined {
2507
+ try {
2508
+ const manifest = createRequire(anchor).resolve("typescript/package.json");
2509
+ const platform = createRequire(manifest).resolve(
2510
+ `@typescript/typescript-${process.platform}-${process.arch}/package.json`,
2511
+ );
2512
+ const binary = path.join(
2513
+ path.dirname(platform),
2514
+ "lib",
2515
+ process.platform === "win32" ? "tsc.exe" : "tsc",
2516
+ );
2517
+ return fs.existsSync(binary) ? binary : undefined;
2518
+ } catch {
2519
+ // This anchor cannot see the compiler; the caller tries the next one.
2520
+ return undefined;
2521
+ }
2522
+ }
2523
+
2456
2524
  function resolveTtsxLauncher(anchors: readonly string[]): string {
2457
2525
  const explicit = process.env.TTSC_TTSX_BINARY?.trim();
2458
2526
  if (explicit) return explicit;