@ttsc/lint 0.28.0 → 0.28.2
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/linthost/compile.go +60 -26
- package/linthost/config.go +86 -13
- package/package.json +2 -2
package/linthost/compile.go
CHANGED
|
@@ -404,8 +404,8 @@ type residentRuleCache struct {
|
|
|
404
404
|
mu sync.Mutex
|
|
405
405
|
key string
|
|
406
406
|
resolver RuleResolver
|
|
407
|
-
configs
|
|
408
|
-
// loads counts the
|
|
407
|
+
configs *residentRuleConfigSnapshot
|
|
408
|
+
// loads counts the resolver loads this memo did not avoid.
|
|
409
409
|
//
|
|
410
410
|
// It exists because a reuse is otherwise unobservable: a RuleResolver holds
|
|
411
411
|
// maps, so it is not a comparable type and two of them cannot be asked
|
|
@@ -414,14 +414,29 @@ type residentRuleCache struct {
|
|
|
414
414
|
loads int
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
+
// residentRuleConfigState is the resolver-owned description of every input a
|
|
418
|
+
// resident answer depends on. Files are native JSON configs; dependencies are
|
|
419
|
+
// the executable loader's full fingerprints, including cache-only package
|
|
420
|
+
// files that deliberately stay out of ConfigPaths and external watch lists.
|
|
421
|
+
type residentRuleConfigState struct {
|
|
422
|
+
dependencies []configDependencyFingerprint
|
|
423
|
+
files []string
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
type residentRuleConfigSnapshot struct {
|
|
427
|
+
dependencies []configDependencyFingerprint
|
|
428
|
+
files map[string][sha256.Size]byte
|
|
429
|
+
}
|
|
430
|
+
|
|
417
431
|
// acquireRules returns the loaded rule configuration, reusing the daemon's memo
|
|
418
|
-
// while the
|
|
432
|
+
// while the complete state it was loaded from is unchanged.
|
|
419
433
|
//
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
434
|
+
// Native JSON configs are validated against their files. Executable configs
|
|
435
|
+
// retain the loader's complete dependency fingerprints, including missing
|
|
436
|
+
// resolution candidates and cache-only package files. The latter must remain
|
|
437
|
+
// outside public project watch lists without becoming invisible to the resident
|
|
438
|
+
// memo: an imported package edit can change the resolved rules just as surely
|
|
439
|
+
// as an edit to lint.config.ts itself.
|
|
425
440
|
func acquireRules(pluginsJSON, cwd, tsconfigPath string) (RuleResolver, error) {
|
|
426
441
|
cache := residentRules
|
|
427
442
|
if cache == nil {
|
|
@@ -457,16 +472,20 @@ func acquireRules(pluginsJSON, cwd, tsconfigPath string) (RuleResolver, error) {
|
|
|
457
472
|
return resolver, nil
|
|
458
473
|
}
|
|
459
474
|
|
|
460
|
-
// hashRuleConfigs records the
|
|
461
|
-
//
|
|
462
|
-
//
|
|
475
|
+
// hashRuleConfigs records the complete state the resolver was built from. JSON
|
|
476
|
+
// configs contribute file digests. Executable configs contribute the loader's
|
|
477
|
+
// full dependency fingerprints, including missing resolution candidates,
|
|
478
|
+
// directories, and cache-only package files that are not public watch inputs.
|
|
479
|
+
// A resolver naming no state records nothing, and a memo with no proof is never
|
|
480
|
+
// reused.
|
|
463
481
|
//
|
|
464
|
-
// A file written after the load began is recorded as nothing at
|
|
465
|
-
// bytes the resolver was built from is unknowable once an edit lands
|
|
466
|
-
// evaluation window, and recording the ones readable now is the one
|
|
467
|
-
// answer that never corrects itself: the memo would agree with a file the
|
|
482
|
+
// A native JSON file written after the load began is recorded as nothing at
|
|
483
|
+
// all. Which bytes the resolver was built from is unknowable once an edit lands
|
|
484
|
+
// inside the evaluation window, and recording the ones readable now is the one
|
|
485
|
+
// wrong answer that never corrects itself: the memo would agree with a file the
|
|
468
486
|
// resolver does not match, and every later request would pass the reuse test
|
|
469
|
-
// until some further edit happened to disagree.
|
|
487
|
+
// until some further edit happened to disagree. Executable configs carry the
|
|
488
|
+
// loader's own pre/post dependency proof instead. Declining costs one reload.
|
|
470
489
|
//
|
|
471
490
|
// After, and not "not before". A filesystem timestamp and the instant the load
|
|
472
491
|
// began are read from the same clock, whose tick is coarse on Windows, so the
|
|
@@ -480,13 +499,25 @@ func acquireRules(pluginsJSON, cwd, tsconfigPath string) (RuleResolver, error) {
|
|
|
480
499
|
// Read first and stated second, in that order. A write landing between the two
|
|
481
500
|
// moves the modification time forward and is caught; stating it first would
|
|
482
501
|
// leave the window the check exists to close.
|
|
483
|
-
func hashRuleConfigs(resolver RuleResolver, started time.Time)
|
|
484
|
-
|
|
502
|
+
func hashRuleConfigs(resolver RuleResolver, started time.Time) *residentRuleConfigSnapshot {
|
|
503
|
+
if configCacheDisabled() {
|
|
504
|
+
return nil
|
|
505
|
+
}
|
|
506
|
+
source, ok := resolver.(interface {
|
|
507
|
+
residentRuleConfigState() residentRuleConfigState
|
|
508
|
+
})
|
|
485
509
|
if !ok {
|
|
486
510
|
return nil
|
|
487
511
|
}
|
|
488
|
-
|
|
489
|
-
|
|
512
|
+
state := source.residentRuleConfigState()
|
|
513
|
+
if len(state.files) == 0 && len(state.dependencies) == 0 {
|
|
514
|
+
return nil
|
|
515
|
+
}
|
|
516
|
+
if !configDependencyDigestsAreCurrent(state.dependencies) {
|
|
517
|
+
return nil
|
|
518
|
+
}
|
|
519
|
+
configs := make(map[string][sha256.Size]byte, len(state.files))
|
|
520
|
+
for _, location := range state.files {
|
|
490
521
|
contents, err := os.ReadFile(location)
|
|
491
522
|
if err != nil {
|
|
492
523
|
return nil
|
|
@@ -497,17 +528,20 @@ func hashRuleConfigs(resolver RuleResolver, started time.Time) map[string][sha25
|
|
|
497
528
|
}
|
|
498
529
|
configs[location] = sha256.Sum256(contents)
|
|
499
530
|
}
|
|
500
|
-
|
|
501
|
-
|
|
531
|
+
return &residentRuleConfigSnapshot{
|
|
532
|
+
dependencies: state.dependencies,
|
|
533
|
+
files: configs,
|
|
502
534
|
}
|
|
503
|
-
return configs
|
|
504
535
|
}
|
|
505
536
|
|
|
506
|
-
func ruleConfigsUnchanged(configs
|
|
507
|
-
if
|
|
537
|
+
func ruleConfigsUnchanged(configs *residentRuleConfigSnapshot) bool {
|
|
538
|
+
if configCacheDisabled() ||
|
|
539
|
+
configs == nil ||
|
|
540
|
+
(len(configs.files) == 0 && len(configs.dependencies) == 0) ||
|
|
541
|
+
!configDependencyDigestsAreCurrent(configs.dependencies) {
|
|
508
542
|
return false
|
|
509
543
|
}
|
|
510
|
-
for location, recorded := range configs {
|
|
544
|
+
for location, recorded := range configs.files {
|
|
511
545
|
contents, err := os.ReadFile(location)
|
|
512
546
|
if err != nil || sha256.Sum256(contents) != recorded {
|
|
513
547
|
return false
|
package/linthost/config.go
CHANGED
|
@@ -226,6 +226,16 @@ func (r boundProjectRuleResolver) ConfigDirectories() []string {
|
|
|
226
226
|
return resolver.ConfigDirectories()
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
func (r boundProjectRuleResolver) residentRuleConfigState() residentRuleConfigState {
|
|
230
|
+
resolver, ok := r.RuleResolver.(interface {
|
|
231
|
+
residentRuleConfigState() residentRuleConfigState
|
|
232
|
+
})
|
|
233
|
+
if !ok {
|
|
234
|
+
return residentRuleConfigState{}
|
|
235
|
+
}
|
|
236
|
+
return resolver.residentRuleConfigState()
|
|
237
|
+
}
|
|
238
|
+
|
|
229
239
|
// ResolveRules implements RuleResolver. A flat RuleConfig has no glob scoping,
|
|
230
240
|
// so every file receives the full map unchanged.
|
|
231
241
|
func (c RuleConfig) ResolveRules(string) ResolvedRuleConfig {
|
|
@@ -361,10 +371,12 @@ func (r InlineRuleResolver) ResolveProjectRules(names []string) (map[string]Proj
|
|
|
361
371
|
// ConfigEntry per file, the extends-target entries declared before the
|
|
362
372
|
// extending file's own entry so local rules win on collision.
|
|
363
373
|
type ConfigStore struct {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
374
|
+
cacheDependencies []configDependencyFingerprint
|
|
375
|
+
cacheFiles []string
|
|
376
|
+
directories []string
|
|
377
|
+
entries []ConfigEntry
|
|
378
|
+
paths []string
|
|
379
|
+
resolutionRoot string
|
|
368
380
|
}
|
|
369
381
|
|
|
370
382
|
// ConfigPaths returns the config and extends files that produced this store.
|
|
@@ -387,6 +399,26 @@ func (s *ConfigStore) ConfigDirectories() []string {
|
|
|
387
399
|
return append([]string(nil), s.directories...)
|
|
388
400
|
}
|
|
389
401
|
|
|
402
|
+
// residentRuleConfigState returns the private cache proof for the configuration
|
|
403
|
+
// that produced this store. Executable configs retain their complete dependency
|
|
404
|
+
// fingerprints, including package implementation files that must invalidate a
|
|
405
|
+
// resident answer without becoming public project-watch inputs. JSON configs
|
|
406
|
+
// retain only their own files because they have no executable module graph.
|
|
407
|
+
func (s *ConfigStore) residentRuleConfigState() residentRuleConfigState {
|
|
408
|
+
if s == nil {
|
|
409
|
+
return residentRuleConfigState{}
|
|
410
|
+
}
|
|
411
|
+
dependencies := make([]configDependencyFingerprint, len(s.cacheDependencies))
|
|
412
|
+
for index, dependency := range s.cacheDependencies {
|
|
413
|
+
dependencies[index] = dependency
|
|
414
|
+
dependencies[index].Realpath = cloneConfigDependencyRealpath(dependency.Realpath)
|
|
415
|
+
}
|
|
416
|
+
return residentRuleConfigState{
|
|
417
|
+
dependencies: dependencies,
|
|
418
|
+
files: append([]string(nil), s.cacheFiles...),
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
390
422
|
// RuleOptions implements the file-agnostic RuleResolver compatibility method.
|
|
391
423
|
// Engine execution does not use this representative value: ResolveRules
|
|
392
424
|
// carries the matching file's options. Callers that only understand the older
|
|
@@ -840,6 +872,7 @@ func collectConfigObject(store *ConfigStore, raw any, baseDir, path string, chai
|
|
|
840
872
|
}
|
|
841
873
|
appendConfigPaths(store, evaluated.dependencies)
|
|
842
874
|
appendConfigDirectories(store, evaluated.dependencyDirectories)
|
|
875
|
+
appendResidentConfigEvaluation(store, location, evaluated)
|
|
843
876
|
if err := collectConfigObject(store, evaluated.value, filepath.Dir(location), path+".extends", extendedChain); err != nil {
|
|
844
877
|
return err
|
|
845
878
|
}
|
|
@@ -1102,9 +1135,30 @@ func loadConfigResolver(
|
|
|
1102
1135
|
}
|
|
1103
1136
|
appendConfigPaths(store, evaluated.dependencies)
|
|
1104
1137
|
appendConfigDirectories(store, evaluated.dependencyDirectories)
|
|
1138
|
+
appendResidentConfigEvaluation(store, location, evaluated)
|
|
1105
1139
|
return store, nil
|
|
1106
1140
|
}
|
|
1107
1141
|
|
|
1142
|
+
func appendResidentConfigEvaluation(
|
|
1143
|
+
store *ConfigStore,
|
|
1144
|
+
location string,
|
|
1145
|
+
evaluated evaluatedConfigFile,
|
|
1146
|
+
) {
|
|
1147
|
+
if evaluated.dependenciesTracked {
|
|
1148
|
+
for _, dependency := range evaluated.dependencyDigests {
|
|
1149
|
+
cloned := dependency
|
|
1150
|
+
cloned.Realpath = cloneConfigDependencyRealpath(dependency.Realpath)
|
|
1151
|
+
store.cacheDependencies = append(store.cacheDependencies, cloned)
|
|
1152
|
+
}
|
|
1153
|
+
return
|
|
1154
|
+
}
|
|
1155
|
+
location = filepath.Clean(location)
|
|
1156
|
+
if !containsPath(store.cacheFiles, location) {
|
|
1157
|
+
store.cacheFiles = append(store.cacheFiles, location)
|
|
1158
|
+
sort.Strings(store.cacheFiles)
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1108
1162
|
func appendConfigPaths(store *ConfigStore, paths []string) {
|
|
1109
1163
|
for _, location := range paths {
|
|
1110
1164
|
location = filepath.Clean(location)
|
|
@@ -1602,17 +1656,28 @@ func configDependencyDigest(
|
|
|
1602
1656
|
if err != nil {
|
|
1603
1657
|
return "", err
|
|
1604
1658
|
}
|
|
1605
|
-
|
|
1606
|
-
|
|
1659
|
+
target := ""
|
|
1660
|
+
entryPath := filepath.Join(dependency.Path, entry.Name())
|
|
1661
|
+
if linked, linkErr := os.Readlink(entryPath); linkErr == nil {
|
|
1662
|
+
// Node reports Windows junctions as symbolic links even though Go's
|
|
1663
|
+
// FileMode does not carry ModeSymlink for the same reparse point. The
|
|
1664
|
+
// loader writes Node's classification, so validation must ask the link
|
|
1665
|
+
// itself before falling back to FileMode or an unchanged junction will
|
|
1666
|
+
// make every cache lookup miss.
|
|
1607
1667
|
kind = "symlink"
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1668
|
+
target = linked
|
|
1669
|
+
} else {
|
|
1670
|
+
switch {
|
|
1671
|
+
case info.Mode()&os.ModeSymlink != 0:
|
|
1672
|
+
kind = "symlink"
|
|
1673
|
+
case info.IsDir():
|
|
1674
|
+
kind = "directory"
|
|
1675
|
+
case info.Mode().IsRegular():
|
|
1676
|
+
kind = "file"
|
|
1677
|
+
}
|
|
1612
1678
|
}
|
|
1613
|
-
target
|
|
1614
|
-
|
|
1615
|
-
target, err = os.Readlink(filepath.Join(dependency.Path, entry.Name()))
|
|
1679
|
+
if kind == "symlink" && target == "" {
|
|
1680
|
+
target, err = os.Readlink(entryPath)
|
|
1616
1681
|
if err != nil {
|
|
1617
1682
|
target = "<unreadable>"
|
|
1618
1683
|
}
|
|
@@ -1633,6 +1698,14 @@ func configDependencyDigest(
|
|
|
1633
1698
|
// script writes the fingerprint and this function is what later decides the
|
|
1634
1699
|
// cached evaluation is still current.
|
|
1635
1700
|
if dependency.Kind == configDependencyEntry {
|
|
1701
|
+
if target, err := os.Readlink(dependency.Path); err == nil {
|
|
1702
|
+
// Keep the entry form in the same Windows-junction vocabulary as the
|
|
1703
|
+
// directory form above and the Node loader that produced the digest.
|
|
1704
|
+
h := sha256.New()
|
|
1705
|
+
h.Write([]byte("symlink\x00"))
|
|
1706
|
+
h.Write([]byte(target))
|
|
1707
|
+
return hex.EncodeToString(h.Sum(nil)), nil
|
|
1708
|
+
}
|
|
1636
1709
|
info, err := os.Lstat(dependency.Path)
|
|
1637
1710
|
if err != nil {
|
|
1638
1711
|
digest := sha256.Sum256([]byte("missing\x00"))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttsc/lint",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.2",
|
|
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.28.
|
|
40
|
+
"ttsc": "0.28.2"
|
|
41
41
|
},
|
|
42
42
|
"repository": {
|
|
43
43
|
"type": "git",
|