Documentation
¶
Overview ¶
Package program builds the type graph and hands the other phases a checker they call as a function.
This is the claim the whole tool rests on. Standing in the checker's own language means `GetTypeAtLocation` is a function call rather than a subprocess: the tool this replaces spent 2.09s per run rebuilding a 9,530-file program in a separate process to answer four type questions, and 2.08s of a 2.25s run went to program construction rather than to rules.
The program is built once, and every phase reads the same one.
Index ¶
- Constants
- Variables
- func CacheClasses(rules []rule.Rule) (pure []rule.Rule, typeAware []rule.Rule, design []rule.Rule, ...)
- func CacheTableInvocation(arguments []string) string
- func CacheableRules(rules []rule.Rule) (cacheable []rule.Rule, uncacheable []rule.Rule)
- func DumpCacheTable(out io.Writer, directory string, table *CacheTable, ...)
- func FileVersion(text string) string
- func HashContent(content string) [sha256.Size]byte
- func HashRuleSet(ruleNames []string) [sha256.Size]byte
- func RunCacheKey(arguments []string, workingDirectory string, facts ...string) (string, error)
- func ShapeClasses(typeAware []rule.Rule) (shaped []rule.Rule, typed []rule.Rule)
- func WriteCacheTable(directory string, table *CacheTable, identity CacheTableIdentity, ...) error
- type AdamicCount
- type AdamicRecord
- type CPUAccount
- type CacheTable
- type CacheTableIdentity
- type CacheTableSections
- type ContentPack
- type Coverage
- type DeadSuppression
- type DesignSystemKey
- type DesignSystemKeyRead
- type DirectoryListings
- type DiskCalls
- type DiskTiming
- type Explanation
- type FileCrash
- type Finding
- type FindingsReuse
- type FormatEntry
- type FormatSection
- type FusedCheck
- type Graph
- func (g *Graph) AllDiagnosticParts(ctx context.Context) TypeDiagnosticParts
- func (g *Graph) AllDiagnostics(ctx context.Context) []*ast.Diagnostic
- func (g *Graph) CheckReusing(ctx context.Context, reuse *TypeDiagnosticsReuse) (TypeDiagnosticParts, bool)
- func (g *Graph) CheckerForFile(ctx context.Context, sourceFile *ast.SourceFile) (*checker.Checker, func())
- func (g *Graph) ConfigDiagnostics(ctx context.Context) []*ast.Diagnostic
- func (g *Graph) DependentClosure(seeds []*ast.SourceFile) ([]*ast.SourceFile, bool)
- func (g *Graph) Diagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic
- func (g *Graph) Explain(ctx context.Context, sourceFile *ast.SourceFile, rules []rule.Rule) (*Explanation, error)
- func (g *Graph) GraduateSignatures(shapes map[string]SignatureEntry, deadline time.Time, ...) (graduated int, abandoned string)
- func (g *Graph) IncrementalDiagnostics(ctx context.Context) []*ast.Diagnostic
- func (g *Graph) IsOptionsDiagnostic(diagnostic *ast.Diagnostic) bool
- func (g *Graph) NewIncrementalSession() *IncrementalSession
- func (g *Graph) ProjectFiles() []*ast.SourceFile
- func (g *Graph) ReadUnchanged(path string, text string) bool
- func (g *Graph) RecordFull(ctx context.Context, reuse *TypeDiagnosticsReuse, parts TypeDiagnosticParts)
- func (g *Graph) Retire()
- func (g *Graph) Retired() bool
- func (g *Graph) SeedSignatures(previous map[string]SignatureEntry) map[string]SignatureEntry
- func (g *Graph) SignatureFingerprints(signatures map[string]SignatureEntry) map[tspath.PathKey][sha256.Size]byte
- func (g *Graph) Signatures(ctx context.Context, previous map[string]SignatureEntry) (map[string]SignatureEntry, int)
- func (g *Graph) SourceFiles() []*ast.SourceFile
- func (g *Graph) TypeFingerprints() map[tspath.PathKey][sha256.Size]byte
- func (g *Graph) Walk(ctx context.Context, files []*ast.SourceFile, rules []rule.Rule) (Result, error)
- func (g *Graph) Workers() int
- type GraphTiming
- type IncrementalSession
- type InputRecorder
- type LibraryParses
- type LintCache
- type LintCacheEntry
- type LintCacheFinding
- type LintCacheWithheld
- type MemoryFS
- func (m *MemoryFS) AppendFile(tspath.RootedFilePath, string) error
- func (m *MemoryFS) CaseSensitivity() tspath.CaseSensitivity
- func (m *MemoryFS) Chtimes(tspath.RootedPath, time.Time, time.Time) error
- func (m *MemoryFS) DirectoryExists(directory tspath.RootedDirectoryPath) bool
- func (m *MemoryFS) FileExists(fileName tspath.RootedFilePath) bool
- func (m *MemoryFS) GetAccessibleEntries(directory tspath.RootedDirectoryPath) vfs.Entries
- func (m *MemoryFS) ReadFile(fileName tspath.RootedFilePath) (string, bool)
- func (m *MemoryFS) Realpath(fileName tspath.RootedPath) tspath.RootedPath
- func (m *MemoryFS) Remove(tspath.RootedPath) error
- func (m *MemoryFS) Stat(rooted tspath.RootedPath) vfs.FileInfo
- func (m *MemoryFS) WriteFile(tspath.RootedFilePath, string) error
- type Options
- type PathAnchor
- type ProjectConfig
- type Readiness
- type Result
- type RuleAccount
- type RuleCrash
- type RuleNotes
- type RuleTiming
- type RunCache
- type RunCacheInput
- type RunCacheKeyParts
- type SignatureEntry
- type StatSnapshot
- type StoredDiagnostic
- type Timings
- type TypeDiagnosticParts
- type TypeDiagnosticsReuse
- type TypesEntry
- type TypesSection
Constants ¶
const DependentClosureLimit = 500
DependentClosureLimit is where a closure stops being a saving and starts being the whole tree.
Measured over all 3,542 project files on the ahra tree: the mean one-hop set is 3.8 files and the mean full closure is 58.4, because 83% of files reach their entire closure within 25. For those the closure costs about what one hop costs, so there is nothing to trade.
The tail is where the bound earns its place. `ClassName.ts` has 567 direct importers and closes at 1,477, which is 42% of the tree, and `useRouter.ts` grows 2, 70, 177, 209, 224, 459, 591, 655. Walking those is the whole-tree cost with extra bookkeeping, so the honest answer above the bound is to say the closure is too large and check everything.
500 rather than a rounder number because the distribution has a gap there: 5% of files close between 101 and 500 and a separate 5% close above it, so the bound falls between two populations rather than through the middle of one.
Variables ¶
var ErrCacheTablePartlyKept = errors.New("cache table written by another cohere commit")
ErrCacheTablePartlyKept means the table's files were written by another cohere commit: its format record is kept, for its own key to decide, and every other file is dropped. Reported like a discard, since most of what the table held is gone.
var ErrCacheTableUnreadable = errors.New("cache table discarded")
ErrCacheTableUnreadable means one or more of the table's files are not ones this build may use. Never a reason to fail a run: the answer is to run without those sections and write them anew.
var ErrRunCacheMiss = errors.New("run cache miss")
ErrRunCacheMiss is the one answer a check gives when it cannot prove a hit. Callers treat every error from Check as a miss and run normally; this exists so a test can tell a clean miss from an unexpected failure.
var EveryCacheTableSection = CacheTableSections{AllRuns: true, Findings: true, Signatures: true, Formatted: true, Types: true}
EveryCacheTableSection is the whole table.
Functions ¶
func CacheClasses ¶
func CacheClasses(rules []rule.Rule) (pure []rule.Rule, typeAware []rule.Rule, design []rule.Rule, derived []rule.Rule, uncacheable []rule.Rule)
CacheClasses splits a rule set five ways, in order: rules whose findings depend only on the file's bytes, type-aware rules whose findings also depend on the types the file can see, design-system rules whose findings also depend on the stylesheets the design system read, derived rules whose findings also depend on a program-wide fingerprint they declare, and rules that may never be cached.
ReadsOtherFiles is the one disqualification left, unless the rule declares rule.ProgramFingerprint. A rule reading the program's file list, a file by name, another file's imports, or the file system reaches something a per-file key cannot name, so its answer can change while every key it could have is unmoved. A rule that names it, as a hash of exactly the program-wide data its verdict reads, is derived: keyed on that hash beside its bytes, and on the type fingerprint too when it reads types (#kdee854). Without one it stays uncacheable, so the default is the sound one. The program's other reads are in the key already: compiler options through the tsconfig chain, the default library through the binary, and this file's own module resolution through the type fingerprint, whose edges come from it. So a rule reading module resolution is type-aware even with no checker (#b8k3bp6).
NeedsTypeChecker used to disqualify too, on the reasoning that a type-aware rule's answer depends on what the file imports. It does, and that is now in its key: Graph.TypeFingerprints hashes a file's import closure and everything global, so an edit to anything the file's types come from changes the fingerprint and re-runs the rule. Measured on ahra before the change: 164 of the 172 rules this excluded were excluded for the type checker alone.
ReadsDesignSystem is keyed on the design system's read set, everything its load asked the file system about, observed rather than listed (#pyhm2t2, #35nqkwc). A rule reading the design system and also the types is excluded rather than given a fifth class: none does, and a key over both would need both to hold.
The asymmetry still holds. Including a rule wrongly serves stale findings silently and forever; excluding one wrongly costs a cache miss. A rule that reads other files and the checker both is excluded, since other files are the larger reach.
func CacheTableInvocation ¶
CacheTableInvocation names an invocation within a table.
func CacheableRules ¶
CacheableRules splits a rule set into the rules whose findings depend on nothing but the file's bytes, and everything else. See CacheClasses for the three-way split the walk uses.
func DumpCacheTable ¶
func DumpCacheTable(out io.Writer, directory string, table *CacheTable, identity CacheTableIdentity)
DumpCacheTable prints a table for a person: every run and every file entry, one line each, so a cache that looks wrong can be read rather than guessed at.
func FileVersion ¶
FileVersion is a file's version as the compiler defines it: the xxh3 hash of its text, in hex.
func HashContent ¶
HashContent is the key a cached entry is stored under.
func HashRuleSet ¶
HashRuleSet covers the names of every rule that ran, in order.
Order is included deliberately, but not for the reason first written here. That reason was: "the order they ran in is the order the findings come back in". It is false. Findings come back in file-then-position order, which the walk imposes after its workers finish (sortDiagnostics in walk.go); rule order never decided it.
The conclusion survives the correction on different grounds. A reordered rule set can change which findings EXIST rather than merely their sequence, because fixes applied by an earlier rule change the text a later rule reads, and suppression directives are consumed in the order rules run. So a cache keyed without rule order could replay a set the current configuration would not produce, which is what this hash prevents.
For anyone wiring this cache: LintCacheEntry.Findings is an ordered slice, and replayed findings must reach the caller through the same sortDiagnostics the walk uses, so a warm run and a cold run return one sequence and cache state cannot decide it.
func RunCacheKey ¶
RunCacheKey hashes what identifies a run apart from its files. It is RunCacheKeyPartsOf's key.
func ShapeClasses ¶
ShapeClasses splits CacheClasses' type-aware rules by what they can see of imported files: those that declare rule.TypeReachShapes, keyed on the shape fingerprint, and the rest, keyed on the type fingerprint.
func WriteCacheTable ¶
func WriteCacheTable(directory string, table *CacheTable, identity CacheTableIdentity, sections CacheTableSections, anchor PathAnchor) error
WriteCacheTable writes the named sections of table into directory, each to its own file, and leaves every other file as it is. A section named but nil in table is not written, so a run that has nothing to say about a section never erases what another run left there. Each file path is written anchored, below the project root as a path from it; table itself is left as it is. See ReadCacheTable.
Each file is written atomically: a temporary in the directory, then a rename. Two runs can share a tree, and a reader must never see half a file. A run that returned to its caller early writes after the caller has moved on, where nothing stops a kill landing mid-write; the rename is what keeps the old file whole if one does (TestAWriterKilledBeforeItsRenameLeavesTheOldTable). Files are not atomic with each other, and need not be: each carries its own proof, and the table's lock orders whole writes against reads.
Types ¶
type AdamicCount ¶
AdamicCount is one rule's measure-only finding count on one file.
type AdamicRecord ¶
type AdamicRecord struct {
// Counts is every cacheable cohere:adamic rule that ran on the file with how many findings it had at the
// set's own options, before suppression, sorted by rule. A rule that found nothing has a zero pair, so a
// rule that ran clean and one that did not run can be told apart.
Counts []AdamicCount
// Skipped is the cacheable cohere:adamic rules that skipped the file, sorted. A skip leaves the file
// unmeasured, not ready.
Skipped []string
}
AdamicRecord is one file's Adamic readiness measurement, kept per rule so an entry whose type-aware rules ran again can be merged rule by rule (refreshClasses), as its findings are.
Measure-only findings are counts, never LintCacheFindings: they cannot be printed, cannot fail a run and carry no fixes, so nothing about them needs replaying beyond how many there were.
func (*AdamicRecord) Findings ¶
func (r *AdamicRecord) Findings() int
Findings is the record's total. AdamicRecord itself is the findings cache's, in lint_cache.go.
func (*AdamicRecord) Measured ¶
func (r *AdamicRecord) Measured() bool
Measured is whether the record can say ready or not: a file has one, and no set rule skipped it. A file with none was replayed from an entry its run never measured, which the findings cache refuses to a run that measures (FindingsReuse.MeasureReadiness), so it reads unmeasured rather than as clean.
type CPUAccount ¶
type CPUAccount struct {
Unavailable string
// WorkersCPU is every walk worker thread's CPU from the start of its walk to the end.
WorkersCPU time.Duration
// WalkCPU is the part of WorkersCPU inside no rule, no shared derivation, and no measurement:
// traversal, dispatch, configuration lookups and checker acquisition. It is the workers' total less
// the measured parts, all read from the same threads' clocks, so it holds no waiting.
WalkCPU time.Duration
// InstrumentCPU is what the measurements themselves cost: the calibrated cost of one measured call,
// times the calls. Subtracted from each call and reported here instead.
InstrumentCPU time.Duration
// Calls is how many calls into a rule or a shared derivation were measured.
Calls int
// OtherCPU is process CPU while the walk ran that no walk worker spent: the garbage collector,
// the runtime, and anything running beside the walk.
OtherCPU time.Duration
}
CPUAccount is where a timed walk's CPU went, beyond the rules' own rows.
type CacheTable ¶
type CacheTable struct {
// Runs is one recorded run per invocation, keyed by CacheTableInvocation. A bare run and `--no-fix`
// print different reports under different keys, and kept apart they never overwrite each other.
Runs map[string]*RunCache
// Findings is the per-file findings cache, nil until a run has walked.
Findings *LintCache
// Signatures is each project file's shape, recorded so the next run computes only the files whose
// bytes changed. Nil until a run has needed shapes.
Signatures map[string]SignatureEntry
// Formatted is the format record: which bytes cohere has already seen formatted, so a run formats
// what changed since cohere last looked. Nil until a run has formatted. Its meaning is the format
// phase's (command/cohere/format_record.go); this file only keeps it.
Formatted *FormatSection
// Types is each project file's semantic diagnostics under its shape fingerprint, for the types phase to
// replay. Nil until a run has recorded them. See TypesSection.
Types *TypesSection
}
The cache table: everything cohere keeps between runs for one project root, in one directory.
It holds the run cache's recorded runs, one per invocation, the findings cache's per-file entries, the shapes, the format record and the types section. Each section keeps its own key and its own proof, and each lives in a file of its own with its own header, so a run reads and writes only the sections it uses (#45ekc65).
Why a file per section ¶
They shared one file until 2026-10-04, and replacing any section re-encoded all of them. Measured on ahra's 16 MB table: the format record encodes alone in 0.5ms, and saving it cost a 15ms read and a 21ms write of everything else. Three quarters of the bytes were the recorded runs, about 38,000 inputs per invocation, which only a replay reads, and only its own. Since every section carries its own proof, no section depends on another being written in the same instant, so the files need no atomicity across them: each is written whole and renamed into place, and the table's lock (command/cohere) keeps a reader from seeing one writer's run beside another's findings.
Why gob ¶
The two JSON files were 7.9 MB and 1.45 MB on ahra, decoding in 14ms and 3ms and encoding in 7ms and 25ms. Measured on #6tfmk0b: decode was about 6% of a replay, so speed was not the reason. The reason is that gob is written by reflection over the structs themselves. The lint cache's history is an encoder that forgot a field, shipped four times across a hand-written binary layout and a JSON one, and a format nobody writes by hand cannot forget a field.
The one way gob can lie, and the guard ¶
gob matches fields by name and skips what it does not recognize, in both directions. A field renamed, added or removed decodes without error into a zero value, which is the silent-default failure this format was chosen to escape, arriving by another door. So every file's header carries cacheTableVersion, and TestCacheTableShapeIsPinnedToItsVersion fails whenever the shape of what is encoded changes and the version does not. A changed shape always arrives as a discard, never as a partial read.
Discard, never repair ¶
Each file's header names the format, the cohere commit, the compiler commit, the Go toolchain, the platform that wrote it, and the section it holds. Any of them differing, a decode error, or a byte left over after the body is a discard of that file alone: the run reads the section as empty, says so once on stderr, and its next write of the section replaces the file. A discard costs one ordinary run. A repaired cache that is wrong costs a green run over a broken tree. The single file the table used to be, table.gob, is removed on sight and never read.
One exception, and it is narrow. A format record written by another cohere commit, with the same format, compiler, toolchain and platform, is kept, while every other file from that commit is dropped. The format record is keyed by the formatter's own identity (FormatSection.Key), which moves only when the formatter can print differently, so a cohere commit that never touched the formatter need not reformat every file. The encoding is still guaranteed by the format version, so nothing here reads a field from the wrong place.
func NewCacheTable ¶
func NewCacheTable() *CacheTable
NewCacheTable is an empty table, what a first run and every discard start from.
func ReadCacheTable ¶
func ReadCacheTable(directory string, identity CacheTableIdentity, sections CacheTableSections, anchor PathAnchor) (*CacheTable, error)
ReadCacheTable loads the named sections of the table in directory. It always returns a table: every section whose file is readable, the format record from another cohere commit, and nothing for the rest. Every file path in it is as anchor spells it: the table stores paths below the project root relative to it, so a run from another spelling of the root reads the same entries (#547dhjz; see PathAnchor). The error says what was not kept. Nothing on disk at all wraps os.ErrNotExist, which is a first run and nothing to report. Anything else wraps ErrCacheTableUnreadable or ErrCacheTablePartlyKept, which a caller reports, because a cache that is silently thrown away every run is a saving that quietly never appears.
The single table.gob of earlier formats is removed here, unread.
type CacheTableIdentity ¶
type CacheTableIdentity struct {
SelfCommit string
CompilerCommit string
GoToolchain string
Platform string
}
CacheTableIdentity is what built the binary that writes or reads a table. It goes in the header, so a table written by any other build is discarded before a byte of its body is trusted.
func (CacheTableIdentity) String ¶
func (identity CacheTableIdentity) String() string
type CacheTableSections ¶
type CacheTableSections struct {
Runs []string
AllRuns bool
Findings bool
Signatures bool
Formatted bool
Types bool
}
CacheTableSections names the parts of a table a read loads or a write replaces. Runs names invocations; AllRuns is every recorded run, which only --cache-dump reads.
type ContentPack ¶
type ContentPack struct {
// contains filtered or unexported fields
}
ContentPack serves file bytes recorded by earlier runs and collects what this run read from disk, for Save to keep. Safe for the program loader's concurrent reads.
func OpenContentPack ¶
func OpenContentPack(directory string) (*ContentPack, error)
OpenContentPack reads the pack kept in directory. It always returns a usable pack: one with nothing in it when there is none yet, and when there is one it cannot trust, alongside an error saying why.
func (*ContentPack) Counts ¶
func (p *ContentPack) Counts() (hits int, misses int, damaged int)
Counts reports how many files this run was served from the pack, read from disk, and found damaged.
func (*ContentPack) Save ¶
func (p *ContentPack) Save() error
Save keeps what this run read from disk, appending it to the data file or, when that has grown too dead or something in it was damaged, writing a new one. A run that read nothing new writes nothing. The caller holds the cache directory's lock, so two runs never write at once.
func (*ContentPack) TrustStats ¶
func (p *ContentPack) TrustStats(snapshot *StatSnapshot)
TrustStats has the pack validate entries against snapshot's stats where it holds one, rather than statting each file served again. See StatSnapshot for why that is sound.
type Coverage ¶
type Coverage struct {
// FilesInProgram is the whole type graph — our code plus every declaration it pulled in.
FilesInProgram int
// FilesWalked is how many files the rules actually visited.
FilesWalked int
// NodesVisited is how many AST nodes this walk touched, across every file.
NodesVisited int
// NodesReplayed is how many nodes the replayed files' recorded walks visited, which this walk did not touch:
// a file whose every rule replayed, or whose walked rules listened to nothing. A count that added these to
// NodesVisited said "2,372,633 nodes visited" on an edit run that walked a few hundred files (#kdee854).
NodesReplayed int
// RulesRun is how many rules ran: were offered at least one file, here or in a replayed verdict.
//
// Counted from what was offered rather than from the rules the walk was handed, which are every
// registered rule. A config that turns most of them off left the footer saying "484 rules" when 118
// ran (#v1ah2qq), the very gap between configured and checked this struct exists to show. A rule
// offered files that it declined still ran: it looked and had nothing to listen for.
RulesRun int
// RulesOffered counts, per rule name, how many files the rule was actually handed. A rule that
// was never wired is offered zero; a rule that was offered files and declined all of them is
// offered many and listens to none. Those are opposite defects — nobody configured it, against
// it is configured and satisfied — and before this existed the coverage note described both with
// one sentence, so a satisfied rule read exactly like a dead one.
RulesOffered map[string]int
// RulesListening counts, per rule name, how many files that rule chose to listen to. A rule that
// declined every file reads as zero here, which is the difference between "ran and found nothing"
// and "never actually looked" — the distinction a bare finding count erases.
RulesListening map[string]int
// RulesReporting counts, per rule name, how many findings that rule produced.
//
// Listening and reporting are different questions and only together do they say what a zero
// means. A rule that listened to no files never looked. A rule that listened to thousands and
// reported nothing looked at real code and had nothing to say, which is either a clean tree or a
// rule that cannot see. Two false positives shipped past a full fixture pair and were caught
// only by running against the tree, so the distinction is worth a counter rather than a habit.
RulesReporting map[string]int
// Suppressed is how many findings a disable comment withheld.
//
// This is the coverage discipline pointed the other way. Coverage stops a run that checked
// nothing from printing the same green as a run that found nothing; this stops a run that hid
// forty findings from printing the same green as a run that had none to hide. A suppression is
// a decision, and a decision that leaves no trace in the output is indistinguishable from the
// tool being blind.
Suppressed int
// SuppressedWithoutReason is how many of those withheld findings were silenced by a directive
// that never said why.
//
// Measured rather than enforced, deliberately. Requiring a reason today would turn 281 working
// suppressions red for no defect, so the number is printed every run instead — which is what
// lets the convention be tightened later from evidence rather than from a guess.
SuppressedWithoutReason int
// UnusedSuppressionsForUnrunRules is the subset of UnusedSuppressions naming only rules this run
// did not run.
//
// During the migration this is most of the number, and the two mean opposite things. A directive
// that silenced nothing while its rule ran is dead scaffolding worth deleting. One naming a rule
// cohere has not ported yet silenced nothing because nothing looked, and deleting it would strip
// a suppression the gate still needs. Collapsing them tells a reader to delete comments that are
// load-bearing today.
UnusedSuppressionsForUnrunRules int
// UnusedSuppressions is how many directives never withheld anything.
//
// An unused suppression is a rule scoped off a file that no longer needs it, and it is how a
// codebase accumulates permanent exemptions nobody chose. It only means what it says after a
// full run with every rule, so a caller running a filtered subset should not report it.
UnusedSuppressions int
// DeadSuppressions names each directive counted in UnusedSuppressions and not in
// UnusedSuppressionsForUnrunRules: one that silenced nothing while a rule it names ran. A count
// alone sends a reader linting files one at a time to find them, and a single file is the wrong
// tool, because without the whole program the type-aware rules go quiet and every directive for
// one of them reads as dead.
DeadSuppressions []DeadSuppression
// FilesCrashed names the files a rule panicked on, with the panic that ended each.
//
// Named rather than counted, because the panic message is the defect and a count of crashes is
// not actionable. A file the linter could not process is not a file with nothing to report, and
// keeping those apart is the whole job of this struct.
FilesCrashed []FileCrash
// RulesCrashed names each rule that panicked on a file, with the file and the panic.
//
// A rule's panic is contained to that rule: it stops hearing about the file, and every other rule
// on the file runs to the end. Before this, one rule's panic cost every rule its verdict on the
// file. prefer-arrow-callback's shared arm crashed 71 files that way, and all 446 rules lost those
// files under an ordinary-looking summary (#qa9nttp). The crashed rule's verdict on the file is
// still missing, which is why it is named rather than counted.
RulesCrashed []RuleCrash
// FilesIgnored is how many files an ignorePattern excluded from linting entirely.
//
// A file skipped by configuration and a file with no findings produce identical output
// otherwise, which is the same ambiguity the coverage line exists to destroy. Exclusion is a
// decision someone made, and a decision that leaves no trace is indistinguishable from the tool
// never having looked.
FilesIgnored int
// RulesScopedOff counts, per rule name, how many files the config turned that rule off for.
//
// This is the narrower half of the same accounting. A rule can be enabled in the config, run on
// the tree, and still be silent across a whole directory because an override scoped it off
// there. Without this number that looks identical to a rule with nothing to report.
RulesScopedOff map[string]int
// RulesUnconfigured counts, per rule name, how many files a rule skipped because the config
// never mentions it.
//
// Deliberately separate from RulesScopedOff. "Someone turned this off" and "nobody has said
// whether this should run" are different facts, and a registered rule missing from the config
// is a decision waiting to be made rather than one already made. Reporting them as one number
// would describe a new rule as though someone had excluded it.
RulesUnconfigured map[string]int
// UnrunRuleReferences counts, by name, the rules disable and enable comments named that are real and
// that cohere doesn't run: an ESLint core rule it hasn't ported, or another plugin's rule. Such a
// directive silences nothing here, which is a note rather than a finding (#v1ah2qq).
UnrunRuleReferences map[string]int
}
Coverage is what a run actually looked at.
This exists because "what did you find" is the less important half of a verdict and the only half most tools report. A run that found nothing and a run that checked nothing print the same green, and this project's history is a list of times that ambiguity cost days: a gate that linted zero files and exited 0, three configs that loaded no plugins, a filter that stopped reporting. Every result this package returns carries what it covered, so a caller that prints a verdict without also printing the population had to work at it.
func (Coverage) NodesCovered ¶
NodesCovered is every node the verdict covers: the ones this walk touched and the ones replayed files' recorded walks did. It is what a walk with no cache would report as NodesVisited.
type DeadSuppression ¶
DeadSuppression is one directive that silenced nothing while a rule it names ran: where it is, and the rules it names (none for a blanket directive).
type DesignSystemKey ¶
type DesignSystemKey struct {
Reads []DesignSystemKeyRead
// Fingerprint covers every read, so entries can say which design system their findings were produced
// under. Never zero for a real key: zero is how an entry says its findings were produced under none.
Fingerprint [sha256.Size]byte
}
DesignSystemKey is what the design-system rules' findings depend on beyond a file's own bytes: every path the design system read, present or absent, with what was there. A Tailwind rule's answer on an unchanged file can move when a stylesheet does, when a new `@utility` or theme token lands, or when a candidate entry point that was missing appears, and each of those is a path in this set (#35nqkwc).
It is checked rather than rebuilt: the next run re-hashes these paths, which costs a few stat calls and a few small reads, and replays the design-system rules only if every one still holds. Building the design system to compare it would cost the run the replay exists to skip.
type DesignSystemKeyRead ¶
DesignSystemKeyRead is one path and what was there: nothing, a directory with a listing, or a file with bytes. Hash covers the listing or the bytes, and where a symbolic link led. Path is anchored (PathAnchor), so the fingerprint over it is one for every spelling of the project root (#547dhjz).
type DirectoryListings ¶
type DirectoryListings struct {
// contains filtered or unexported fields
}
DirectoryListings are directory listings a run has already read, for a build to serve its own listings from (Options.Listings): discovery reads every directory the repository keeps, and the tsconfig's include enumeration then read 2,399 of the same directories again, one at a time, 68ms of a cold ahra run (#bjv0tg4, from cache's trace on #g3046x5).
A listing is the entries as os.ReadDir returned them, and a build reads it the way the real disk answers GetAccessibleEntries: regular files and directories as they are, a symbolic link by what it points to (asked of the disk then, as the real disk does), and anything else left out. A directory nobody listed is read from the disk. The listings are as fresh as the walk that read them, a moment before the build, which is the same promise the build's own memoizing layer makes for the listings it reads itself.
Safe for concurrent use: discovery adds from its parallel walk, and a build reads afterwards.
func NewDirectoryListings ¶
func NewDirectoryListings() *DirectoryListings
NewDirectoryListings holds no listing yet.
func (*DirectoryListings) Add ¶
func (l *DirectoryListings) Add(directory string, entries []os.DirEntry)
Add keeps one directory's listing, by its absolute path.
func (*DirectoryListings) Len ¶
func (l *DirectoryListings) Len() int
Len is how many directories are held.
func (*DirectoryListings) Listing ¶
func (l *DirectoryListings) Listing(directory string) ([]os.DirEntry, bool)
Listing is a directory's entries as os.ReadDir returned them, sorted by name, and whether it was listed. The directory is an absolute path, slash or platform separated. An entry's Type() needs no stat, except a symbolic link's target; entry.Info() stats again. A directory that was not listed is not empty: read it from disk. The format walk reads discovery's listings through this (#g3046x5), as the build does through listingFS.
type DiskTiming ¶
type DiskTiming struct {
Reads DiskCalls // ReadFile
Existence DiskCalls // FileExists and DirectoryExists
Stats DiskCalls // Stat
Listings DiskCalls // GetAccessibleEntries, a directory listing
Realpaths DiskCalls // Realpath
}
DiskTiming is filesystem calls by kind, each a count and the time summed across every goroutine that made one.
func (DiskTiming) Summed ¶
func (d DiskTiming) Summed() time.Duration
Summed is every call's duration, added up.
type Explanation ¶
type Explanation struct {
FileName string
// FileIgnored is set when no rule ran because an ignorePattern excluded the file, with the
// pattern responsible. A file skipped by configuration and a file with no findings are
// otherwise identical output.
FileIgnored bool
IgnoredBy string
TotalDuration time.Duration
// Accounts is every rule offered this file, listened or not, in registry order.
Accounts []RuleAccount
}
Explanation is the full account of one file: every rule, what it did, and what it cost.
The debugging counterpart to --timing. Timing says a rule is expensive across the tree; this says what it did on the file where it was expensive.
type FileCrash ¶
FileCrash is a file a rule panicked on, and the panic it raised.
The cause is carried rather than summarized because the panic message is the only evidence of what went wrong: `Unhandled case in Node.Text: *ast.Token` names the defect precisely, and a count of crashes names nothing.
type FindingsReuse ¶
type FindingsReuse struct {
// contains filtered or unexported fields
}
FindingsReuse is one run's use of the findings cache: what the last run recorded, and what this one records for the next.
The walk consults it per file. A file whose bytes, key and applied cacheable rules all match an entry is walked with only its uncacheable rules, and the cacheable rules' findings and coverage are taken from the entry. Every other file is walked in full and, if eligible, recorded.
Coverage is replayed along with findings, by ruling: coverage recorded over inputs proven unchanged is still true of the tree, the argument the run cache settled. A replay is never silent about itself either: Replayed counts the files served from cache, and the lint line says how many, so a walked verdict and a remembered one can always be told apart.
func NewFindingsReuse ¶
func NewFindingsReuse(key [sha256.Size]byte, previous *LintCache, anchor PathAnchor) *FindingsReuse
NewFindingsReuse prepares a run's reuse. A previous cache under a different key is dropped at once rather than consulted per file: nothing in it can be valid. anchor is the project root, which the design system's paths are named relative to; the zero anchor names them as they are.
The previous cache's lookup index is built here, before any worker runs. Lookup would otherwise build it on first use, and sixteen workers missing at once would race to build it.
func (*FindingsReuse) MeasureReadiness ¶
func (r *FindingsReuse) MeasureReadiness()
MeasureReadiness says this run measures Adamic readiness, so an entry recorded by a run that did not is a miss: its file was never measured, and replaying it would read as measured and clean. Called before any worker runs.
func (*FindingsReuse) Misses ¶
func (r *FindingsReuse) Misses() string
Misses says why the files this run looked up and did not replay were not replayed, in words, empty when every one was or none was looked up. A run whose whole findings section was missing or under another key says so itself (the command does), since then no file was looked up in anything.
func (*FindingsReuse) NoteDesignSystem ¶
func (r *FindingsReuse) NoteDesignSystem(reads []rule.FileRead, loaded bool)
NoteDesignSystem records what this run's design system read, from rule.DesignSystemReads after a walk. A run that walks more than once adds every walk's reads, and loaded is whether any walk loaded it at all.
func (*FindingsReuse) Recorded ¶
func (r *FindingsReuse) Recorded() *LintCache
Recorded is what this run leaves for the next: every file it served or walked eligibly, under this run's key. Files this run did not reach are dropped, so the cache never outgrows the tree.
The design system is settled here, once the walks are done. Entries whose design-system rules ran this run carry a zero fingerprint until now; they take this run's. Entries that replayed theirs keep the previous one, unless this run loaded a design system that differs from it, and then every entry's design-system findings are dropped from replay, since which design system they saw can no longer be told.
func (*FindingsReuse) Replayed ¶
func (r *FindingsReuse) Replayed() int
Replayed is how many files this run served from cache.
type FormatEntry ¶
FormatEntry is one file's format status: Sum is the SHA-256 of a text the formatter left unchanged under the options Options fingerprints, and Size and ModifiedNanoseconds are the file's signature when its bytes last matched Sum, so an unchanged file is a stat rather than a read.
type FormatSection ¶
type FormatSection struct {
Key string
Entries map[string]FormatEntry
}
FormatSection is the format record's section of the table. Key names the binary and root its entries were recorded under; a section under any other key says nothing, since a rebuilt formatter may print differently.
type FusedCheck ¶
type FusedCheck struct {
// contains filtered or unexported fields
}
FusedCheck is a cold run's type check done inside the walk: each file is checked on the goroutine that walks it, just before its rules run, rather than by the compiler's whole-program check on goroutines of its own (#679s763).
Why ¶
The whole-program check and the walk want the same checkers. Run beside each other, they took turns at every checker's lock and doubled the goroutines asking for cores: measured on ahra, the check slowed from 1.6s alone to 2.0 to 2.2s beside the walk and format, and became the run's critical path. Fused, one goroutine owns a checker at a time, and the rules read types the check computed a moment earlier.
Why the diagnostics are the same ¶
The compiler's whole-program check is itself a goroutine per checker, checking that checker's files one at a time and collecting each file's diagnostics as it finishes it (checkerPool.forEachCheckerGroupDo). A file checked alone, on the checker that owns it, is the same call on the same checker. What can differ is the order a checker sees its files in, and TestEveryFileWalkedOnAForeignCheckerFindsTheSame and CheckReusing's own per-file assembly already hold the diagnostics to not depending on it.
A stolen file is checked on the thief's checker ¶
A worker whose own group is done walks files taken from another group on its own checker (walkQueue), and checks them there too, rather than queueing for the owner's checker. Queued, the thieves waited 1.28s summed on cold ahra, most of it in the walk's last 300ms, with the cores idle (#tcbrgdx). Deferring the stolen checks until after the walk only moved that tail, since one owner then checked its stolen files alone. The diagnostics are the same whichever checker computes them, because a type in a generic cycle is computed from a canonical root rather than from whatever the checker met first (#rt071h3); TestEveryFileWalkedOnAForeignCheckerFindsTheSame checks every file on a foreign checker at 4 and 16 checkers against one checker alone.
The parts are assembled as AllDiagnosticParts returns them: syntactic diagnostics alone if there are any, else bind diagnostics, then every file's semantic diagnostics sorted and deduplicated together, as the compiler's whole-program call does.
func NewFusedCheck ¶
func NewFusedCheck() *FusedCheck
NewFusedCheck is an empty check, for a graph whose walk will do its checking.
func (*FusedCheck) Checked ¶
func (f *FusedCheck) Checked() int
Checked is how many files have been checked so far.
func (*FusedCheck) DiagnosticParts ¶
func (f *FusedCheck) DiagnosticParts(ctx context.Context, g *Graph) TypeDiagnosticParts
DiagnosticParts is the check's parts, as AllDiagnosticParts would have returned them. A program file the walk did not check (a third-party declaration, a file the config ignores, any file when no walk ran) is checked here, on its own checker, a goroutine per checker.
type Graph ¶
type Graph struct {
// Program is the compiler's own program handle. Exposed because a rule context carries it and
// some type questions are asked of the program rather than of a checker.
Program *compiler.Program
// Config is the resolved tsconfig this graph was built from, kept so a later snapshot can hash
// what it was built against rather than guess.
Config *tsoptions.ParsedCommandLine
// ConfigFileName is the absolute path of the tsconfig that produced this graph.
ConfigFileName string
// Anchor is the directory the graph was built in, which every fingerprint names paths relative to, so
// that two spellings of one directory fingerprint every file alike (#547dhjz). See PathAnchor.
Anchor PathAnchor
// CompilerHost is the filesystem view the program was built through, kept because the
// incremental machinery needs the same one.
//
// ReadBuildInfoProgram takes a host and a ParsedCommandLine, and it has to be the host the
// program was built with rather than a fresh one: the build info's file paths are resolved
// relative to it, and a second host with a different current directory or a cold cache
// would resolve them somewhere else and report every file as changed. Silently, and in the
// direction that looks like a cold cache rather than like a bug.
CompilerHost compiler.CompilerHost
// LintConfig decides which rules apply to which files, and which files are not linted at all.
//
// Nil means every rule applies to every file, which is what the tests and any caller predating
// this layer expect. The command always loads a real one and fails loudly if it cannot, so the
// permissive default is reachable only from a caller that chose it deliberately.
LintConfig *configuration.Config
// RuleOptions says how to decode each rule's configuration, and which rules cannot run without
// it. Empty means no rule takes options, which is what a test that did not set one expects.
RuleOptions configuration.OptionsRegistry
// RegisteredRuleNames is every rule the registry holds, whether or not this run selected it.
//
// The walk reads it to tell a suppression naming an unknown rule from one naming a real rule that
// is merely not running here. Nil turns that check off, which is what a test that did not set it
// expects: without a registry every name would read as unknown.
RegisteredRuleNames []string
// CollectTimings turns on per-rule cost measurement for the next Walk.
//
// Off by default and guarded at every timing site, so an ordinary run reads no clocks and
// allocates no accumulators. The instrument should cost nothing when nobody asked for it.
CollectTimings bool
// FindingsReuse, when set, lets Walk serve unchanged files' cacheable findings from the last run
// and records this run's for the next. Nil walks everything, exactly as before it existed.
FindingsReuse *FindingsReuse
// Readiness, when set, measures every walked file against cohere:adamic for the run's readiness, whatever
// the project's chain enables. Nil measures nothing and the walk is exactly as before. See Readiness.
Readiness *Readiness
// Shapes is every project file's shape for this run (Graph.Signatures), when the caller computed them.
// Walk keys shape-keyed rules on them; without them it keys those rules on the type fingerprint.
Shapes map[string]SignatureEntry
// YieldedFiles is how many files the tsconfig named that a nearer one owns, left out of ProjectFiles.
// See Options.Yielded.
YieldedFiles int
// WalkOnForeignCheckers walks every file on a checker other than its own: a test instrument, for holding a
// stolen file's findings and type diagnostics to its home checker's. See walkQueue.
WalkOnForeignCheckers bool
// ContentKeyedShapes is how many project files Signatures keyed on their content, with no recorded
// declaration signature to compute theirs from. See Signatures.
ContentKeyedShapes int
// FusedCheck, when set, has the next Walk type-check each file it visits, just before the file's rules run,
// and the types phase read the result rather than check the program itself. Nil walks without checking.
FusedCheck *FusedCheck
// contains filtered or unexported fields
}
Graph is a built type graph: the program, and the checkers that answer questions about it.
A Graph is never returned half-built. Build either hands back a Graph whose program resolved, or an error — there is no state in between, because the failure this package exists to make impossible is a program that silently contains nothing. An empty file list is indistinguishable from a clean tree, and that exact confusion let the gate this tool replaces print green over zero files for days.
func Build ¶
Build resolves a tsconfig and constructs the program and its checkers.
Every failure here is loud. A tsconfig that does not exist, one that does not parse, one whose options are contradictory, and one that matches no files are four different errors rather than four flavors of an empty result, because the whole point of this package is that a run which checked nothing must not be able to look like a run that found nothing.
func (*Graph) AllDiagnosticParts ¶
func (g *Graph) AllDiagnosticParts(ctx context.Context) TypeDiagnosticParts
AllDiagnosticParts is AllDiagnostics with its parts kept apart. See TypeDiagnosticParts.
func (*Graph) AllDiagnostics ¶
func (g *Graph) AllDiagnostics(ctx context.Context) []*ast.Diagnostic
AllDiagnostics returns TypeScript's findings for the whole program.
This is not a convenience wrapper around Diagnostics in a loop, and the difference is a measured 3.7s versus 1.1s on a 3,408-file tree. The compiler's diagnostic entry points branch on whether a file was named: pass one and it checks that file lazily, on whichever single checker owns it; pass nil and it first runs CheckSourceFiles, which fans every checker out across all four workers at once. So a loop that asks file by file quietly serializes the most expensive phase in the tool onto one core, and it does it without any call looking wrong.
The findings are identical either way. Only the wall clock differs, which is exactly what makes this the kind of mistake that survives review.
func (*Graph) CheckReusing ¶
func (g *Graph) CheckReusing(ctx context.Context, reuse *TypeDiagnosticsReuse) (TypeDiagnosticParts, bool)
CheckReusing checks the project files whose shape fingerprint moved since the section was recorded and replays the rest. It reports false when it cannot, and the caller runs the full check and hands its parts to RecordFull: no section, a section under other compiler options or from a run that saw global diagnostics, no shapes this run, or so much changed that the full check, which spreads every checker over every file, is the faster way.
func (*Graph) CheckerForFile ¶
func (g *Graph) CheckerForFile(ctx context.Context, sourceFile *ast.SourceFile) (*checker.Checker, func())
CheckerForFile returns the checker that owns a file, and a function to release it.
There are several checkers, one per worker, and a file belongs to exactly one of them. That ownership is not an optimization detail a caller may ignore: types obtained from two different checkers cannot be compared or mixed, so a rule must ask the checker that owns the file it is looking at. Asking for the wrong one produces answers that are individually plausible and jointly meaningless, which is the worst failure shape available here.
The release function must be called, conventionally by defer.
func (*Graph) ConfigDiagnostics ¶
func (g *Graph) ConfigDiagnostics(ctx context.Context) []*ast.Diagnostic
ConfigDiagnostics returns findings about the configuration itself rather than about any file — contradictory options, an unreachable lib, a target that does not exist.
func (*Graph) DependentClosure ¶
func (g *Graph) DependentClosure(seeds []*ast.SourceFile) ([]*ast.SourceFile, bool)
DependentClosure is the seeds plus every project file that reaches them through imports.
The second return is false when the closure exceeded DependentClosureLimit, and the caller is expected to check the whole tree rather than to use a truncated set. A partial closure reported as a complete one is the failure this whole scoping effort has to avoid: it would be fast, silent, and wrong in the direction that hides findings.
Demonstrated before this existed, by changing one function's return type to `any`: the whole tree reported seven new findings, three in the edited file and four in a consumer two directories away, and a run scoped to the edited file alone saw only the three.
func (*Graph) Diagnostics ¶
func (g *Graph) Diagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic
Diagnostics returns TypeScript's own findings for one file: syntax, then binding, then semantics.
The order is the one `tsc` reports in and it matters. A file that does not parse produces cascading nonsense from the later phases, so when syntax fails we stop rather than bury the real error under fifty invented ones.
Prefer AllDiagnostics for a whole-program check. Calling this in a loop over every file is more than three times slower for a reason that is invisible at the call site — see the note there.
func (*Graph) Explain ¶
func (g *Graph) Explain(ctx context.Context, sourceFile *ast.SourceFile, rules []rule.Rule) (*Explanation, error)
Explain runs every rule against one file and records what each did.
A separate path from Walk rather than a flag on it. Walk is the hot path across thousands of files and must stay free of per-rule bookkeeping; this runs once, on one file, where the overhead is irrelevant and completeness matters more than speed.
func (*Graph) GraduateSignatures ¶
func (g *Graph) GraduateSignatures(shapes map[string]SignatureEntry, deadline time.Time, abandonAfter time.Duration) (graduated int, abandoned string)
GraduateSignatures gives files whose shape is keyed on content a real declaration signature, emitting them one at a time until every one is done or deadline passes, and updates shapes in place. It returns how many graduated, and the file it abandoned, "" when it abandoned none (#9knyr86).
Signatures never emits a file without a real recorded signature, because an emit has no ceiling and cannot be cancelled (#5txm9gg), so a project with no build info keyed every shape on content forever. Here the run's verdict is already out, so an emit nobody waits on can be left behind at the deadline: the caller writes the table and exits, which ends it. The file it was on is marked Abandoned, and is not tried again while its bytes are the same, but only when that emit had run for abandonAfter: a file the deadline caught a moment after it began is innocent (ahra's slowest emit is 116ms, its median 0.1ms), and the next run tries it first, with the whole budget. A file the deadline stopped before it began is simply tried by the next run.
The emits run on a goroutine of their own, and only this function touches shapes, so whatever the goroutine finishes after the deadline is dropped.
func (*Graph) IncrementalDiagnostics ¶
func (g *Graph) IncrementalDiagnostics(ctx context.Context) []*ast.Diagnostic
IncrementalDiagnostics is the one-shot form: check warm and persist for next time, falling back to the full pass when the graph cannot support a session.
Prefer this at a call site that just wants the findings. Use NewIncrementalSession directly when the caller needs to decide whether to write, for example when it is about to report a failure and would rather not persist state from a run it is discarding.
func (*Graph) IsOptionsDiagnostic ¶
func (g *Graph) IsOptionsDiagnostic(diagnostic *ast.Diagnostic) bool
IsOptionsDiagnostic reports whether a diagnostic is about the compiler options rather than about source: a code from either list, located in no file or in one outside this program.
func (*Graph) NewIncrementalSession ¶
func (g *Graph) NewIncrementalSession() *IncrementalSession
NewIncrementalSession prepares a warm-capable pass. Returns nil when the graph cannot support one, which is a reason to run the full pass rather than an error: no compiler host, no config, or a tsconfig without `incremental` all mean the caller should use AllDiagnostics.
func (*Graph) ProjectFiles ¶
func (g *Graph) ProjectFiles() []*ast.SourceFile
ProjectFiles is the subset of the program that the tsconfig actually named — our code, without the declarations it pulled in.
On the ahra tree that is roughly 3,400 files out of 9,530. The other 6,100 are `node_modules` `.d.ts` that the checker needs in order to answer questions and that no rule should visit: a rule walking them would cost most of the run to report findings against code nobody here can edit.
func (*Graph) ReadUnchanged ¶
* ReadUnchanged reports whether text, the program's copy of a file, is the file's bytes now, so a reader can * use it instead of reading the file again (#q6dey77). Exact, by three checks: * - the program read the file under a stat, its content pack's or its own, and a stat taken now matches it, * so the file holds the bytes the program read; * - those bytes were the program's copy exactly. The program reads through typescript-go's decodeBytes, * which drops a UTF-8 byte order mark and decodes UTF-16 behind its mark, and returns every other file * byte for byte. So the file must not start with a mark (#hkv1hgp). The recorded size must also be the * copy's length, which no dropped mark leaves true and which costs no read, so a marked file is turned * away before it is opened. The length alone isn't enough: UTF-16 heavy in characters that take three * bytes in UTF-8 can decode to exactly its own size. * False on any doubt, so the reader reads the disk, as it would have.
func (*Graph) RecordFull ¶
func (g *Graph) RecordFull(ctx context.Context, reuse *TypeDiagnosticsReuse, parts TypeDiagnosticParts)
RecordFull records a full check's semantic diagnostics per project file, for the next run to replay.
func (*Graph) Retire ¶
func (g *Graph) Retire()
Retire marks a graph replaced: the fix phase rewrote files and a new graph was built from the new bytes.
A check still running on this graph is left to finish rather than cancelled, and what it finds is never recorded into the run cache's types section, since it describes bytes that no longer exist. It is not cancelled because the compiler's whole-program check cannot be: a checker that sees its context end marks itself cancelled, the compiler then hands it the next file in its group, and GetGlobalDiagnostics panics on a compiler goroutine nothing here can recover. The gate itself died that way, a bare `cohere` in 5 of 6 runs once its fix phase rewrote a file before the early check finished (#sm79kfv).
func (*Graph) SeedSignatures ¶
func (g *Graph) SeedSignatures(previous map[string]SignatureEntry) map[string]SignatureEntry
SeedSignatures returns previous with an entry added for every project file it lacks, taken from the signatures the compiler's own buildinfo recorded. A first run of the table then starts from what the compiler already computed instead of computing every signature (about 500ms on ahra) or falling back to versions, which would make each file's first edit invalidate its importers once for nothing.
Only real shapes are taken. An entry whose signature is its version, or that has none, says the compiler never computed a shape for that file, and the file is left to be computed here.
func (*Graph) SignatureFingerprints ¶
func (g *Graph) SignatureFingerprints(signatures map[string]SignatureEntry) map[tspath.PathKey][sha256.Size]byte
SignatureFingerprints gives each project file a hash of everything its types can depend on, like TypeFingerprints, except that what it hashes of every OTHER file in its import closure is that file's signature rather than its bytes. A body edit to a file imported by half the tree changes its own fingerprint and nobody else's.
The file's own bytes are still in its fingerprint, all of them: a rule reads its own file's syntax. The global component is TypeFingerprints' own, bytes and all, since a global declaration changes types everywhere without being imported.
It is sound only for a rule that sees other files through types. A rule that reads an imported declaration's syntax stays on TypeFingerprints.
func (*Graph) Signatures ¶
func (g *Graph) Signatures(ctx context.Context, previous map[string]SignatureEntry) (map[string]SignatureEntry, int)
Signatures returns every project file's signature, keyed by file name, and how many were computed rather than carried forward from previous.
func (*Graph) SourceFiles ¶
func (g *Graph) SourceFiles() []*ast.SourceFile
SourceFiles is every file in the program, third-party declarations included.
This is the program's population, not the set a phase should lint. Use ProjectFiles for that.
func (*Graph) TypeFingerprints ¶
TypeFingerprints gives each project file a hash of everything its types can depend on, so a type-aware rule's findings for a file can be cached on the file's bytes plus this.
A type-aware rule asks the checker about nodes in one file, and the checker's answers come from that file, every file it reaches through imports, and everything global. So the fingerprint covers:
- the file's import closure among project files, hashed per strongly connected component so import cycles, which real trees have, are one unit, and so a file reached by 40% of the tree is hashed once rather than once per importer;
- a global component folded into every fingerprint: every file in the program that is not a project file (installed declarations), and every project file that can change other files' types without being imported: declaration files, scripts, and anything that says `declare global` or `declare module`.
The global component is a superset on purpose. Including a file that turns out not to matter costs a miss when it changes; leaving out one that does would replay a type-aware finding computed against types that are gone, which is the stale verdict this cache must never produce. The lib files are not here because they are embedded in the binary, which is in the cache's key.
Edges come from the program's own module resolution, normalised the way DependentClosure normalises them: compared raw, on a case-insensitive volume, they produced a graph with almost no edges, which reads as valid rather than as broken.
Edges only join project files, so each member also carries where every one of its imports resolved, installed files included. A re-export that starts resolving to another installed declaration, both already in the program, moves no edge and no file in the global component: a package.json's "types" changing is enough. Without its resolutions in its hash, the importers of that re-exporter kept their key while the types they see changed (#9bjjk4a, found answering @system_cohere_lint).
func (*Graph) Walk ¶
func (g *Graph) Walk(ctx context.Context, files []*ast.SourceFile, rules []rule.Rule) (Result, error)
Walk visits every file in the given set once, dispatching every rule's listeners as it goes.
One traversal serves every rule. This is the arrangement the whole tool exists to make possible: a rule that wanted its own pass would multiply the only unavoidable cost in the system by the number of rules, which is exactly how 53 rules of ordinary shape came to cost 1.81s. Here the five-hundredth rule costs a function call per node it asked about, and nothing at all for the nodes it did not.
Files are walked in parallel, one goroutine per checker, because a file must be visited by the checker that owns it and there is no use in more workers than there are checkers.
func (*Graph) Workers ¶
Workers is how many files can be checked at once.
It matches the checker count the program built, because a file must be visited by the checker that owns it and there is no benefit to more goroutines than there are checkers to serve them.
This asks the program rather than remembering what we requested. Until the move to `microsoft/TypeScript`, `Program.SingleThreaded()` was unexported and the checker count was a private constant, so this had to mirror the compiler's own arithmetic and hope the two stayed in step. Both are reachable now, so the mirror is gone: a count that disagreed with the compiler's would hand files to a worker whose checker does not own them, and types from two checkers cannot be mixed.
type GraphTiming ¶
type GraphTiming struct {
// Builds is how many times the program was built: two when the first build's roots moved under it
// and Build built again. Every duration below covers all of them.
Builds int
// Config is parsing the tsconfig, which includes enumerating the directories its `include`
// patterns reach. That enumeration is the directory walk a parallel readDirectory would speed up.
Config time.Duration
// Program is compiler.NewProgram: every file read, parsed and its imports resolved.
Program time.Duration
// Verify is matching the config's files to the program's, and asking the disk about any missing.
Verify time.Duration
// SourceFileLoads and SourceFileSummed are the compiler host's GetSourceFile calls, each a read
// and a parse, summed across the loaders.
SourceFileLoads int64
SourceFileSummed time.Duration
// ConfigDisk and ProgramDisk are the real filesystem calls made while the config was parsed and
// while the program was built: beneath the stat and readdir cache, so a call it answered is not
// here, and above the content pack, so a file the pack served is here at the cost of serving it.
ConfigDisk DiskTiming
ProgramDisk DiskTiming
// PackServed and PackRead are the content pack's own count of the reads above: files it served from
// its mapping, and files it had to read from disk. Both zero when the build had no pack. A read the
// pack served costs a copy, not an open, so the two must be told apart to read Reads' time.
PackServed int
PackRead int
// CheckedAnswers is how many of the existence checks and stats above were answered from the run cache's
// check (Options.CheckedStats) rather than by the disk, across config and program alike.
CheckedAnswers int64
}
GraphTiming is what building the graph cost, by part, collected when Options.Timing is set.
The graph phase was one number, `graph built in 547ms`, and nobody knew what it was made of (#cazsft3). The levers proposed for it were guesses: parallel directory enumeration, a resolution cache, a declaration pack. Each is worth something only if its part is a large share, and that is what this measures rather than reasons about.
Two kinds of number, and they must not be read as one:
- Wall: Config, Program and Verify are each one stretch of the build's own goroutine, and they add up to most of the phase.
- Summed: the compiler loads files on many goroutines at once, so the time spent inside its loads and inside the disk is summed across all of them and runs far past Program's wall. It says where the loaders' work goes, not how long anyone waited.
Parse and module resolution are not separable from here. Both run inside the compiler's file loader, interleaved per file, behind one call. What can be measured from outside is each load, which is a read and a parse, and the disk beneath it; resolution and the loader's own bookkeeping are the part of Program's wall those leave unexplained.
type IncrementalSession ¶
type IncrementalSession struct {
// contains filtered or unexported fields
}
IncrementalSession is one warm-capable pass over a graph: check, then persist what the next run needs to know which files it can skip.
It exists as a type rather than as two methods on Graph because of the single most expensive thing to get wrong here. The build info must be emitted from the SAME incremental program that performed the checking. That program's snapshot records which files it checked, and the emit writes those records; a build info emitted from a freshly-wrapped program has an empty snapshot, so it marks every file as "unchanged but never checked", which on load means "check it".
Measured, because the difference does not look like anything from the call site:
emitted from a fresh wrapper warm semantic pass 712ms, nothing skipped emitted from the checker warm semantic pass 4ms
The first version of this file had two independent methods and produced the 712ms. It was correct, it was slower than not caching at all, and every test passed. Holding the program across both calls is what makes the type worth its weight.
THE RISK, stated where the code is rather than in a task nobody will read: a cache that fails to invalidate reports zero findings forever, and that is indistinguishable from a clean tree on every measurement except a planted violation. "Cold equals warm" does not test it, because a permanently-stale cache passes that too. The test that matters is warm-after-a-change, in incremental_test.go.
func (*IncrementalSession) DiagnosticParts ¶
func (s *IncrementalSession) DiagnosticParts(ctx context.Context) TypeDiagnosticParts
DiagnosticParts is Diagnostics with its parts kept apart. See TypeDiagnosticParts.
func (*IncrementalSession) Diagnostics ¶
func (s *IncrementalSession) Diagnostics(ctx context.Context) []*ast.Diagnostic
Diagnostics returns the same findings AllDiagnostics returns, re-checking only the files whose contents changed since the last Write.
The skip is not a replay of stored diagnostics. The build info stores content hashes; on load, upstream seeds a "no diagnostics" entry for every file whose hash still matches, and the checker visits only the files missing from that map.
The program is still built either way. A build info can skip checking the graph, never constructing it, so the caller has already paid the graph phase before calling this.
func (*IncrementalSession) Write ¶
func (s *IncrementalSession) Write(ctx context.Context) []*ast.Diagnostic
Write persists what the next run needs to tell changed files from unchanged ones.
It costs about 146ms on ahra warm, and the cost is CPU rather than disk. Measured by instrumenting inside upstream's emitBuildInfo:
snapshotToBuildInfo 104ms walking the snapshot json.Marshal 9ms 3.5 MB of JSON the write itself 0.4ms
Writing 3.5 MB on this machine is 0.5ms, ten trials. So "the cache rewrites an unchanged file every run" is true and is not worth fixing: skipping the write when the bytes match was built, measured, and reverted, because reading the old file to compare costs more than the write it avoids (blocked A/B, six runs each: 362ms median unchanged against 371ms with the skip).
Upstream's own already-up-to-date check cannot fire for us and that is not a bug to fix here: buildInfoEmitPending is initialized true for any incremental config with no in-memory predecessor (programtosnapshot.go:69) and set true again unconditionally after semantic diagnostics (program.go:319). Both are correct for the watch process it was written for. cohere is one-shot, so a fresh snapshot every run means the flag is structurally always true.
The remaining cost lives in rebuilding state a resident process would still hold -- the 204ms parse in NewIncrementalSession and the 104ms snapshot walk here are the same expense paid at both ends. Neither is reachable without deciding whether cohere stays one-shot. See #hfv0ae3.
Call it after Diagnostics, on the same session. Calling it before, or on a different session, produces a build info that records nothing as checked and a warm run that skips nothing, which costs more than not caching at all.
Cohere does not otherwise emit, and this does not change that: under NoEmit the emit path writes the build info and no JavaScript.
Returns any diagnostics from the write itself. A failed write must not pass silently: the next run would be cold while this one reported success, and the symptom is a saving that never appears rather than an error anyone can see.
type InputRecorder ¶
type InputRecorder struct {
// contains filtered or unexported fields
}
InputRecorder notes every path the compiler read or looked for on disk during a build.
It exists for the run cache, and the reason it observes rather than enumerates is that every hand-written list of a build's inputs turned out to be missing something. The program's source files are the obvious inputs. Less obvious: the tsconfig's extends chain, every package.json module resolution consulted, and every path resolution probed and did NOT find, since creating one of those changes what an import resolves to. An in-place edit to a package.json beside the tsconfig moves no directory's mtime, so a cache that only watched program files and their directories would replay a stale verdict over a changed resolution. Recording at the filesystem boundary sees all of them because it sees whatever the compiler did, not what someone remembered it does.
It wraps the real disk beneath the memoizing layer, so each path reaches it about once, and beneath the embedded lib overlay, so the bundled lib.*.d.ts files never appear here: they are part of the binary, and the binary is already in the run cache's key.
func NewInputRecorder ¶
func NewInputRecorder() *InputRecorder
NewInputRecorder returns an empty recorder.
func (*InputRecorder) Inputs ¶
func (r *InputRecorder) Inputs() (present []string, absent []string, probed []string)
Inputs reports what the build depended on: paths that existed, and paths it looked for and did not find. Anything the build itself wrote is excluded. Sorted so two recordings of one tree compare equal.
probed is the present paths the build only asked whether they exist, never read, listed or statted, a subset of present. Module resolution asks that of every directory from the project up to the root, on its way to a package.json or a node_modules, and the answer can only change by the path ceasing to be there. See RecordRunCache, which records such a directory for its existence alone.
The same tree records the same inputs on every build (#zc57tqg). Which existence probes reach the disk depends on which of the parallel loaders asks first: on excalidraw, six builds of one tree each recorded the probe of a package's directory, such as node_modules/react-dom/, only sometimes, the times module resolution answered a second import of the package from its own cache by a path the first did not take. Every one of them had a path beneath it that every build read, its package.json among them. So a probe is left out when a present path lies beneath it: the descendant can only be there while the directory is, so its own record already catches the directory going away, and the probe adds nothing a check could fail on. What remains is the same set whichever loader went first. A path is recorded without a trailing slash, since the resolver asks for some directories both ways.
type LibraryParses ¶
type LibraryParses struct {
// contains filtered or unexported fields
}
LibraryParses is one parse of each bundled lib file, shared read-only by every program built with it (Options.LibraryParses).
Every program parses the lib files its target pulls in: 58 of them for ES2022, about 3.25ms and 2.5 MB retained, against 0.1ms for the one-file fixture a rule test builds around them. A test binary builds thousands of those programs, so it parsed the same embedded files thousands of times and kept every copy (#7nx3z1n, #0rj7wvg). The files are compiled into the binary and cannot change while it runs, so one parse of each can serve every program, the way typescript-go's language server shares parses across its projects (internal/project/parsecache.go).
Why sharing a parsed file is safe ¶
A parse is a function of its key: the parse options (file name, path, and how the file decides whether it is a module), the script kind, and the text. That is upstream's own parse cache key, and a lib file's target or lib setting enters none of it: a different target loads a different set of lib files, not a different parse of the same one. The text is keyed by its hash rather than assumed, so a stand-in lib directory can never be answered with the embedded parse.
After the parse, nothing a program does writes to the file. Binding is once per file and the same for every program (SourceFile.BindOnce), and every type a checker computes lives in that checker. The compiler's loader writes to a file only for content-mapped ones, which a lib file never is.
Only bundled files are shared. A file on disk can change between builds, and the run cache and the content pack exist to say when it has; the embedded libs are the one set that provably cannot.
func NewLibraryParses ¶
func NewLibraryParses() *LibraryParses
NewLibraryParses returns an empty cache. One per process is the intended use: every program built with it shares its parses for as long as it lives.
func (*LibraryParses) Len ¶
func (l *LibraryParses) Len() int
Len is how many distinct parses the cache holds.
type LintCache ¶
type LintCache struct {
// Version is the format. A different version is a miss, never a best-effort read.
Version int
// Key covers everything a cacheable rule's answer depends on beyond the file's own bytes: the
// binary, the lint config, the tsconfig chain, the project root and the rule set in order. Any
// change to one invalidates every entry at once, which is what makes it a miss rather than a
// stale pass. It was called RuleSetHash when the rule set was all it covered.
Key [sha256.Size]byte
// KeyParts is Key's parts, so a run under another key can say which part moved. Zero when the run that
// recorded it kept none. See RunCacheKeyParts.
KeyParts RunCacheKeyParts
// Entries is one record per cached file.
Entries []LintCacheEntry
// DesignSystem is the design system the design-system rules' findings were produced under, nil when none
// was recorded. See DesignSystemKey.
DesignSystem *DesignSystemKey
// contains filtered or unexported fields
}
The lint findings cache: what each rule reported about one file, replayed when neither the file nor the rule set has changed.
This is the cheapest of the three caches to make correct, and the reason is worth stating because it does not hold for the other two. A pure rule's findings are a function of exactly one file's bytes. Same bytes, same rule, same findings, with nothing else in the world able to change the answer. So the key is a content hash and there is no invalidation graph.
The word doing the work there is "pure". A rule that reads other files is not, and caching it under one file's hash serves a stale result forever when the OTHER file changes. Two live rules are cross-file today, and a third case is latent: the upstream adapter hands the whole program to every rule it wraps and cannot inspect what it wrapped, so it can never truthfully declare purity for a body it does not own.
That is what CacheableRules exists to decide, and why it defaults to excluding rather than including. The asymmetry is the same one the codebase already chose for the type checker: under-declaring serves a stale finding silently and forever, over-declaring costs a cache miss. One of those is a correctness failure and the other is a performance one.
It is wired as the run cache's second layer: when an eligible run's inputs are not all unchanged, files whose bytes and configuration are unchanged are walked with only their uncacheable rules, and the cacheable rules' findings and coverage are replayed. See FindingsReuse.
It was measured and left unwired once, and the reversal is worth keeping because the measurement method is the same and only the tree changed. On 2026-08-25, with 212 rules, walking with only the uncacheable ones saved about 9%: the walk visits every node whatever the rule set, so with light rules there was little left to skip. On 2026-10-02, with 464 rules, the same experiment on ahra's real config, each walk on a freshly built graph:
walk, all 464 rules about 2.56s walk, the 45 ReadsProgram rules only about 1.37s
The second line was the ceiling for a fully cached run, since those rules always ran. #b8k3bp6 split ReadsProgram into ProgramReads by kind, and only the 9 rules reading other files still always run: about 0.23s on 2026-10-02, best of 3 on fresh graphs, against 1.35s for the 45 the same day. The 1.37s was first published here as 0.4s, measured on a graph whose earlier walks had already paid for type checking: the checker computes types lazily and whichever walk asks first pays, so a later walk looks cheap for a reason that has nothing to do with its rules. Every walk is now on a fresh graph. A real one-file-changed run on ahra saved about 1.1s of the fix phase, close to that ceiling.
The node count is identical across rule sets, so the walk itself still cannot be skipped; what grew is the work rules do at each node. Type-aware rules are cached on a type fingerprint as well as the file's bytes; see CacheClasses and Graph.TypeFingerprints.
Two kinds of file are never served from cache. A file with any suppression directive, because a directive's accounting spans every rule in the file, cached and walked alike. A file with any finding that carries a fix or a suggestion, because a cached finding stores neither and the fix phase needs the edit itself.
func (*LintCache) Lookup ¶
func (c *LintCache) Lookup(path string, contentHash [sha256.Size]byte, key [sha256.Size]byte, rules []string) (LintCacheEntry, bool)
Lookup returns a file's cached entry, and whether the cache had a usable answer.
The second return distinguishes "cached, no findings" from "not cached", and that distinction is the whole correctness of this cache: a clean file and an unknown file both produce no findings, and treating them the same is how a cache silently reports a clean tree.
A hit needs the key, the path, the bytes and the applied rule list all to match. The index is built on first use, so a caller that loads a cache and discards it on a key mismatch never pays for it.
func (*LintCache) Store ¶
func (c *LintCache) Store(entry LintCacheEntry)
Store records one file's findings, replacing any earlier entry for the same path.
Replace rather than append, because a path appearing twice makes Lookup's answer depend on which copy the index happened to keep. The index is invalidated rather than patched: Store is called once per file during a walk and Lookup runs afterward, so rebuilding once on the next lookup costs less than maintaining the map through every insert.
Empty findings are stored deliberately. A clean file is the most valuable thing this cache holds, because most files are clean, and an entry saying "these bytes produce nothing" is a real answer. Skipping it would make a clean file indistinguishable from an unknown one, which is the exact ambiguity Lookup's second return exists to destroy.
type LintCacheEntry ¶
type LintCacheEntry struct {
// Path identifies the file. Kept for debugging and for eviction; the hash is what decides
// a hit.
Path string
// ContentHash is the hash of the file's bytes when these findings were produced.
ContentHash [sha256.Size]byte
// Rules is the cacheable rules the configuration applied to this file, in order. A hit requires
// the same list now, so an override that changes which rules reach this file is a miss even under
// an unchanged key.
Rules []string
// TypedRules is the type-aware rules applied to this file, in order. Their findings depend on the
// types the file can see as well as its bytes, so they replay only while TypeFingerprint matches
// too. See Graph.TypeFingerprints.
TypedRules []string
// TypeFingerprint is the file's type fingerprint when TypedRules' findings were produced.
TypeFingerprint [sha256.Size]byte
// ShapedRules is the type-aware rules applied to this file that declare rule.TypeReachShapes. Their
// findings replay while ShapeFingerprint matches, which an edit inside an imported function's body does
// not move. See Graph.SignatureFingerprints.
ShapedRules []string
// ShapeFingerprint is the file's shape fingerprint when ShapedRules' findings were produced.
ShapeFingerprint [sha256.Size]byte
// DesignRules is the rules applied to this file that read the design system (rule.ReadsDesignSystem). Their
// findings replay while the design system is the one DesignFingerprint names. See DesignSystemKey.
DesignRules []string
// DesignFingerprint is the design system's fingerprint when DesignRules' findings were produced, zero when
// they cannot be replayed under any.
DesignFingerprint [sha256.Size]byte
// DerivedRules is the rules applied to this file that declare rule.ProgramFingerprint. Their findings replay
// while DerivedFingerprint matches.
DerivedRules []string
// DerivedFingerprint hashes each DerivedRules rule's name and program fingerprint, and the file's type
// fingerprint when any of them reads types, as they were when DerivedRules' findings were produced. Zero
// when one had no fingerprint that run, which no lookup matches.
DerivedFingerprint [sha256.Size]byte
// Listening is the subset of Rules and TypedRules that registered a listener on this file. A replay
// counts them as listening, which is what keeps the coverage line identical to a walked run's.
Listening []string
// VisitedNodes is how many nodes the full walk of this file visited. The walk counts nodes only
// when some rule listens, so a cached file whose remaining rules listen to nothing would otherwise
// contribute zero and change the coverage line.
VisitedNodes int
// Notes is what the cacheable rules noted through rule.Context.Note, per rule and per key, nil when none
// did. A replay restores the notes of the rules it replays, so a warm run counts them as a walked one does.
Notes RuleNotes
// Findings is what the cacheable rules reported. Empty is a real answer: it means the rules ran
// and found nothing, which is exactly the case worth caching since most files are clean.
Findings []LintCacheFinding
// Directives is whether the file has a suppression directive, or names a rule in a disable or enable comment.
// A replay of such a file still reads its comments, so its directives are tallied and its unknown rule names
// reported as a walk's are.
Directives bool
// Withheld is the cacheable rules' findings a directive silenced, each as its rule and the directive's index
// (suppression.Index.SuppressedBy). A replay marks the directive applied for each one whose rule it replays,
// so a directive is used or unused exactly as a walk finds it. The bytes are the entry's, so the indices name
// the same directives (#kdee854).
Withheld []LintCacheWithheld
// Adamic is what the cacheable cohere:adamic rules measured on this file for Adamic readiness, nil when
// the run that produced the entry did not measure readiness. Nil is not "ready": a run that measures
// readiness misses on it (FindingsReuse.lookup), since replaying it would read every unmeasured file as
// having no findings, which is the silent clean this cache must never produce (#drbrp8c).
Adamic *AdamicRecord
}
LintCacheEntry is one file's findings, keyed by what the file contained.
type LintCacheFinding ¶
type LintCacheFinding struct {
RuleName string
Start int32
End int32
// MessageId identifies the message within its rule. Not unique across rules; see above.
MessageId string
// MessageDescription is the rendered sentence, stored because nothing can rebuild it.
MessageDescription string
FixCount int32
SuggestionCount int32
}
LintCacheFinding is one finding, flattened.
rule.Diagnostic carries a *ast.SourceFile belonging to a program that no longer exists by the time this is read back, so the file is implied by the entry rather than stored per finding.
MessageDescription holds the rendered text. An earlier version of this comment said text was "derived from the id when a finding is rendered" and therefore safe to omit. That was untrue, and the field was absent because of it. Nothing in this tree derives text from an id: there is no id-to-text table, and `printRuleDiagnostic` prints `Message.Description` directly. Measured 2026-08-25 over `internal/rules`: 340 message ids, of which 11 build their description with `fmt.Sprintf` from runtime values, so for those one id maps to many strings and no table could exist. A cached finding replaying only the id would print an empty or wrong sentence beside a correct file, range and rule — the right metadata carrying the wrong message, which is the one failure here that looks exactly like success.
The text is stored rather than the 11 being refused because refusing them buys bytes with a standing per-rule obligation, and this format already depends on `ProgramReads` carrying one. The size scales the right way: the artifact grows with a dirty tree, which is the run where this cache saves least, because changed files are recomputed anyway.
Message ids are not unique on their own, which is a second reason the id could never have been the key. `unexpected` carries five distinct descriptions across six core rules, colliding across rules and never within one, so the honest identity of a message is the pair (RuleName, MessageId). Lookups key on the pair even though the text is now stored, so nobody later assumes ids are unique.
Fixes and suggestions are counted rather than stored, and that omission IS sound: a fix is a replacement over a byte range, and replaying one computed against different bytes would corrupt the file it claims to repair. A cached finding is a report, never an edit.
func (LintCacheFinding) MessageKey ¶
func (f LintCacheFinding) MessageKey() (string, string)
MessageKey is the honest identity of a message: the pair, never the id alone.
type LintCacheWithheld ¶
LintCacheWithheld is one cacheable rule's finding a directive silenced: the rule, and the directive's index among the file's directives.
type MemoryFS ¶
type MemoryFS struct {
// contains filtered or unexported fields
}
MemoryFS is a read-only filesystem over files held in memory, for Options.FileSystem: a rule test builds its fixture program from a map rather than from files it writes to a temporary directory and deletes.
The rule-test harness wrote about 16,000 fixture directories to /tmp for one registry run, and deleting them at exit was 3.7s of its 7.6s: the filesystem serializes the unlinks, so deleting them in parallel was slower (#nxgt2ca). A program reads its tree through vfs.FS and nothing else, so a map serves it the same bytes and costs no syscall.
typescript-go has a map-backed test filesystem of its own, vfstest.FromMap, but it is generic, and a generic function cannot cross the shim's linkname. This is the read half of it, which is all a build asks of a filesystem: paths are case-sensitive and slash-separated, nothing is a symbolic link, and every write fails, since a build that writes (an incremental session's build info) has nowhere to write to.
Safe for concurrent use: it is never written after NewMemoryFS returns.
func NewMemoryFS ¶
NewMemoryFS holds files, by absolute slash path, and every directory above them.
func (*MemoryFS) AppendFile ¶
func (m *MemoryFS) AppendFile(tspath.RootedFilePath, string) error
func (*MemoryFS) CaseSensitivity ¶
func (m *MemoryFS) CaseSensitivity() tspath.CaseSensitivity
func (*MemoryFS) DirectoryExists ¶
func (m *MemoryFS) DirectoryExists(directory tspath.RootedDirectoryPath) bool
func (*MemoryFS) FileExists ¶
func (m *MemoryFS) FileExists(fileName tspath.RootedFilePath) bool
func (*MemoryFS) GetAccessibleEntries ¶
func (m *MemoryFS) GetAccessibleEntries(directory tspath.RootedDirectoryPath) vfs.Entries
func (*MemoryFS) ReadFile ¶
func (m *MemoryFS) ReadFile(fileName tspath.RootedFilePath) (string, bool)
ReadFile answers as the disk does: a file's bytes decoded the way the real filesystem decodes them, so a fixture's byte order mark or UTF-16 reaches the compiler as it would from a file.
func (*MemoryFS) Realpath ¶
func (m *MemoryFS) Realpath(fileName tspath.RootedPath) tspath.RootedPath
type Options ¶
type Options struct {
// ConfigFileName is the tsconfig to build from. Relative paths resolve against CurrentDirectory.
ConfigFileName string
// CurrentDirectory is the working directory paths resolve against. Empty means the process's own.
CurrentDirectory string
// LibraryPath overrides where the lib.*.d.ts files come from.
//
// Empty means the ones embedded in our pinned typescript-go, which is the correct default: the
// checker and its libs are one version, and mixing a newer lib into an older checker produces
// hundreds of spurious TS2550/TS2554 (measured: 118 of them on the ahra tree). Point this at a
// specific lib directory only when deliberately reproducing another toolchain's result.
LibraryPath string
// Checkers is how many checkers to create. Zero means the compiler's own default.
//
// It is the count that parallelizes the checking phase, not the graph build: construction is
// parse and resolve, which this does not touch. Raising it trades memory for wall clock, since
// each checker holds its own type tables.
Checkers int
// SingleThreaded forces one checker instead of one per core. Off by default; useful when a
// measurement needs to be reproducible, or when a caller wants types from different files to be
// comparable (types from two checkers cannot be mixed).
SingleThreaded bool
// Overlay is file contents that stand in for the disk's, by absolute path: an editor's unsaved
// buffer, so the graph describes the text about to be saved rather than the text being replaced.
// Nil reads everything from disk.
Overlay map[string]string
// Inputs, when set, is told every path the build reads or looks for on disk, so the run cache can
// record what this build depended on by observation rather than by a list. Nil records nothing and
// costs nothing.
Inputs *InputRecorder
// ContentPack, when set, serves files whose stat is unchanged from the bytes recorded by earlier runs,
// so they are not opened, and collects what this build read from disk. Nil reads everything from disk.
ContentPack *ContentPack
// LibraryParses, when set, serves the bundled lib files from one parse shared by every program built
// with it, rather than parsing them for each program. Nil parses them per program, as before. See
// LibraryParses.
LibraryParses *LibraryParses
// Timing, when set, is filled with what the build cost by part. Nil reads no clocks and wraps
// nothing, so a build nobody is timing costs what it did before this existed. See GraphTiming.
Timing *GraphTiming
// FileSystem, when set, is the disk the build reads instead of the real one: a MemoryFS, for a test
// that builds its fixture from a map. The bundled lib files are laid over it as over the real disk.
// Nil reads the real disk, as before.
FileSystem vfs.FS
// Yielded is files this tsconfig includes that a nearer one owns, by absolute path: they stay in the
// program, so every type question about them is answered, and leave ProjectFiles, so no phase lints
// them or reports their diagnostics here. Nil yields nothing. See ReadProjectConfig, and discovery's
// ownership in the command.
Yielded map[string]struct{}
// CheckedStats, when set, answers the build's existence checks and stats from what the run cache's check found
// at each path it statted this run, rather than asking the disk again. Beneath the input recorder, so a run
// cache still records every path the build asked about. Nil asks the disk. See StatSnapshot.
CheckedStats *StatSnapshot
// Listings, when set, serve the build's directory listings from ones already read, discovery's, rather
// than reading those directories again. Beneath the input recorder, so a run cache still records every
// listing the build asked for. Nil reads every listing from disk. See DirectoryListings.
Listings *DirectoryListings
}
Options controls how a graph is built.
type PathAnchor ¶
type PathAnchor struct {
// contains filtered or unexported fields
}
PathAnchor names paths relative to a project root, so that what the caches store and hash does not depend on how the root was spelled (#547dhjz).
One directory can have several absolute spellings: on macOS /tmp is a symbolic link to /private/tmp, and a project under a linked directory is reached through the link from one shell and through its target from another. Every cache entry and fingerprint used to hold the spelled path, and the cache directory, inside the project, is the same directory under every spelling. So a run from the other spelling missed everything and then rewrote the files the first spelling's next run would read: alternating, every run checked every file, and nothing said why.
A path under the root, reached through either the spelling this run was given or the root's resolved target, is stored as "root:" and its path below the root, slash separated. Any other path is stored as it is. The compiler names files by a key that is lowercased on a case-insensitive file system, so both roots are matched in that form too. Reading back, a stored path is joined to the root as this run spells it, so everything in memory, and everything printed, stays in the caller's spelling.
func NewPathAnchor ¶
func NewPathAnchor(root string) PathAnchor
NewPathAnchor anchors paths at root, as this run spells it. A root whose target cannot be resolved is anchored at its spelling alone, which is what every run did before.
func (PathAnchor) Resolved ¶
func (anchor PathAnchor) Resolved() string
Resolved is the root's target with every symbolic link resolved, or its spelling when that failed. A key that must be the same for every spelling of one directory is built from this.
func (PathAnchor) Root ¶
func (anchor PathAnchor) Root() string
Root is the root as this run spells it.
func (PathAnchor) Spelled ¶
func (anchor PathAnchor) Spelled(stored string) string
Spelled is a stored path as this run spells it: one below the root joined to the root as given, and any other as it was stored.
func (PathAnchor) Stable ¶
func (anchor PathAnchor) Stable(path string) string
Stable is path as the caches store and hash it: below the root, "root:" and its slash path from there, by either spelling of the root in either case; anywhere else, unchanged. The zero anchor changes nothing.
type ProjectConfig ¶
type ProjectConfig struct {
// FileNames are the files the tsconfig includes, absolute and cleaned to the platform's separator.
FileNames []string
// References are the tsconfigs it references, absolute: a reference to a directory names the
// tsconfig.json in it, as tsc reads one.
References []string
}
ProjectConfig is what a tsconfig says belongs to it, read without building its program: the files it includes and the tsconfigs it references. Discovery reads one per project, to find solution roots and to give each file to the nearest tsconfig that includes it.
func ReadProjectConfig ¶
func ReadProjectConfig(configFileName string) (ProjectConfig, error)
ReadProjectConfig reads a tsconfig the way Build does, and fails where Build would refuse it, so a config discovery cannot read is one the project's own run reports.
type Readiness ¶
type Readiness struct {
// Options is each set rule's raw options, as the set writes them.
Options map[string][]json.RawMessage
}
Readiness is the cohere:adamic set a readiness run measures every file against (#drbrp8c).
A file is Adamic-ready when the type phase reports nothing in it and no cohere:adamic rule finds anything in it, at the set's own options, whatever the project's chain says: Adamic will not read a disable comment or a departure, so readiness cannot either. So a rule the chain enables is counted as it runs, its findings counted before suppression, and a rule the chain leaves off or never names runs measure-only: its findings are counted and never reported, never fail the run, and never carry a fix to the edit engine.
A rule the chain enables with options of its own is counted at the chain's options, not the set's. Today only ban-ts-comment can differ, and only in a chain that sets it; running it twice to measure the set's options is the refinement, if a project shows the difference matters.
type Result ¶
type Result struct {
Diagnostics []rule.Diagnostic
Coverage Coverage
// Timings is per-rule cost, populated only when the caller asked for it by setting
// Graph.CollectTimings. Nil otherwise, so an ordinary run pays nothing for the instrument.
Timings *Timings
// FilesReplayed is how many files this walk served from the findings cache rather than walking
// with every rule. Zero without a cache. The lint line reports it, so a reader can tell a walked
// verdict from a remembered one.
FilesReplayed int
// TypeAwareRerun and ShapeKeyedRerun are how many of the replayed files ran their content-keyed and
// their shape-keyed type-aware rules again, because something they import changed. The difference
// between the two is what shapes saved.
TypeAwareRerun int
ShapeKeyedRerun int
// DesignSystemRerun is how many of the replayed files ran their design-system rules again, because a
// stylesheet the design system read changed or the last run's could not be keyed.
DesignSystemRerun int
// DerivedRerun is how many of the replayed files ran their derived rules again, because a rule's program
// fingerprint moved, or the file's type fingerprint did under a derived rule that reads types.
DerivedRerun int
// FilesOnForeignCheckers is how many files were walked on a checker other than the one that owns them:
// taken by a worker whose own group was done, or every file under WalkOnForeignCheckers. See walkQueue.
FilesOnForeignCheckers int
// Adamic is each walked file's readiness measurement, by file name, when Graph.Readiness asked for one: what
// the walk ran, with what the findings cache replayed for the rest. A file whose record is nil was never
// measured, and reads unmeasured, not ready. See Readiness.
Adamic map[string]*AdamicRecord
// Notes is what each file's rules noted through rule.Context.Note, by file name. Kept per file and
// per rule rather than summed, because the findings cache stores a file's notes beside its findings
// and a refresh re-walks only some of a file's rules: a total could not be split back apart.
Notes map[string]RuleNotes
}
Result is the findings of one walk, and the coverage that produced them.
type RuleAccount ¶
type RuleAccount struct {
Name string
// Listened is whether the rule accepted the file. A rule that declines returns no listeners,
// which is the cheapest thing it can do and the thing most worth being able to see.
Listened bool
// DeclinedBecause names why a rule did not run when the configuration made that choice rather
// than the rule. Empty when the rule itself declined, which is a different fact.
DeclinedBecause string
// NodeKinds is what the rule asked to hear about, sorted so two runs read the same.
NodeKinds []string
// NodesOffered is how many nodes the walk called it for.
NodesOffered int
// Duration is setup plus listeners for this one file.
Duration time.Duration
// Findings are what it reported and what a directive withheld, kept apart so a suppressed
// finding is visible as suppressed rather than absent.
Findings []Finding
Suppressed []Finding
}
RuleAccount is what one rule did on one file.
Declining is recorded as deliberately as reporting, because "the rule looked and had nothing to say" and "the rule never looked" are different facts that produce identical output everywhere else. That ambiguity kept four rules dead for months in the gate this tool replaces.
type RuleTiming ¶
type RuleTiming struct {
Name string
// SetupCPU is CPU inside the rule's Run function, deciding what to listen to.
SetupCPU time.Duration
// ListenerCPU is CPU inside the rule's listeners, walking nodes.
ListenerCPU time.Duration
// FilesListened is how many files the rule accepted, and FilesDeclined how many it refused.
//
// Declining is the cheapest and most valuable thing a rule can do, so a rule that declines
// nothing is worth seeing: registering for a very common node kind and returning early is
// legitimate, and it is also how a cheap-looking rule becomes expensive at scale.
FilesListened int
FilesDeclined int
// NodesOffered is how many nodes the walk called this rule's listeners for.
NodesOffered int
// Findings is how many diagnostics the rule reported, before suppression.
Findings int
}
RuleTiming is what one rule cost over a run, in CPU.
CPU, not wall time, because wall time lied (#8qyzmxw). Each listener call used to be charged its wall time, and a goroutine descheduled mid-call was charged for the time it spent off the CPU. Under load that is most of the number: the better-tailwindcss family read about 1.2s on the old table, while eight interleaved cold pairs with the family off and on differed by a median of 0.049s, and a CPU profile put it under 1%. A rule called most often was interrupted most often, so the table ranked call counts and called them cost.
Setup and listener time are kept apart because they are different defects. A rule burning time in Run and then declining the file is doing expensive work to decide it has nothing to do, which is the shape that made consistency-no-abbreviated-identifier 2,580ms of a 3,700ms run: it resolved a scope lookup on every identifier before testing whether the name even looked abbreviated. A cheap structural gate took it to 152ms, 17x, findings byte-identical. A rule burning time in its listeners is doing expensive work per node, which is a different fix.
func (RuleTiming) TotalCPU ¶
func (r RuleTiming) TotalCPU() time.Duration
TotalCPU is what the rule cost altogether.
type RunCache ¶
type RunCache struct {
// Version is the manifest format. A manifest written by a different format is a miss, never a
// best-effort read: the fields would be read from the wrong places and still parse.
Version int
// Key covers everything about the run that is not a file on disk: the flags, the binary's
// identity, the working directory. Two runs that differ in any of those are different runs even
// over identical files, and must not replay each other.
Key string
// KeyParts is Key's parts, so a run under another key can say which part moved. See RunCacheKeyParts.
KeyParts RunCacheKeyParts
// Inputs is every file and directory the run depended on, with the signature it had.
Inputs []RunCacheInput
// Output is exactly what the run printed to stdout, replayed byte for byte on a hit.
Output []byte
// Errors is what it printed to stderr. Kept apart rather than interleaved, so each stream replays
// to the stream it was written to.
Errors []byte
// RecordedUnixNanoseconds is when the replayed run happened. A replay prints that run's report,
// durations included, so it has to be able to say when those numbers are from.
RecordedUnixNanoseconds int64
// ExitCode is the process exit code to return on a hit. A cache that replayed the output and
// exited zero on a failing tree would pass CI while printing a failure.
ExitCode int
// Summary is the run's summary as the command encodes it, opaque here. The footer a run ends with
// carries that run's time, so it is never in Output; a replay renders its own from this, with the
// replay's time, rather than reprinting a recorded one that would claim the recorded run's.
Summary []byte
}
The run cache: replay a whole run's output without building anything, when every input it depended on is provably unchanged.
This is a different cache from the lint findings cache beside it, and the difference is the whole reason it exists. That cache was measured at about 9% and left unwired because it still walked the tree: it could only skip map lookups the one-traversal design had already made nearly free. This one skips the walk AND the graph build, which is roughly 0.45s on the ahra tree before any rule runs. Its prize is the entire run, so the earlier verdict says nothing about it.
The rule it has to keep ¶
A stale run cache that replays a clean verdict over a tree that is no longer clean is the worst thing this tool could do, and it would look exactly like success. So the cache answers "hit" only when it can prove every input is unchanged, and any doubt at all is a miss: an unreadable manifest, a format version it does not recognize, a single entry that does not match. A miss costs one ordinary run. A false hit costs a lie.
The input it cannot see, and how it sees it anyway ¶
The program's file list only exists after the graph is built, which is the cost this cache exists to skip. So a full run records the list and the next run re-stats what was recorded. That is sound for a file edited or deleted. It is blind to a file ADDED: a new source file under an include glob is an input the recorded list has never heard of, and re-statting the old list finds nothing wrong.
Directories close that gap. Adding or removing an entry changes the mtime of the directory that holds it, so the manifest records every directory the program draws from and re-stats those too. Every directory, not the include roots: verified on macOS, a file added in a subdirectory changes that subdirectory's mtime and leaves its parent's alone, so watching only the roots would miss anything added one level down. On the ahra tree that is 1,725 directories beside 10,360 files, and statting all 12,085 costs about 13ms across eight workers.
What "unchanged" means for one entry ¶
Size and modification time, at nanosecond resolution. A file whose bytes changed and whose mtime did not is the case this cannot see, and it requires something deliberately resetting the mtime: `touch -r`, some archive extractors, a tool that preserves timestamps on copy. Content hashing every file would close it at a cost of reading 110 MB per run, which is the run this cache exists to avoid. The trade is stated here so nobody discovers it by surprise; Record also hashes each file's content so a later change of policy has the data to tighten this without a format change.
func RecordRunCache ¶
func RecordRunCache(key string, files []string, extraDirectories []string, absent []string, probed []string, output []byte, exitCode int, readSince time.Time) (*RunCache, error)
RecordRunCache captures a run that just finished.
files is every path the run depended on that existed, and directories among them are fine: whether an entry is a file or a directory is read from the disk here, never inferred from which list it arrived in. An earlier version stamped every entry of files as a file, so a directory the compiler had probed was recorded as one and every check then reported it "changed between file and directory". The cache never hit, which is the most deceptive way for a cache to fail: it runs, it misses, and it looks merely cold. The probe that found it was the untouched-tree control.
The directory holding each file is added here too, so a caller cannot forget them and quietly ship a cache blind to added files. extraDirectories adds more, and absent lists paths the run looked for and did not find, which must stay absent for a hit. probed lists present paths the run only asked whether they exist; a directory among them that holds no file the run read and is not among extraDirectories is recorded ExistenceOnly.
readSince is when the run began reading its inputs. An input changed after it may have been read before the change, so the stat taken here would sign the new bytes against a verdict computed from the old ones, and the next run would replay that verdict over a tree it does not describe: a file saved in an editor while cohere runs is exactly this. Such a run is not recorded. The zero time checks nothing.
func (*RunCache) ChangedInput ¶
ChangedInput reports the first recorded input whose signature no longer matches the disk, nil when every one still does. It is Check without the key, so --cache-dump can say which input keeps a run from replaying rather than leaving a miss silent (#r9jevk9).
The input named is the earliest recorded one that moved, whichever worker found it, so a run that misses says the same thing every time rather than the first thing a worker happened to reach (#547dhjz).
func (*RunCache) Check ¶
Check reports whether a stored run can be replayed, returning nil only on a provable hit.
It returns the stored run only when the key matches and every input still has the signature it was recorded with. The first mismatch is the answer; there is no partial hit, because a run's verdict is a property of all its inputs together.
func (*RunCache) CheckNoting ¶
func (c *RunCache) CheckNoting(key string, snapshot *StatSnapshot) error
CheckNoting is Check, noting in snapshot what it found at every path it statted, so the build and the content pack can use stats this run already took rather than take each again (#kdee854). A nil snapshot notes nothing.
type RunCacheInput ¶
type RunCacheInput struct {
Path string
// Directory is true for an entry recorded to catch files added beneath it. Kept explicit
// rather than inferred, because a path that was a directory and is now a file is a change.
Directory bool
// Exists is false for an input that was absent when recorded and must stay absent. A config the
// run looked for and did not find is still an input: creating it changes the run.
Exists bool
Size int64
ModifiedNanoseconds int64
// ChangedNanoseconds and Inode catch what the modification time cannot: different bytes of the same
// size under a restored modification time, as cp -p, rsync -t and touch -r leave a file. Nothing in
// userland can set a change time, and a file replaced by a rename has a new inode. Zero where the
// platform's stat does not give them.
ChangedNanoseconds int64
Inode uint64
// ExistenceOnly is set on a directory the run only asked whether it exists (see InputRecorder.Inputs),
// which matches while it is still the same directory, by inode, whatever was added to it since. Module
// resolution asks it of every directory from the project up to the root, and on a busy machine /tmp and
// the like change many times a second: checked at full signature, they cost a replay on a tree nothing
// had touched (#q51f02a). A package.json or node_modules appearing in one is its own absent input.
ExistenceOnly bool
}
RunCacheInput is one input's signature.
type RunCacheKeyParts ¶
type RunCacheKeyParts struct {
// Binary is the running binary: its resolved path, size and modification time, and the format and
// platform it reads. Keyed by path, size and time rather than by hashing its bytes: it is about 70 MB,
// reading it every run would spend the budget this cache exists to save, and a rebuilt binary has a
// new modification time, which is all this needs.
Binary string
// Directory is the working directory the caller resolved, the project root's target.
Directory string
// Arguments is the command line.
Arguments string
// Facts is anything else the run's output depends on that is not a file the build reads: config paths
// the command resolved, and results it computes from sources the build never sees. Order matters, and
// an empty fact still counts.
Facts string
}
RunCacheKeyParts is what identifies a run apart from its files, in four parts hashed apart, so a run that misses can say which one moved rather than that something did (#547dhjz).
func RunCacheKeyPartsOf ¶
func RunCacheKeyPartsOf(arguments []string, workingDirectory string, facts ...string) (RunCacheKeyParts, error)
RunCacheKeyPartsOf hashes each part of a run's identity.
func (RunCacheKeyParts) Changed ¶
func (parts RunCacheKeyParts) Changed(recorded RunCacheKeyParts) string
Changed says which parts differ from recorded, in words, or that the record kept no parts to compare, as one written before they were kept.
func (RunCacheKeyParts) Key ¶
func (parts RunCacheKeyParts) Key() string
Key is the parts as one key.
type SignatureEntry ¶
type SignatureEntry struct {
Version string
Signature string
// Syntax is the hash of the file's text with its function bodies elided. Computed from the parse
// alone, so it costs no checker and is filled in for any entry that lacks it, a seeded one included.
Syntax string
// Abandoned is set on a content-keyed entry whose emit was still running at GraduateSignatures' deadline, so
// these bytes are never emitted again. Edited bytes start a fresh entry without it.
Abandoned bool
}
SignatureEntry is one file's recorded shape, with the version of the bytes it was computed from.
type StatSnapshot ¶
type StatSnapshot struct {
// contains filtered or unexported fields
}
StatSnapshot holds what the run cache's check found at every path it statted: a file or a directory with its stat, or nothing there. A run the check could not replay has just statted every input it recorded, the files the last build read, the directories holding them and the paths it looked for and did not find, and the build that follows asks the disk the same questions again. Two consumers read the answers instead (#kdee854):
- the content pack validates a file it serves against the check's stat rather than taking one: about 18ms of a graph phase's 32ms of serving on ahra, at 12 workers;
- the build's existence checks and stats are answered from it (Options.CheckedStats), most of all module resolution's, which asks whether a path exists about 35,000 times on an ahra edit run and is told no for most of them, two stats each beneath the recorder.
It is sound because nothing the run reads can be newer than its clock. The run-cache clock starts before the check (startRunCacheClock), so a path that changed after its stat here, a file edited or a file created where the build was told nothing is, moved its own times or its directory's past the run's readSince, and either RecordRunCache refuses to record that run or the next run's check finds the absent path present. Every other cache keys on what the build actually saw. The run reports the tree as it stood at the check, and nothing replays it past a later change.
The one writer that clock cannot see is the run itself. A graph rebuilt after the fix phase rewrote files must not read through a snapshot taken before the rewrite (distrustCheckStats in the command).
func NewStatSnapshot ¶
func NewStatSnapshot() *StatSnapshot
NewStatSnapshot returns an empty snapshot for a check to fill.
type StoredDiagnostic ¶
type StoredDiagnostic struct {
Pos, End int
Code int32
Category int64
MessageKey string
MessageArgs []string
Source string
MessageText string
ReportsUnnecessary bool
ReportsDeprecated bool
SkippedOnNoEmit bool
NoFile bool
Chain []StoredDiagnostic
Related []StoredDiagnostic
}
StoredDiagnostic is a compiler diagnostic in the file it belongs to, as the compiler's own build info stores one: location, code, category, message key and arguments or external text, chain and related information. NoFile marks a part the compiler gave no file.
type Timings ¶
type Timings struct {
// Account is the walk's CPU as a whole: what the workers spent, what of it no rule did, and what the
// rest of the process did meanwhile.
Account CPUAccount
// contains filtered or unexported fields
}
Timings collects per-rule cost across a run.
The cost is the walk thread's own CPU clock, read around every call into a rule. Each walk worker is locked to its OS thread for the walk, so the clock it reads before a call and after it is the same thread's, and a thread's CPU clock does not move while the thread waits: a rule that sleeps, blocks, or is descheduled costs nothing here, which is the property the wall clock lacked.
A CPU profile was the other candidate, and it was built and measured first. On macOS it does not work: the profiling signal is process-wide and lands on whichever thread the kernel picks, so a planted rule spinning 330ms of CPU across 80 files was billed 70 to 120ms, most of its samples landing on threads that were not running it (golang/go#14434 and #57722; Linux has per-thread timers since Go 1.18, macOS still does not). A table that is right only on Linux is wrong where it is read.
The clock's price is about 100ns a read on darwin/arm64, so a call costs two reads where the wall clock cost two 28ns ones. That cost is measured on each worker before its walk (see calibrateFrame) and subtracted from every call, and what was subtracted is reported as the instrument's own CPU rather than left inside some rule's number.
func NewTimings ¶
NewTimings returns a collector ready for a run.
func (*Timings) SetForTest ¶
SetForTest populates one rule's measured cost, for tests that need a known table without running a program. Not used outside tests.
func (*Timings) SetSharedForTest ¶
SetSharedForTest populates one family of shared derivations' cost, for tests. Not used outside tests.
func (*Timings) SharedCPU ¶
SharedCPU is what each family of cached derivations cost across the run, keyed by family.
func (*Timings) Sorted ¶
func (t *Timings) Sorted() []RuleTiming
Sorted returns every rule's timing, most expensive first.
Sorted by cost so an outlier is obvious without arithmetic, with the name as a tiebreak so two runs over the same tree produce the same order and a diff between them means something.
type TypeDiagnosticParts ¶
type TypeDiagnosticParts struct {
Syntactic []*ast.Diagnostic
Bind []*ast.Diagnostic
Semantic []*ast.Diagnostic
}
TypeDiagnosticParts is a check's syntactic, bind and semantic diagnostics, kept apart so the semantic ones can be recorded per file. With any syntactic diagnostic the check stops there, as the compiler's own does, and the other two are empty.
func (TypeDiagnosticParts) All ¶
func (p TypeDiagnosticParts) All() []*ast.Diagnostic
All is the parts in the order AllDiagnostics returns them.
type TypeDiagnosticsReuse ¶
type TypeDiagnosticsReuse struct {
// contains filtered or unexported fields
}
TypeDiagnosticsReuse is one run's use of the section: what was stored, and what this run records.
func NewTypeDiagnosticsReuse ¶
func NewTypeDiagnosticsReuse(stored *TypesSection, key [sha256.Size]byte) *TypeDiagnosticsReuse
NewTypeDiagnosticsReuse wraps what the table held, which may be nil, for a run whose compiler options hash to key. Only a section recorded under the same key is replayed, and what this run records carries it.
func (*TypeDiagnosticsReuse) FinishGlobals ¶
func (r *TypeDiagnosticsReuse) FinishGlobals(clean bool)
FinishGlobals says whether this run saw any global diagnostic. Until it is told none, what it recorded may only be read as a full check: a run that saw one records nothing to replay.
func (*TypeDiagnosticsReuse) Recorded ¶
func (r *TypeDiagnosticsReuse) Recorded() *TypesSection
Recorded is the section this run leaves for the next, nil when it recorded nothing.
func (*TypeDiagnosticsReuse) Replayed ¶
func (r *TypeDiagnosticsReuse) Replayed() int
Replayed is how many files' semantic diagnostics this run took from the section.
func (*TypeDiagnosticsReuse) ReplayedAmong ¶
func (r *TypeDiagnosticsReuse) ReplayedAmong(files []*ast.SourceFile) int
ReplayedAmong is how many of files had their semantic diagnostics taken from the section. The check covers the whole program whatever the run's scope, so Replayed counts files a scoped run never reports on, and a line saying "3955 of 1 files" read as nonsense (#rxqptqp).
func (*TypeDiagnosticsReuse) Unchanged ¶
func (r *TypeDiagnosticsReuse) Unchanged() bool
Unchanged reports whether what this run recorded is what the table already holds: every file replayed under the same key, and the global verdict the same. Writing it back would rewrite the table for nothing.
type TypesEntry ¶
type TypesEntry struct {
Fingerprint [sha256.Size]byte
Diagnostics []StoredDiagnostic
}
TypesEntry is one file's semantic diagnostics and the fingerprint they hold under.
type TypesSection ¶
type TypesSection struct {
Version int
// Key is what the diagnostics depend on beyond the files the fingerprints cover: the tsconfig and
// everything it extends. See NewTypeDiagnosticsReuse.
Key [sha256.Size]byte
// GlobalsClean is whether the run that recorded it saw no global diagnostics. Only then can a run that
// skips files report what a run that checked them all would.
GlobalsClean bool
// Entries is each project file's semantic diagnostics, by file name, under the shape fingerprint they
// were computed against. A file with none has an entry with none.
Entries map[string]TypesEntry
}
TypesSection is the types phase's section of the cache table.
Source Files
¶
- cache_table.go
- content_pack.go
- content_pack_map_unix.go
- cpu_clock_unix.go
- dependents.go
- design_system_key.go
- directory_listings.go
- explain.go
- file_identity_linux.go
- findings_reuse.go
- fused_check.go
- graph_timing.go
- incremental.go
- input_recorder.go
- library_parses.go
- lint_cache.go
- memory_fs.go
- options_diagnostics.go
- overlay.go
- path_anchor.go
- program.go
- project_config.go
- read_identities.go
- readiness.go
- run_cache.go
- signatures.go
- stat_snapshot.go
- timing.go
- type_diagnostics_reuse.go
- type_fingerprints.go
- walk.go