@ttsc/evidence 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +279 -0
- package/lib/index.d.ts +65 -0
- package/lib/index.js +87 -0
- package/lib/internal/loadPrismaModels.d.ts +1 -0
- package/lib/internal/loadPrismaModels.js +197 -0
- package/lib/internal/loadSwaggerOperations.d.ts +1 -0
- package/lib/internal/loadSwaggerOperations.js +156 -0
- package/lib/internal/normalizeSwaggerDocument.d.ts +1 -0
- package/lib/internal/normalizeSwaggerDocument.js +7 -0
- package/lib/structures/ITtscEvidenceDocumentedConfig.d.ts +32 -0
- package/lib/structures/ITtscEvidenceDocumentedConfig.js +2 -0
- package/lib/structures/ITtscEvidenceGraphClaim.d.ts +14 -0
- package/lib/structures/ITtscEvidenceGraphClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +102 -0
- package/lib/structures/ITtscEvidenceGraphClaimBase.js +2 -0
- package/lib/structures/ITtscEvidenceGraphConfig.d.ts +19 -0
- package/lib/structures/ITtscEvidenceGraphConfig.js +2 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownClaim.d.ts +40 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +76 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +65 -0
- package/lib/structures/ITtscEvidenceGraphPrismaClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +75 -0
- package/lib/structures/ITtscEvidenceGraphPrismaReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphReference.d.ts +14 -0
- package/lib/structures/ITtscEvidenceGraphReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +73 -0
- package/lib/structures/ITtscEvidenceGraphReferenceBase.js +2 -0
- package/lib/structures/ITtscEvidenceGraphSwaggerReference.d.ts +39 -0
- package/lib/structures/ITtscEvidenceGraphSwaggerReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +54 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +83 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.js +2 -0
- package/lib/structures/index.d.ts +13 -0
- package/lib/structures/index.js +29 -0
- package/lib/typings/ITtscLintContributorRules.d.ts +34 -0
- package/lib/typings/ITtscLintContributorRules.js +2 -0
- package/lib/typings/ITtscLintRuleOptionsMap.d.ts +20 -0
- package/lib/typings/ITtscLintRuleOptionsMap.js +2 -0
- package/lib/typings/TtscEvidenceGraphMarkdownSymbol.d.ts +32 -0
- package/lib/typings/TtscEvidenceGraphMarkdownSymbol.js +2 -0
- package/lib/typings/TtscEvidenceGraphPrismaSymbol.d.ts +38 -0
- package/lib/typings/TtscEvidenceGraphPrismaSymbol.js +2 -0
- package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.d.ts +64 -0
- package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.js +2 -0
- package/lib/typings/index.d.ts +5 -0
- package/lib/typings/index.js +21 -0
- package/native/base.go +298 -0
- package/native/config.go +798 -0
- package/native/declaration.go +240 -0
- package/native/documented.go +353 -0
- package/native/exports.go +484 -0
- package/native/glob.go +207 -0
- package/native/graph.go +1409 -0
- package/native/health.go +45 -0
- package/native/hints.go +261 -0
- package/native/imports.go +373 -0
- package/native/inputs.go +125 -0
- package/native/loader.go +424 -0
- package/native/markdown.go +423 -0
- package/native/model.go +296 -0
- package/native/prisma.go +719 -0
- package/native/prisma_cache.go +213 -0
- package/native/prisma_locator.go +417 -0
- package/native/singular.go +369 -0
- package/native/swagger.go +331 -0
- package/native/swagger_cache.go +274 -0
- package/native/todo.go +138 -0
- package/native/typescript.go +1199 -0
- package/native/typescript_cache.go +96 -0
- package/package.json +64 -0
package/native/graph.go
ADDED
|
@@ -0,0 +1,1409 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"sort"
|
|
7
|
+
"strings"
|
|
8
|
+
|
|
9
|
+
"github.com/samchon/ttsc/packages/lint/rule"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
type graphRule struct{}
|
|
13
|
+
|
|
14
|
+
func (graphRule) Name() string { return graphRuleName }
|
|
15
|
+
|
|
16
|
+
func (graphRule) NeedsTypeChecker() bool { return false }
|
|
17
|
+
|
|
18
|
+
func (graphRule) Check(ctx *rule.ProjectContext) {
|
|
19
|
+
if ctx == nil {
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
typeScriptInventories.beginCycle()
|
|
23
|
+
cycle := &graphCycleState{}
|
|
24
|
+
// SetState survives a later project finding and belongs only to this
|
|
25
|
+
// Program cycle. File rules can therefore coordinate diagnostics even when
|
|
26
|
+
// the graph itself fails, while Hints remains protected by the host's
|
|
27
|
+
// passed-only publication gate.
|
|
28
|
+
ctx.SetState(cycle)
|
|
29
|
+
config, problems := decodeGraphConfig(ctx.Options)
|
|
30
|
+
if len(problems) != 0 {
|
|
31
|
+
reportProblems(ctx, problems)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
config = enabledGraphConfig(config)
|
|
35
|
+
if len(config.Claims) == 0 {
|
|
36
|
+
cycle.Corpus = graphCorpus{Config: config}
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
root := evidenceProjectRoot(ctx.Identity)
|
|
40
|
+
if root == "" {
|
|
41
|
+
ctx.Report("Evidence graph could not resolve the project root. Run ttsc with a project config or explicit project root so project-relative evidence globs have one stable base.")
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
info, err := os.Stat(root)
|
|
45
|
+
if err != nil || !info.IsDir() {
|
|
46
|
+
ctx.Report("Evidence graph project root '" + root + "' is not a readable directory. Fix the ttsc project identity before evaluating evidence globs.")
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
// Every population is anchored before anything is read, so a loader, a
|
|
50
|
+
// diagnostic, and the corpus the editor receives all speak of one resolved
|
|
51
|
+
// base rather than each re-deriving it from the author's spelling.
|
|
52
|
+
resolveGraphBases(root, &config)
|
|
53
|
+
|
|
54
|
+
// A claim with no selected own unit has nothing that can own an
|
|
55
|
+
// acknowledgement. Materialize only claim-side populations first, so an
|
|
56
|
+
// inactive claim cannot make its reference loaders perform work or report
|
|
57
|
+
// diagnostics.
|
|
58
|
+
typescript := loadTypeScriptInventories(
|
|
59
|
+
root,
|
|
60
|
+
ctx.Sources,
|
|
61
|
+
claimPopulationConfig(config, artifactTypeScript),
|
|
62
|
+
)
|
|
63
|
+
markdownClaims, markdownClaimProblems := loadMarkdownInventories(
|
|
64
|
+
root,
|
|
65
|
+
claimPopulationConfig(config, artifactMarkdown),
|
|
66
|
+
)
|
|
67
|
+
prismaClaims, prismaClaimProblems := loadPrismaInventories(
|
|
68
|
+
root,
|
|
69
|
+
claimPopulationConfig(config, artifactPrisma),
|
|
70
|
+
)
|
|
71
|
+
problems = append(problems, markdownClaimProblems...)
|
|
72
|
+
problems = append(problems, prismaClaimProblems...)
|
|
73
|
+
config = activeGraphConfig(config, markdownClaims, prismaClaims, typescript)
|
|
74
|
+
extendTypeScriptInventories(root, ctx.Sources, config, typescript)
|
|
75
|
+
markdown, markdownProblems := loadMarkdownInventories(root, config)
|
|
76
|
+
prisma, prismaProblems := loadPrismaInventories(root, config)
|
|
77
|
+
swagger, swaggerProblems := loadSwaggerInventories(root, config)
|
|
78
|
+
problems = append(problems, markdownProblems...)
|
|
79
|
+
problems = append(problems, prismaProblems...)
|
|
80
|
+
problems = append(problems, swaggerProblems...)
|
|
81
|
+
loader := newTypeScriptLoader(root, typescript)
|
|
82
|
+
states, stateProblems := materializeClaimStates(
|
|
83
|
+
config,
|
|
84
|
+
markdown,
|
|
85
|
+
prisma,
|
|
86
|
+
swagger,
|
|
87
|
+
typescript,
|
|
88
|
+
loader,
|
|
89
|
+
)
|
|
90
|
+
problems = append(problems, stateProblems...)
|
|
91
|
+
problems = append(problems, evaluateEvidenceGraph(states, loader)...)
|
|
92
|
+
reportProblems(ctx, problems)
|
|
93
|
+
if len(problems) == 0 {
|
|
94
|
+
// Published only on a clean evaluation, because the host reads state
|
|
95
|
+
// from a rule that passed and reporting anything marks this one failed
|
|
96
|
+
// (`linthost/hints.go:147-149`, `linthost/project_engine.go:68-77`).
|
|
97
|
+
// Setting it unconditionally would not widen the gate; it would only
|
|
98
|
+
// hide where the gate is.
|
|
99
|
+
cycle.Corpus = graphCorpus{
|
|
100
|
+
Config: config,
|
|
101
|
+
Markdown: markdown,
|
|
102
|
+
Prisma: prisma,
|
|
103
|
+
Swagger: swagger,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func init() {
|
|
109
|
+
rule.RegisterProject(graphRule{})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
func evidenceProjectRoot(identity rule.ProjectIdentity) string {
|
|
113
|
+
for _, candidate := range []string{
|
|
114
|
+
identity.PhysicalProjectRoot,
|
|
115
|
+
identity.LogicalProjectRoot,
|
|
116
|
+
identity.ExplicitProjectRoot,
|
|
117
|
+
identity.InvocationCwd,
|
|
118
|
+
} {
|
|
119
|
+
if candidate == "" {
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
absolute, err := filepath.Abs(candidate)
|
|
123
|
+
if err == nil {
|
|
124
|
+
return filepath.Clean(absolute)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return ""
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// claimPopulationConfig removes every reference and other artifact kind from
|
|
131
|
+
// the loader input used to decide activation.
|
|
132
|
+
//
|
|
133
|
+
// This is a loading boundary, not merely an evaluation filter: a claim cannot
|
|
134
|
+
// become inactive because a failed reference was inspected before the claim's
|
|
135
|
+
// own selected host population was known.
|
|
136
|
+
func claimPopulationConfig(
|
|
137
|
+
config graphConfig,
|
|
138
|
+
kind artifactKind,
|
|
139
|
+
) graphConfig {
|
|
140
|
+
claims := make([]claimSpec, 0, len(config.Claims))
|
|
141
|
+
for _, claim := range config.Claims {
|
|
142
|
+
if claim.Type != kind {
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
claim.References = nil
|
|
146
|
+
claims = append(claims, claim)
|
|
147
|
+
}
|
|
148
|
+
return graphConfig{Claims: claims}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// activeGraphConfig omits healthy claim populations that contain no own unit
|
|
152
|
+
// selected by the claim's symbol set.
|
|
153
|
+
//
|
|
154
|
+
// TypeScript, Prisma, and Markdown are the three claim-capable artifact kinds.
|
|
155
|
+
// A healthy zero-file match is empty and therefore inactive, including when a
|
|
156
|
+
// typo caused it. Unhealthy or unreadable populations stay active because
|
|
157
|
+
// failed input cannot prove the selected population is empty.
|
|
158
|
+
func activeGraphConfig(
|
|
159
|
+
config graphConfig,
|
|
160
|
+
markdown map[string]*artifactInventory,
|
|
161
|
+
prisma map[string]*artifactInventory,
|
|
162
|
+
typescript map[string]*artifactInventory,
|
|
163
|
+
) graphConfig {
|
|
164
|
+
active := make([]claimSpec, 0, len(config.Claims))
|
|
165
|
+
for _, claim := range config.Claims {
|
|
166
|
+
inventories := inventoriesOf(
|
|
167
|
+
claim.Type,
|
|
168
|
+
markdown,
|
|
169
|
+
prisma,
|
|
170
|
+
map[string]*artifactInventory{},
|
|
171
|
+
typescript,
|
|
172
|
+
)
|
|
173
|
+
if claimIsInactive(claim, inventories) {
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
active = append(active, claim)
|
|
177
|
+
}
|
|
178
|
+
config.Claims = active
|
|
179
|
+
return config
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
func claimIsInactive(
|
|
183
|
+
claim claimSpec,
|
|
184
|
+
inventories map[string]*artifactInventory,
|
|
185
|
+
) bool {
|
|
186
|
+
switch claim.Type {
|
|
187
|
+
case artifactMarkdown, artifactPrisma, artifactTypeScript:
|
|
188
|
+
default:
|
|
189
|
+
return false
|
|
190
|
+
}
|
|
191
|
+
paths := matchingInventoryPaths(inventories, claim.Base, claim.Files)
|
|
192
|
+
if !populationIsHealthy(inventories, claim.Base, paths) ||
|
|
193
|
+
unreadableBaseProblem(claim.Base, claim.Type) != "" {
|
|
194
|
+
return false
|
|
195
|
+
}
|
|
196
|
+
for _, path := range paths {
|
|
197
|
+
for _, unit := range inventories[path].Units {
|
|
198
|
+
if unit.Hidden == "" && claim.Symbols.contains(unit.Symbol) {
|
|
199
|
+
return false
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return true
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
func materializeClaimStates(
|
|
207
|
+
config graphConfig,
|
|
208
|
+
markdown map[string]*artifactInventory,
|
|
209
|
+
prisma map[string]*artifactInventory,
|
|
210
|
+
swagger map[string]*artifactInventory,
|
|
211
|
+
typescript map[string]*artifactInventory,
|
|
212
|
+
loader *typeScriptLoader,
|
|
213
|
+
) ([]claimState, []string) {
|
|
214
|
+
states := make([]claimState, 0, len(config.Claims))
|
|
215
|
+
problems := []string{}
|
|
216
|
+
for _, claim := range config.Claims {
|
|
217
|
+
inventories := inventoriesOf(claim.Type, markdown, prisma, swagger, typescript)
|
|
218
|
+
paths := matchingInventoryPaths(inventories, claim.Base, claim.Files)
|
|
219
|
+
state := claimState{
|
|
220
|
+
Spec: claim,
|
|
221
|
+
Paths: paths,
|
|
222
|
+
Healthy: populationIsHealthy(inventories, claim.Base, paths),
|
|
223
|
+
OutsideCarrier: map[string]bool{},
|
|
224
|
+
}
|
|
225
|
+
carrierPaths := map[string]bool{}
|
|
226
|
+
if len(claim.ExclusionCarriers.Patterns) != 0 {
|
|
227
|
+
// The carriers narrow the claim's own population and never widen
|
|
228
|
+
// it, so a pattern matching a file this claim does not select
|
|
229
|
+
// contributes nothing and leaves the set empty.
|
|
230
|
+
claimPaths := map[string]bool{}
|
|
231
|
+
for _, path := range paths {
|
|
232
|
+
claimPaths[path] = true
|
|
233
|
+
}
|
|
234
|
+
for _, carrier := range matchingInventoryPaths(inventories, claim.Base, claim.ExclusionCarriers) {
|
|
235
|
+
if claimPaths[carrier] {
|
|
236
|
+
carrierPaths[carrier] = true
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// A carrier set that selects nothing would silently refuse every
|
|
240
|
+
// exclusion the claim writes, so the misspelling is reported where
|
|
241
|
+
// it was made rather than as a placement finding on each tag.
|
|
242
|
+
if len(carrierPaths) == 0 && len(paths) != 0 {
|
|
243
|
+
problems = append(
|
|
244
|
+
problems,
|
|
245
|
+
claimLabel(claim)+" declares evidenceExcludeCarriers "+describePopulation(claim.Base, claim.ExclusionCarriers)+", which selects none of its "+decimal(len(paths))+" claim file(s). Point the patterns at a file this claim already selects, or drop the property to accept an exclusion anywhere in the population.",
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if len(paths) == 0 && state.Healthy {
|
|
250
|
+
problems = append(
|
|
251
|
+
problems,
|
|
252
|
+
claimLabel(claim)+" matched no "+string(claim.Type)+" files for "+describePopulation(claim.Base, claim.Files)+". Fix the globs or the root they resolve against; '*' stays within one segment, '**' crosses segments, and a bare directory is not recursive.",
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
hostsByID := map[string]bool{}
|
|
256
|
+
for _, path := range paths {
|
|
257
|
+
for _, unit := range inventories[path].Units {
|
|
258
|
+
if unit.Hidden != "" ||
|
|
259
|
+
!claim.Symbols.contains(unit.Symbol) ||
|
|
260
|
+
hostsByID[unit.ID] {
|
|
261
|
+
continue
|
|
262
|
+
}
|
|
263
|
+
hostsByID[unit.ID] = true
|
|
264
|
+
state.Hosts = append(state.Hosts, unit)
|
|
265
|
+
}
|
|
266
|
+
state.Declarations = append(
|
|
267
|
+
state.Declarations,
|
|
268
|
+
inventories[path].Declarations...,
|
|
269
|
+
)
|
|
270
|
+
if len(claim.ExclusionCarriers.Patterns) != 0 && !carrierPaths[path] {
|
|
271
|
+
for _, declaration := range inventories[path].Declarations {
|
|
272
|
+
state.OutsideCarrier[declaration.ID] = true
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
sortUnits(state.Hosts)
|
|
277
|
+
for _, reference := range claim.References {
|
|
278
|
+
referenceInventories := inventoriesOf(
|
|
279
|
+
reference.Type,
|
|
280
|
+
markdown,
|
|
281
|
+
prisma,
|
|
282
|
+
swagger,
|
|
283
|
+
typescript,
|
|
284
|
+
)
|
|
285
|
+
if reference.Type == artifactTypeScript && reference.entrySelected() {
|
|
286
|
+
entryState, entryProblems := materializeEntryReference(
|
|
287
|
+
claim,
|
|
288
|
+
reference,
|
|
289
|
+
loader,
|
|
290
|
+
)
|
|
291
|
+
problems = append(problems, entryProblems...)
|
|
292
|
+
state.References = append(state.References, entryState)
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
295
|
+
if reference.Type == artifactTypeScript && reference.Package != "" {
|
|
296
|
+
packageState, packageProblems := materializePackageGlobReference(
|
|
297
|
+
claim,
|
|
298
|
+
reference,
|
|
299
|
+
loader,
|
|
300
|
+
)
|
|
301
|
+
problems = append(problems, packageProblems...)
|
|
302
|
+
state.References = append(state.References, packageState)
|
|
303
|
+
continue
|
|
304
|
+
}
|
|
305
|
+
if reference.Type == artifactTypeScript {
|
|
306
|
+
localState, localProblems := materializeLocalTypeScriptReference(
|
|
307
|
+
claim,
|
|
308
|
+
reference,
|
|
309
|
+
referenceInventories,
|
|
310
|
+
loader,
|
|
311
|
+
)
|
|
312
|
+
problems = append(problems, localProblems...)
|
|
313
|
+
state.References = append(state.References, localState)
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
referencePaths := matchingReferencePaths(
|
|
317
|
+
referenceInventories,
|
|
318
|
+
reference,
|
|
319
|
+
)
|
|
320
|
+
referenceState := referenceState{
|
|
321
|
+
Spec: reference,
|
|
322
|
+
Paths: referencePaths,
|
|
323
|
+
UnitsByScope: map[string][]*evidenceUnit{},
|
|
324
|
+
Healthy: populationIsHealthy(referenceInventories, reference.Base, referencePaths),
|
|
325
|
+
}
|
|
326
|
+
if len(referencePaths) == 0 && referenceState.Healthy {
|
|
327
|
+
if reference.Type == artifactSwagger {
|
|
328
|
+
problems = append(
|
|
329
|
+
problems,
|
|
330
|
+
claimLabel(claim)+" "+referenceLabel(reference)+" matched no swagger source for "+describeReferenceSources(reference)+". Fix the reference location; this obligation cannot materialize evidence units without a source.",
|
|
331
|
+
)
|
|
332
|
+
} else {
|
|
333
|
+
problems = append(
|
|
334
|
+
problems,
|
|
335
|
+
claimLabel(claim)+" "+referenceLabel(reference)+" matched no "+string(reference.Type)+" files for "+describePopulation(reference.Base, reference.Files)+". Fix the reference globs or the root they resolve against; this obligation cannot materialize evidence units without files.",
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
selectedInventoryProblem := false
|
|
340
|
+
availableUnits := map[string]*evidenceUnit{}
|
|
341
|
+
selectedUnits := map[string]bool{}
|
|
342
|
+
for _, path := range referencePaths {
|
|
343
|
+
for _, inventoryProblem := range referenceInventories[path].Problems {
|
|
344
|
+
if inventoryProblem.Symbol == "*" ||
|
|
345
|
+
reference.Symbols.contains(inventoryProblem.Symbol) {
|
|
346
|
+
selectedInventoryProblem = true
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
for _, unit := range referenceInventories[path].Units {
|
|
350
|
+
// A withdrawn declaration is not an obligation and not an
|
|
351
|
+
// aggregate scope either, so it never enters the map that
|
|
352
|
+
// promotes ancestors. It is still recorded, so a citation
|
|
353
|
+
// naming it is answered with the tag rather than with a
|
|
354
|
+
// target that appears not to exist.
|
|
355
|
+
if unit.Hidden != "" {
|
|
356
|
+
if !selectedUnits[unit.ID] {
|
|
357
|
+
selectedUnits[unit.ID] = true
|
|
358
|
+
referenceState.Hidden = append(
|
|
359
|
+
referenceState.Hidden,
|
|
360
|
+
unit,
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
continue
|
|
364
|
+
}
|
|
365
|
+
availableUnits[unit.ID] = unit
|
|
366
|
+
if !reference.Symbols.contains(unit.Symbol) ||
|
|
367
|
+
selectedUnits[unit.ID] {
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
370
|
+
selectedUnits[unit.ID] = true
|
|
371
|
+
referenceState.Units = append(referenceState.Units, unit)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
sortUnits(referenceState.Units)
|
|
375
|
+
scopesByID := map[string]*evidenceUnit{}
|
|
376
|
+
for _, unit := range referenceState.Units {
|
|
377
|
+
for scope := unit; scope != nil; scope = availableUnits[scope.ParentID] {
|
|
378
|
+
referenceState.UnitsByScope[scope.ID] = append(
|
|
379
|
+
referenceState.UnitsByScope[scope.ID],
|
|
380
|
+
unit,
|
|
381
|
+
)
|
|
382
|
+
if scopesByID[scope.ID] == nil {
|
|
383
|
+
scopesByID[scope.ID] = scope
|
|
384
|
+
referenceState.Scopes = append(referenceState.Scopes, scope)
|
|
385
|
+
}
|
|
386
|
+
if scope.ParentID == "" {
|
|
387
|
+
break
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
sortUnits(referenceState.Scopes)
|
|
392
|
+
if len(referencePaths) != 0 &&
|
|
393
|
+
len(referenceState.Units) == 0 &&
|
|
394
|
+
referenceState.Healthy &&
|
|
395
|
+
!selectedInventoryProblem {
|
|
396
|
+
problems = append(
|
|
397
|
+
problems,
|
|
398
|
+
claimLabel(claim)+" "+referenceLabel(reference)+" matched "+decimal(len(referencePaths))+" file(s) but materialized no selected evidence units ("+reference.Symbols.names()+"). Select symbol kinds present in those files or correct the reference globs.",
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
state.References = append(state.References, referenceState)
|
|
402
|
+
}
|
|
403
|
+
states = append(states, state)
|
|
404
|
+
}
|
|
405
|
+
return states, problems
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
func evaluateEvidenceGraph(
|
|
409
|
+
states []claimState,
|
|
410
|
+
loader *typeScriptLoader,
|
|
411
|
+
) []string {
|
|
412
|
+
problems := []string{}
|
|
413
|
+
targets := map[string]map[string]*evidenceUnit{}
|
|
414
|
+
markdownTargets := map[string]map[string]*evidenceUnit{}
|
|
415
|
+
// Scoped targets are keyed by owning file as well as name, which is what
|
|
416
|
+
// makes import-scope resolution unambiguous: two modules exporting `get`
|
|
417
|
+
// never compete, because resolution already knows which file it landed in.
|
|
418
|
+
scopedTargets := map[scopedTargetKey]map[string]*evidenceUnit{}
|
|
419
|
+
// The same two indexes over the units a documentation tag withdrew. A
|
|
420
|
+
// citation that lands here resolved to a real declaration and must be told
|
|
421
|
+
// that the tag is why the target is not evidence; the alternative is an
|
|
422
|
+
// unresolved-target message that sends the author looking for a typo.
|
|
423
|
+
hiddenTargets := map[string]*evidenceUnit{}
|
|
424
|
+
scopedHidden := map[scopedTargetKey]*evidenceUnit{}
|
|
425
|
+
for _, state := range states {
|
|
426
|
+
for _, reference := range state.References {
|
|
427
|
+
for _, unit := range reference.Hidden {
|
|
428
|
+
for _, address := range append([]string{unit.Target}, unit.Aliases...) {
|
|
429
|
+
hiddenTargets[address] = unit
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
for _, address := range reference.Published {
|
|
433
|
+
if address.Unit.Hidden == "" {
|
|
434
|
+
continue
|
|
435
|
+
}
|
|
436
|
+
scopedHidden[scopedTargetKey{
|
|
437
|
+
path: address.Module,
|
|
438
|
+
target: address.Address,
|
|
439
|
+
}] = address.Unit
|
|
440
|
+
}
|
|
441
|
+
if len(reference.Published) == 0 {
|
|
442
|
+
for _, unit := range reference.Hidden {
|
|
443
|
+
for _, address := range append([]string{unit.Target}, unit.Aliases...) {
|
|
444
|
+
scopedHidden[scopedTargetKey{
|
|
445
|
+
path: unit.Path,
|
|
446
|
+
target: address,
|
|
447
|
+
}] = unit
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// A traversed address is valid in the module that publishes it, not in
|
|
452
|
+
// the one that declares the symbol. Identity still belongs to the
|
|
453
|
+
// declaring file; only reachability moves, and a declaration several
|
|
454
|
+
// selected modules publish is citable from each of them.
|
|
455
|
+
scopeIDs := map[string]bool{}
|
|
456
|
+
for _, unit := range reference.Scopes {
|
|
457
|
+
scopeIDs[unit.ID] = true
|
|
458
|
+
}
|
|
459
|
+
for _, address := range reference.Published {
|
|
460
|
+
if !scopeIDs[address.Unit.ID] {
|
|
461
|
+
continue
|
|
462
|
+
}
|
|
463
|
+
key := scopedTargetKey{path: address.Module, target: address.Address}
|
|
464
|
+
if scopedTargets[key] == nil {
|
|
465
|
+
scopedTargets[key] = map[string]*evidenceUnit{}
|
|
466
|
+
}
|
|
467
|
+
scopedTargets[key][address.Unit.ID] = address.Unit
|
|
468
|
+
}
|
|
469
|
+
for _, unit := range reference.Scopes {
|
|
470
|
+
for _, address := range append([]string{unit.Target}, unit.Aliases...) {
|
|
471
|
+
if targets[address] == nil {
|
|
472
|
+
targets[address] = map[string]*evidenceUnit{}
|
|
473
|
+
}
|
|
474
|
+
targets[address][unit.ID] = unit
|
|
475
|
+
}
|
|
476
|
+
if unit.Type == artifactMarkdown {
|
|
477
|
+
if markdownTargets[unit.Target] == nil {
|
|
478
|
+
markdownTargets[unit.Target] = map[string]*evidenceUnit{}
|
|
479
|
+
}
|
|
480
|
+
markdownTargets[unit.Target][unit.ID] = unit
|
|
481
|
+
}
|
|
482
|
+
// A population whose addresses came from traversal already indexed
|
|
483
|
+
// every module that publishes this unit. Only a population addressed
|
|
484
|
+
// by its declaring files needs this fallback.
|
|
485
|
+
if unit.Type == artifactTypeScript && len(reference.Published) == 0 {
|
|
486
|
+
for _, address := range append([]string{unit.Target}, unit.Aliases...) {
|
|
487
|
+
key := scopedTargetKey{path: unit.Path, target: address}
|
|
488
|
+
if scopedTargets[key] == nil {
|
|
489
|
+
scopedTargets[key] = map[string]*evidenceUnit{}
|
|
490
|
+
}
|
|
491
|
+
scopedTargets[key][unit.ID] = unit
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
declarations := map[string]*evidenceDeclaration{}
|
|
499
|
+
owners := map[string][]claimState{}
|
|
500
|
+
for _, state := range states {
|
|
501
|
+
for _, declaration := range state.Declarations {
|
|
502
|
+
declarations[declaration.ID] = declaration
|
|
503
|
+
seen := false
|
|
504
|
+
for _, owner := range owners[declaration.ID] {
|
|
505
|
+
if owner.Spec.Index == state.Spec.Index {
|
|
506
|
+
seen = true
|
|
507
|
+
break
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if !seen {
|
|
511
|
+
owners[declaration.ID] = append(owners[declaration.ID], state)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
declarationIDs := make([]string, 0, len(declarations))
|
|
516
|
+
for id := range declarations {
|
|
517
|
+
declarationIDs = append(declarationIDs, id)
|
|
518
|
+
}
|
|
519
|
+
sort.Strings(declarationIDs)
|
|
520
|
+
|
|
521
|
+
resolved := map[string]string{}
|
|
522
|
+
for _, id := range declarationIDs {
|
|
523
|
+
declaration := declarations[id]
|
|
524
|
+
context := declarationObligationContext(owners[id])
|
|
525
|
+
if !declaration.valid() {
|
|
526
|
+
problems = append(
|
|
527
|
+
problems,
|
|
528
|
+
"Malformed @"+string(declaration.Tag)+" declaration at "+declaration.location()+" for "+context+": target and non-empty reason are mandatory. Write '@"+string(declaration.Tag)+" <target> <reason>'.",
|
|
529
|
+
)
|
|
530
|
+
continue
|
|
531
|
+
}
|
|
532
|
+
if isInlineLinkTarget(declaration.Target) {
|
|
533
|
+
unitID, problem := resolveInlineLinkDeclaration(
|
|
534
|
+
declaration,
|
|
535
|
+
loader,
|
|
536
|
+
scopedTargets,
|
|
537
|
+
scopedHidden,
|
|
538
|
+
context,
|
|
539
|
+
)
|
|
540
|
+
if problem != "" {
|
|
541
|
+
problems = append(problems, problem)
|
|
542
|
+
continue
|
|
543
|
+
}
|
|
544
|
+
resolved[id] = unitID
|
|
545
|
+
continue
|
|
546
|
+
}
|
|
547
|
+
if declaration.Type == artifactTypeScript &&
|
|
548
|
+
looksLikeTypeScriptTarget(declaration.Target, targets, markdownTargets) {
|
|
549
|
+
problems = append(
|
|
550
|
+
problems,
|
|
551
|
+
"Unbraced TypeScript evidence target '"+declaration.Target+"' at "+declaration.location()+" for "+context+": a target naming a symbol is now written as an inline link, so the citing module's import is what resolves it. Write '@"+string(declaration.Tag)+" {@link "+declaration.Target+"} <reason>' and import the symbol; 'import type' is enough.",
|
|
552
|
+
)
|
|
553
|
+
continue
|
|
554
|
+
}
|
|
555
|
+
candidates := declarationCandidates(declaration.Target, targets, markdownTargets)
|
|
556
|
+
// The configuration guard refuses a code population to a claim that
|
|
557
|
+
// cannot address one, but the address map is built from every claim at
|
|
558
|
+
// once — so a Markdown claim could still land on a symbol materialized
|
|
559
|
+
// by some *other* claim's TypeScript reference. Measured: it resolved
|
|
560
|
+
// silently, which left repository-wide name uniqueness load-bearing
|
|
561
|
+
// through a door the guard does not cover. Closing it here rather than
|
|
562
|
+
// by scoping the whole map keeps resolution global for the artifacts
|
|
563
|
+
// that are addressed by path, where a shared map costs nothing.
|
|
564
|
+
if declaration.Type != artifactTypeScript {
|
|
565
|
+
addressable, code := splitCodeCandidates(candidates)
|
|
566
|
+
if len(addressable) == 0 && len(code) != 0 {
|
|
567
|
+
problems = append(
|
|
568
|
+
problems,
|
|
569
|
+
"Code evidence target '"+declaration.Target+"' at "+declaration.location()+" for "+context+": a "+string(declaration.Type)+" claim cannot cite a TypeScript symbol, because a symbol citation resolves through the citing module's imports and this artifact has none. Invert the obligation so the code cites this artifact, or move the citation into TypeScript.",
|
|
570
|
+
)
|
|
571
|
+
continue
|
|
572
|
+
}
|
|
573
|
+
candidates = addressable
|
|
574
|
+
}
|
|
575
|
+
switch len(candidates) {
|
|
576
|
+
case 0:
|
|
577
|
+
// A failed reference population may contain the declaration's target;
|
|
578
|
+
// absence from the partial address map proves nothing until that
|
|
579
|
+
// population is healthy again. Its loader diagnostic already names
|
|
580
|
+
// the repair boundary, so an unresolved-target diagnostic here would
|
|
581
|
+
// be a derivative false claim.
|
|
582
|
+
if declarationResolutionUncertain(owners[id]) {
|
|
583
|
+
continue
|
|
584
|
+
}
|
|
585
|
+
if hidden := hiddenTargets[declaration.Target]; hidden != nil {
|
|
586
|
+
problems = append(
|
|
587
|
+
problems,
|
|
588
|
+
hiddenTargetProblem(declaration, hidden, context),
|
|
589
|
+
)
|
|
590
|
+
continue
|
|
591
|
+
}
|
|
592
|
+
problems = append(
|
|
593
|
+
problems,
|
|
594
|
+
"Unresolved evidence target '"+declaration.Target+"' at "+declaration.location()+" for "+context+": no configured source materializes that evidence unit. Correct the target, or make one of the named references select the source unit this claim actually uses.",
|
|
595
|
+
)
|
|
596
|
+
case 1:
|
|
597
|
+
for unitID := range candidates {
|
|
598
|
+
resolved[id] = unitID
|
|
599
|
+
}
|
|
600
|
+
default:
|
|
601
|
+
descriptions := make([]string, 0, len(candidates))
|
|
602
|
+
for _, unit := range candidates {
|
|
603
|
+
descriptions = append(descriptions, unit.Readable+" at "+unit.location())
|
|
604
|
+
}
|
|
605
|
+
sort.Strings(descriptions)
|
|
606
|
+
problems = append(
|
|
607
|
+
problems,
|
|
608
|
+
"Ambiguous evidence target '"+declaration.Target+"' at "+declaration.location()+" for "+context+": it matches "+strings.Join(descriptions, "; ")+". Rename or qualify the source symbols so the target has exactly one meaning.",
|
|
609
|
+
)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
participates := map[string]bool{}
|
|
614
|
+
uncertain := map[string]bool{}
|
|
615
|
+
outOfScope := map[string][]string{}
|
|
616
|
+
outOfScopeSelections := map[string]symbolSet{}
|
|
617
|
+
// An exclusion outside its claim's declared carriers is a placement
|
|
618
|
+
// finding, not a host-kind one, so it carries its own obligations and the
|
|
619
|
+
// carrier globs its message must name.
|
|
620
|
+
outsideCarrier := map[string][]string{}
|
|
621
|
+
outsideCarrierGlobs := map[string]string{}
|
|
622
|
+
for _, state := range states {
|
|
623
|
+
if len(state.Paths) == 0 {
|
|
624
|
+
continue
|
|
625
|
+
}
|
|
626
|
+
if !state.Healthy {
|
|
627
|
+
for _, declaration := range state.Declarations {
|
|
628
|
+
uncertain[declaration.ID] = true
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
for _, reference := range state.References {
|
|
632
|
+
if !reference.Healthy {
|
|
633
|
+
for _, declaration := range state.Declarations {
|
|
634
|
+
uncertain[declaration.ID] = true
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
single := reference.Spec.Policy.SingleEvidencePerSymbol
|
|
638
|
+
unique := reference.Spec.Policy.UniqueEvidence
|
|
639
|
+
// An empty population owes nothing and normally ends the reference
|
|
640
|
+
// here. A healthy empty one still judges hosts under
|
|
641
|
+
// singleEvidencePerSymbol, because every selected host then truthfully
|
|
642
|
+
// cites zero units rather than the one it owes.
|
|
643
|
+
if len(reference.Units) == 0 &&
|
|
644
|
+
(!single || !state.Healthy || !reference.Healthy || len(reference.Paths) == 0) {
|
|
645
|
+
continue
|
|
646
|
+
}
|
|
647
|
+
acknowledged := map[string]bool{}
|
|
648
|
+
evidenceByUnit := map[string]*evidenceDeclaration{}
|
|
649
|
+
exclusionByUnit := map[string]*evidenceDeclaration{}
|
|
650
|
+
evidenceByHostAndScope := map[string]map[string]*evidenceDeclaration{}
|
|
651
|
+
selectedHosts := map[string]*evidenceUnit{}
|
|
652
|
+
evidenceUnitsByHost := map[string]map[string]bool{}
|
|
653
|
+
if single || unique {
|
|
654
|
+
for _, host := range state.Hosts {
|
|
655
|
+
selectedHosts[host.ID] = host
|
|
656
|
+
if single {
|
|
657
|
+
evidenceUnitsByHost[host.ID] = map[string]bool{}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
evidenceHostsByUnit := map[string]map[string]bool{}
|
|
662
|
+
if unique {
|
|
663
|
+
for _, unit := range reference.Units {
|
|
664
|
+
evidenceHostsByUnit[unit.ID] = map[string]bool{}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
scopesByID := map[string]*evidenceUnit{}
|
|
668
|
+
for _, scope := range reference.Scopes {
|
|
669
|
+
scopesByID[scope.ID] = scope
|
|
670
|
+
}
|
|
671
|
+
for _, declaration := range state.Declarations {
|
|
672
|
+
scopeID := resolved[declaration.ID]
|
|
673
|
+
covered := reference.UnitsByScope[scopeID]
|
|
674
|
+
if len(covered) == 0 {
|
|
675
|
+
continue
|
|
676
|
+
}
|
|
677
|
+
if declaration.Tag == tagExclude && state.OutsideCarrier[declaration.ID] {
|
|
678
|
+
outsideCarrier[declaration.ID] = appendUniqueString(
|
|
679
|
+
outsideCarrier[declaration.ID],
|
|
680
|
+
claimLabel(state.Spec)+" "+referenceLabel(reference.Spec),
|
|
681
|
+
)
|
|
682
|
+
outsideCarrierGlobs[declaration.ID] = describePopulation(state.Spec.Base, state.Spec.ExclusionCarriers)
|
|
683
|
+
continue
|
|
684
|
+
}
|
|
685
|
+
if !declarationEligibleForClaim(declaration, state.Spec) {
|
|
686
|
+
outOfScope[declaration.ID] = appendUniqueString(
|
|
687
|
+
outOfScope[declaration.ID],
|
|
688
|
+
claimLabel(state.Spec)+" "+referenceLabel(reference.Spec),
|
|
689
|
+
)
|
|
690
|
+
if outOfScopeSelections[declaration.ID] == nil {
|
|
691
|
+
outOfScopeSelections[declaration.ID] = symbolSet{}
|
|
692
|
+
}
|
|
693
|
+
for symbol := range state.Spec.Symbols {
|
|
694
|
+
outOfScopeSelections[declaration.ID][symbol] = true
|
|
695
|
+
}
|
|
696
|
+
continue
|
|
697
|
+
}
|
|
698
|
+
// Physical file ownership, resolved reference scope, and host
|
|
699
|
+
// eligibility decide which overlapping claims this declaration
|
|
700
|
+
// belongs to. A declaration may participate in several eligible
|
|
701
|
+
// obligations, but an ineligible overlap must not reject one
|
|
702
|
+
// already owned elsewhere.
|
|
703
|
+
participates[declaration.ID] = true
|
|
704
|
+
if !state.Healthy || !reference.Healthy {
|
|
705
|
+
continue
|
|
706
|
+
}
|
|
707
|
+
if declaration.Tag == tagExclude && reference.Spec.Policy.NoExclude {
|
|
708
|
+
problems = append(
|
|
709
|
+
problems,
|
|
710
|
+
"Forbidden @evidenceExclude for '"+scopesByID[scopeID].Target+"' at "+declaration.location()+" in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": noEvidenceExclude requires positive @evidence for this reference. Remove the exclusion and cite the target from a selected "+string(state.Spec.Type)+" host.",
|
|
711
|
+
)
|
|
712
|
+
continue
|
|
713
|
+
}
|
|
714
|
+
if declaration.Tag == tagEvidence && declaration.HostID != "" {
|
|
715
|
+
byScope := evidenceByHostAndScope[declaration.HostID]
|
|
716
|
+
if byScope == nil {
|
|
717
|
+
byScope = map[string]*evidenceDeclaration{}
|
|
718
|
+
evidenceByHostAndScope[declaration.HostID] = byScope
|
|
719
|
+
}
|
|
720
|
+
if first := byScope[scopeID]; first != nil {
|
|
721
|
+
problems = append(
|
|
722
|
+
problems,
|
|
723
|
+
"Duplicate @evidence for '"+scopesByID[scopeID].Target+"' on the same host at "+declaration.location()+"; first declared at "+first.location()+".",
|
|
724
|
+
)
|
|
725
|
+
} else {
|
|
726
|
+
byScope[scopeID] = declaration
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if declaration.Tag == tagEvidence && (single || unique) {
|
|
730
|
+
for _, hostID := range declaration.SemanticHostIDs {
|
|
731
|
+
if selectedHosts[hostID] == nil {
|
|
732
|
+
continue
|
|
733
|
+
}
|
|
734
|
+
for _, unit := range covered {
|
|
735
|
+
if single {
|
|
736
|
+
evidenceUnitsByHost[hostID][unit.ID] = true
|
|
737
|
+
}
|
|
738
|
+
if !unique {
|
|
739
|
+
continue
|
|
740
|
+
}
|
|
741
|
+
// A covered unit is a selected unit of this reference,
|
|
742
|
+
// so the counter usually exists. Allocating on demand
|
|
743
|
+
// keeps a future scope map that reaches wider from
|
|
744
|
+
// turning a count into a nil-map panic in the compiler.
|
|
745
|
+
hosts := evidenceHostsByUnit[unit.ID]
|
|
746
|
+
if hosts == nil {
|
|
747
|
+
hosts = map[string]bool{}
|
|
748
|
+
evidenceHostsByUnit[unit.ID] = hosts
|
|
749
|
+
}
|
|
750
|
+
hosts[hostID] = true
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
var conflictingUnit *evidenceUnit
|
|
755
|
+
var conflictingDeclaration *evidenceDeclaration
|
|
756
|
+
var duplicateExclusionUnit *evidenceUnit
|
|
757
|
+
var firstExclusion *evidenceDeclaration
|
|
758
|
+
for _, unit := range covered {
|
|
759
|
+
acknowledged[unit.ID] = true
|
|
760
|
+
if declaration.Tag == tagEvidence {
|
|
761
|
+
if first := exclusionByUnit[unit.ID]; first != nil && conflictingUnit == nil {
|
|
762
|
+
conflictingUnit = unit
|
|
763
|
+
conflictingDeclaration = first
|
|
764
|
+
}
|
|
765
|
+
if evidenceByUnit[unit.ID] == nil {
|
|
766
|
+
evidenceByUnit[unit.ID] = declaration
|
|
767
|
+
}
|
|
768
|
+
continue
|
|
769
|
+
}
|
|
770
|
+
if first := evidenceByUnit[unit.ID]; first != nil && conflictingUnit == nil {
|
|
771
|
+
conflictingUnit = unit
|
|
772
|
+
conflictingDeclaration = first
|
|
773
|
+
}
|
|
774
|
+
if first := exclusionByUnit[unit.ID]; first != nil {
|
|
775
|
+
if duplicateExclusionUnit == nil {
|
|
776
|
+
duplicateExclusionUnit = unit
|
|
777
|
+
firstExclusion = first
|
|
778
|
+
}
|
|
779
|
+
continue
|
|
780
|
+
}
|
|
781
|
+
exclusionByUnit[unit.ID] = declaration
|
|
782
|
+
}
|
|
783
|
+
if conflictingUnit != nil {
|
|
784
|
+
evidence := declaration
|
|
785
|
+
exclusion := conflictingDeclaration
|
|
786
|
+
if declaration.Tag == tagExclude {
|
|
787
|
+
evidence = conflictingDeclaration
|
|
788
|
+
exclusion = declaration
|
|
789
|
+
}
|
|
790
|
+
problems = append(
|
|
791
|
+
problems,
|
|
792
|
+
"Conflicting acknowledgements for '"+conflictingUnit.Target+"' in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": @evidence at "+evidence.location()+" overlaps @evidenceExclude at "+exclusion.location()+".",
|
|
793
|
+
)
|
|
794
|
+
}
|
|
795
|
+
if duplicateExclusionUnit != nil {
|
|
796
|
+
problems = append(
|
|
797
|
+
problems,
|
|
798
|
+
"Duplicate @evidenceExclude for '"+duplicateExclusionUnit.Target+"' in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": exclusion at "+declaration.location()+" overlaps exclusion at "+firstExclusion.location()+".",
|
|
799
|
+
)
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if !state.Healthy || !reference.Healthy || len(reference.Paths) == 0 {
|
|
803
|
+
continue
|
|
804
|
+
}
|
|
805
|
+
if single {
|
|
806
|
+
for _, host := range state.Hosts {
|
|
807
|
+
count := len(evidenceUnitsByHost[host.ID])
|
|
808
|
+
if count == 1 {
|
|
809
|
+
continue
|
|
810
|
+
}
|
|
811
|
+
problems = append(
|
|
812
|
+
problems,
|
|
813
|
+
"Evidence host "+host.Readable+" at "+host.location()+" in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+" cites "+decimal(count)+" distinct selected evidence unit(s); singleEvidencePerSymbol requires exactly 1. Keep positive @evidence citations on this semantic host to exactly one distinct unit.",
|
|
814
|
+
)
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
for _, unit := range reference.Units {
|
|
818
|
+
if !acknowledged[unit.ID] {
|
|
819
|
+
repair := "Use @evidence on a selected " + string(state.Spec.Type) + " host or @evidenceExclude on an eligible carrier."
|
|
820
|
+
if reference.Spec.Policy.NoExclude {
|
|
821
|
+
repair = "Use @evidence on a selected " + string(state.Spec.Type) + " host; this reference forbids @evidenceExclude."
|
|
822
|
+
}
|
|
823
|
+
problems = append(
|
|
824
|
+
problems,
|
|
825
|
+
"Missing acknowledgement for '"+unit.Target+"' ("+unit.Readable+" at "+unit.location()+") in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+". "+repair,
|
|
826
|
+
)
|
|
827
|
+
}
|
|
828
|
+
hostCount := len(evidenceHostsByUnit[unit.ID])
|
|
829
|
+
if !unique || hostCount <= 1 {
|
|
830
|
+
continue
|
|
831
|
+
}
|
|
832
|
+
problems = append(
|
|
833
|
+
problems,
|
|
834
|
+
"Evidence unit '"+unit.Target+"' ("+unit.Readable+" at "+unit.location()+") in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+" has "+decimal(hostCount)+" distinct positive evidence host(s); uniqueEvidence allows at most 1. Keep the one selected "+string(state.Spec.Type)+" host that owns this unit and remove the other citation(s).",
|
|
835
|
+
)
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
for _, id := range declarationIDs {
|
|
840
|
+
if resolved[id] == "" || participates[id] {
|
|
841
|
+
continue
|
|
842
|
+
}
|
|
843
|
+
declaration := declarations[id]
|
|
844
|
+
context := declarationObligationContext(owners[id])
|
|
845
|
+
if obligations := outsideCarrier[id]; len(obligations) != 0 {
|
|
846
|
+
problems = append(
|
|
847
|
+
problems,
|
|
848
|
+
"Misplaced @evidenceExclude at "+declaration.location()+" for "+strings.Join(obligations, "; ")+", target '"+displayTarget(declaration.Target)+"': evidenceExcludeCarriers confines this claim's exclusions to "+outsideCarrierGlobs[id]+". Move the tag there, or delete it and implement the target this claim owes.",
|
|
849
|
+
)
|
|
850
|
+
continue
|
|
851
|
+
}
|
|
852
|
+
if obligations := outOfScope[id]; len(obligations) != 0 {
|
|
853
|
+
host := declaration.Hosts.names()
|
|
854
|
+
if len(declaration.Hosts) == 0 {
|
|
855
|
+
host = "unsupported or non-exported declaration"
|
|
856
|
+
}
|
|
857
|
+
if declaration.Tag == tagExclude {
|
|
858
|
+
problems = append(
|
|
859
|
+
problems,
|
|
860
|
+
"Out-of-scope @evidenceExclude carrier at "+declaration.location()+" for "+strings.Join(obligations, "; ")+", target '"+displayTarget(declaration.Target)+"': '"+host+"' is not an eligible exclusion carrier in these matching claim files. Move the exclusion to a supported public export or selected declaration host, or use a top-level unattached Prisma documentation comment.",
|
|
861
|
+
)
|
|
862
|
+
continue
|
|
863
|
+
}
|
|
864
|
+
problems = append(
|
|
865
|
+
problems,
|
|
866
|
+
"Out-of-scope @"+string(declaration.Tag)+" host at "+declaration.location()+" for "+strings.Join(obligations, "; ")+", target '"+displayTarget(declaration.Target)+"': host kind '"+host+"' is not selected ("+outOfScopeSelections[id].names()+") by any of these claim obligations. Move the declaration to a selected host, or widen only the claim symbol selector that genuinely owns this target.",
|
|
867
|
+
)
|
|
868
|
+
continue
|
|
869
|
+
}
|
|
870
|
+
if uncertain[id] {
|
|
871
|
+
// A failed loader makes non-participation unknowable. Its direct
|
|
872
|
+
// diagnostic is the repair path; adding a ghost finding here would
|
|
873
|
+
// derive a second claim from an incomplete graph.
|
|
874
|
+
continue
|
|
875
|
+
}
|
|
876
|
+
problems = append(
|
|
877
|
+
problems,
|
|
878
|
+
"Non-participating @"+string(declaration.Tag)+" target '"+displayTarget(declaration.Target)+"' at "+declaration.location()+" for "+context+": the target resolves, but none of this declaration's configured references selects it. Correct the target or reference, or move the tag to an eligible host or exclusion carrier in the claim that owes it; a resolving tag must discharge at least one obligation.",
|
|
879
|
+
)
|
|
880
|
+
}
|
|
881
|
+
return problems
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// declarationEligibleForClaim keeps ownership evidence on the selected host
|
|
885
|
+
// while allowing an intentional exclusion to live on a claim-file carrier.
|
|
886
|
+
func declarationEligibleForClaim(
|
|
887
|
+
declaration *evidenceDeclaration,
|
|
888
|
+
claim claimSpec,
|
|
889
|
+
) bool {
|
|
890
|
+
if claim.Symbols.intersects(declaration.Hosts) {
|
|
891
|
+
return true
|
|
892
|
+
}
|
|
893
|
+
return declaration.Tag == tagExclude && declaration.ExclusionCarrier
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// hiddenTargetProblem names the documentation tag that withdrew a cited
|
|
897
|
+
// declaration.
|
|
898
|
+
//
|
|
899
|
+
// The repair is the author's either way, and which repair is right depends on
|
|
900
|
+
// which of the two statements is wrong — the tag or the citation — so the
|
|
901
|
+
// message states both rather than choosing.
|
|
902
|
+
func hiddenTargetProblem(
|
|
903
|
+
declaration *evidenceDeclaration,
|
|
904
|
+
hidden *evidenceUnit,
|
|
905
|
+
context string,
|
|
906
|
+
) string {
|
|
907
|
+
return "Hidden evidence target '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context + ": " + hidden.Readable + " at " + hidden.location() + " carries '" + hidden.Hidden + "' in its documentation comment, which withdraws it from the evidence population along with everything nested inside it. Remove the tag if the declaration is public contract, or drop this citation if it is not."
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
func declarationResolutionUncertain(owners []claimState) bool {
|
|
911
|
+
for _, owner := range owners {
|
|
912
|
+
for _, reference := range owner.References {
|
|
913
|
+
if !reference.Healthy {
|
|
914
|
+
return true
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return false
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// materializeEntryReference builds a population by walking an entry module's
|
|
922
|
+
// export graph rather than by matching paths.
|
|
923
|
+
//
|
|
924
|
+
// The entry is what a consumer can actually import, so the population is the
|
|
925
|
+
// public contract instead of whatever files a glob happened to sweep in. It is
|
|
926
|
+
// also the only selection that can reach a package symbol nothing imports,
|
|
927
|
+
// because such a symbol is absent from the Program by definition.
|
|
928
|
+
func materializeEntryReference(
|
|
929
|
+
claim claimSpec,
|
|
930
|
+
reference referenceSpec,
|
|
931
|
+
loader *typeScriptLoader,
|
|
932
|
+
) (referenceState, []string) {
|
|
933
|
+
state := referenceState{
|
|
934
|
+
Spec: reference,
|
|
935
|
+
UnitsByScope: map[string][]*evidenceUnit{},
|
|
936
|
+
Healthy: true,
|
|
937
|
+
}
|
|
938
|
+
entry, problem := resolveReferenceEntry(claim, reference, loader)
|
|
939
|
+
if problem != "" {
|
|
940
|
+
return state, []string{problem}
|
|
941
|
+
}
|
|
942
|
+
state.Paths = []string{entry}
|
|
943
|
+
population := materializeEntryUnits(loader, []string{entry}, reference.Symbols)
|
|
944
|
+
state.Units = population.Units
|
|
945
|
+
state.Hidden = population.Hidden
|
|
946
|
+
state.Published = population.Published
|
|
947
|
+
if failure := loader.failure(entry); failure != "" {
|
|
948
|
+
state.Healthy = false
|
|
949
|
+
return state, []string{
|
|
950
|
+
claimLabel(claim) + " " + referenceLabel(reference) + " could not read TypeScript entry '" + entry + "': " + failure + ". Fix filesystem access or the package installation; coverage cannot be evaluated from a partial entry graph.",
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if len(state.Units) == 0 {
|
|
954
|
+
return state, []string{
|
|
955
|
+
claimLabel(claim) + " " + referenceLabel(reference) + " reached no selected evidence units (" + reference.Symbols.names() + ") from entry '" + entry + "'. Select symbol kinds the entry exposes, or point the entry at the module that declares them.",
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
applyTraversedScopes(&state, population.Reached)
|
|
959
|
+
return state, nil
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// applyTraversedScopes gives a traversed population its containment closure.
|
|
963
|
+
//
|
|
964
|
+
// Containment follows the declaration hierarchy rather than the address text. A
|
|
965
|
+
// type and a callable may share one public name, so treating a common address
|
|
966
|
+
// prefix as ownership would make an unrelated same-name declaration an ancestor
|
|
967
|
+
// and turn every citation of that name ambiguous. Every reached declaration
|
|
968
|
+
// takes part, which keeps an unselected ancestor addressable as the aggregate
|
|
969
|
+
// scope of its selected descendants.
|
|
970
|
+
func applyTraversedScopes(state *referenceState, reached []*evidenceUnit) {
|
|
971
|
+
sortUnits(state.Units)
|
|
972
|
+
available := map[string]*evidenceUnit{}
|
|
973
|
+
for _, unit := range reached {
|
|
974
|
+
available[unit.ID] = unit
|
|
975
|
+
}
|
|
976
|
+
scopesByID := map[string]*evidenceUnit{}
|
|
977
|
+
for _, unit := range state.Units {
|
|
978
|
+
for scope := unit; scope != nil; scope = available[scope.ParentID] {
|
|
979
|
+
state.UnitsByScope[scope.ID] = append(state.UnitsByScope[scope.ID], unit)
|
|
980
|
+
if scopesByID[scope.ID] == nil {
|
|
981
|
+
scopesByID[scope.ID] = scope
|
|
982
|
+
state.Scopes = append(state.Scopes, scope)
|
|
983
|
+
}
|
|
984
|
+
if scope.ParentID == "" {
|
|
985
|
+
break
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
sortUnits(state.Scopes)
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// materializeLocalTypeScriptReference selects modules of the active project
|
|
993
|
+
// with globs and takes the population from what those modules publish.
|
|
994
|
+
//
|
|
995
|
+
// A glob names modules, while an obligation is owed by symbols. So a matched
|
|
996
|
+
// module contributes its own exports and everything it re-exports: a barrel is
|
|
997
|
+
// selected for the surface it presents, and pulling that surface apart into
|
|
998
|
+
// whichever files happen to declare it would make the same population depend on
|
|
999
|
+
// how the sources are laid out.
|
|
1000
|
+
func materializeLocalTypeScriptReference(
|
|
1001
|
+
claim claimSpec,
|
|
1002
|
+
reference referenceSpec,
|
|
1003
|
+
inventories map[string]*artifactInventory,
|
|
1004
|
+
loader *typeScriptLoader,
|
|
1005
|
+
) (referenceState, []string) {
|
|
1006
|
+
paths := matchingReferencePaths(inventories, reference)
|
|
1007
|
+
state := referenceState{
|
|
1008
|
+
Spec: reference,
|
|
1009
|
+
Paths: paths,
|
|
1010
|
+
UnitsByScope: map[string][]*evidenceUnit{},
|
|
1011
|
+
Healthy: populationIsHealthy(inventories, reference.Base, paths),
|
|
1012
|
+
}
|
|
1013
|
+
if len(paths) == 0 {
|
|
1014
|
+
if !state.Healthy {
|
|
1015
|
+
return state, nil
|
|
1016
|
+
}
|
|
1017
|
+
return state, []string{
|
|
1018
|
+
claimLabel(claim) + " " + referenceLabel(reference) + " matched no typescript files for " + describePopulation(reference.Base, reference.Files) + ". Fix the reference globs or the root they resolve against; this obligation cannot materialize evidence units without files.",
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
selectedInventoryProblem := false
|
|
1022
|
+
for _, path := range paths {
|
|
1023
|
+
for _, inventoryProblem := range inventories[path].Problems {
|
|
1024
|
+
if inventoryProblem.Symbol == "*" ||
|
|
1025
|
+
reference.Symbols.contains(inventoryProblem.Symbol) {
|
|
1026
|
+
selectedInventoryProblem = true
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
population := materializeEntryUnits(loader, paths, reference.Symbols)
|
|
1031
|
+
state.Units = population.Units
|
|
1032
|
+
state.Hidden = population.Hidden
|
|
1033
|
+
state.Published = population.Published
|
|
1034
|
+
applyTraversedScopes(&state, population.Reached)
|
|
1035
|
+
if !state.Healthy {
|
|
1036
|
+
return state, nil
|
|
1037
|
+
}
|
|
1038
|
+
if len(state.Units) == 0 && !selectedInventoryProblem {
|
|
1039
|
+
return state, []string{
|
|
1040
|
+
claimLabel(claim) + " " + referenceLabel(reference) + " matched " + decimal(len(paths)) + " file(s) but materialized no selected evidence units (" + reference.Symbols.names() + "). Select symbol kinds present in those files or correct the reference globs.",
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return state, nil
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// materializePackageGlobReference narrows an installed package with globs that
|
|
1047
|
+
// resolve against the package root.
|
|
1048
|
+
//
|
|
1049
|
+
// Narrowing a large SDK to one area is what makes the obligation adoptable at
|
|
1050
|
+
// all. The globs are written as a consumer thinks of the package — `lib/api/**`
|
|
1051
|
+
// — rather than carrying the `node_modules` prefix, which is an installation
|
|
1052
|
+
// detail rather than part of the package's shape.
|
|
1053
|
+
//
|
|
1054
|
+
// A matched module is an entry, not a leaf: its own exports and everything it
|
|
1055
|
+
// re-exports both belong to the population, because a glob selects modules
|
|
1056
|
+
// while an obligation is owed by the symbols those modules publish.
|
|
1057
|
+
func materializePackageGlobReference(
|
|
1058
|
+
claim claimSpec,
|
|
1059
|
+
reference referenceSpec,
|
|
1060
|
+
loader *typeScriptLoader,
|
|
1061
|
+
) (referenceState, []string) {
|
|
1062
|
+
state := referenceState{
|
|
1063
|
+
Spec: reference,
|
|
1064
|
+
UnitsByScope: map[string][]*evidenceUnit{},
|
|
1065
|
+
Healthy: true,
|
|
1066
|
+
}
|
|
1067
|
+
base := referenceBase(loader, reference)
|
|
1068
|
+
candidates, walkProblem := loader.walk(base)
|
|
1069
|
+
if walkProblem != "" {
|
|
1070
|
+
state.Healthy = false
|
|
1071
|
+
return state, []string{
|
|
1072
|
+
claimLabel(claim) + " " + referenceLabel(reference) + " could not inspect TypeScript package '" + reference.Package + "': " + walkProblem + ". Fix filesystem access or reinstall the package; coverage cannot be evaluated from a partial population.",
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
problems := []string{}
|
|
1076
|
+
for _, candidate := range candidates {
|
|
1077
|
+
relative := strings.TrimPrefix(strings.TrimPrefix(candidate, base), "/")
|
|
1078
|
+
if !reference.Files.matches(relative) {
|
|
1079
|
+
continue
|
|
1080
|
+
}
|
|
1081
|
+
if loader.inventory(candidate) == nil {
|
|
1082
|
+
state.Healthy = false
|
|
1083
|
+
problems = append(
|
|
1084
|
+
problems,
|
|
1085
|
+
claimLabel(claim)+" "+referenceLabel(reference)+" could not read TypeScript source '"+candidate+"': "+loader.failure(candidate)+". Fix filesystem access or reinstall the package; coverage cannot be evaluated from a partial population.",
|
|
1086
|
+
)
|
|
1087
|
+
continue
|
|
1088
|
+
}
|
|
1089
|
+
state.Paths = append(state.Paths, candidate)
|
|
1090
|
+
}
|
|
1091
|
+
if len(state.Paths) == 0 {
|
|
1092
|
+
if !state.Healthy {
|
|
1093
|
+
return state, problems
|
|
1094
|
+
}
|
|
1095
|
+
return state, []string{
|
|
1096
|
+
claimLabel(claim) + " " + referenceLabel(reference) + " matched no files inside package '" + reference.Package + "' for " + describePatterns(reference.Files) + ". The globs resolve against the package root, not the project root. Check that the installed package actually contains those paths before changing the reference: an empty population demands nothing, so a claim that reaches this state reports full coverage while checking nothing.",
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
// The glob decides membership, the entry decides addresses. A matched module
|
|
1100
|
+
// is still traversed as an entry, because a matched barrel owes what it
|
|
1101
|
+
// publishes rather than only what its own file declares — but that traversal
|
|
1102
|
+
// is used for nothing except which units are in. Their addresses come from
|
|
1103
|
+
// the package entry, which is the only module a consumer has a specifier
|
|
1104
|
+
// for, and it is under that specifier that an inline link is resolved.
|
|
1105
|
+
// Publishing a narrowed unit under the matched module instead is what made
|
|
1106
|
+
// `functional.health.get` collapse to `get` and left it with no spelling
|
|
1107
|
+
// that resolves.
|
|
1108
|
+
membership := materializeEntryUnits(loader, state.Paths, reference.Symbols)
|
|
1109
|
+
population := membership
|
|
1110
|
+
if entry := loader.packageEntryModule(reference.Package); entry != "" {
|
|
1111
|
+
population = narrowTraversedPopulation(
|
|
1112
|
+
materializeEntryUnits(loader, []string{entry}, reference.Symbols),
|
|
1113
|
+
membership,
|
|
1114
|
+
)
|
|
1115
|
+
}
|
|
1116
|
+
state.Units = population.Units
|
|
1117
|
+
state.Hidden = population.Hidden
|
|
1118
|
+
state.Published = population.Published
|
|
1119
|
+
applyTraversedScopes(&state, population.Reached)
|
|
1120
|
+
if !state.Healthy {
|
|
1121
|
+
return state, problems
|
|
1122
|
+
}
|
|
1123
|
+
if len(state.Units) == 0 {
|
|
1124
|
+
return state, []string{
|
|
1125
|
+
claimLabel(claim) + " " + referenceLabel(reference) + " matched " + decimal(len(state.Paths)) + " file(s) inside package '" + reference.Package + "' but materialized no selected evidence units (" + reference.Symbols.names() + ") reachable from the package entry. Select symbol kinds present in those files, correct the globs, or narrow to modules the entry publishes.",
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return state, nil
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
func resolveReferenceEntry(
|
|
1132
|
+
claim claimSpec,
|
|
1133
|
+
reference referenceSpec,
|
|
1134
|
+
loader *typeScriptLoader,
|
|
1135
|
+
) (string, string) {
|
|
1136
|
+
entry := loader.packageEntryModule(reference.Package)
|
|
1137
|
+
if entry == "" {
|
|
1138
|
+
return "", claimLabel(claim) + " " + referenceLabel(reference) + " could not resolve the declaration entry of package '" + reference.Package + "'. Install it, or select its declarations with 'files'; the entry is read from the 'types' condition of 'exports', then 'typesVersions', then 'types'."
|
|
1139
|
+
}
|
|
1140
|
+
return entry, ""
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// scopedTargetKey identifies one address inside one module.
|
|
1144
|
+
//
|
|
1145
|
+
// A struct key rather than a joined string: the map is built once per unit per
|
|
1146
|
+
// reference and read once per citation, and concatenating two paths to hash
|
|
1147
|
+
// them allocates a string whose only purpose is to be thrown away.
|
|
1148
|
+
type scopedTargetKey struct {
|
|
1149
|
+
path string
|
|
1150
|
+
target string
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// looksLikeTypeScriptTarget reports whether an unbraced target names a symbol.
|
|
1154
|
+
//
|
|
1155
|
+
// The migration diagnostic has to be told apart from an ordinary typo, and the
|
|
1156
|
+
// signal is that the same spelling still materializes as a TypeScript unit
|
|
1157
|
+
// somewhere in the configured graph. A Markdown path or a Swagger operation
|
|
1158
|
+
// never does, so neither is mistaken for a symbol that lost its braces.
|
|
1159
|
+
func looksLikeTypeScriptTarget(
|
|
1160
|
+
target string,
|
|
1161
|
+
targets map[string]map[string]*evidenceUnit,
|
|
1162
|
+
markdownTargets map[string]map[string]*evidenceUnit,
|
|
1163
|
+
) bool {
|
|
1164
|
+
if len(markdownTargets[target]) != 0 {
|
|
1165
|
+
return false
|
|
1166
|
+
}
|
|
1167
|
+
for _, unit := range targets[target] {
|
|
1168
|
+
if unit.Type == artifactTypeScript {
|
|
1169
|
+
return true
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return false
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// resolveInlineLinkDeclaration resolves a braced target through the citing
|
|
1176
|
+
// module's imports, the way TypeScript resolves the same name.
|
|
1177
|
+
//
|
|
1178
|
+
// Every failure gets its own diagnostic. A single "unresolved" would leave the
|
|
1179
|
+
// author guessing which of four independent things went wrong, and three of
|
|
1180
|
+
// them are repaired in completely different places.
|
|
1181
|
+
func resolveInlineLinkDeclaration(
|
|
1182
|
+
declaration *evidenceDeclaration,
|
|
1183
|
+
loader *typeScriptLoader,
|
|
1184
|
+
scopedTargets map[scopedTargetKey]map[string]*evidenceUnit,
|
|
1185
|
+
scopedHidden map[scopedTargetKey]*evidenceUnit,
|
|
1186
|
+
context string,
|
|
1187
|
+
) (string, string) {
|
|
1188
|
+
target := inlineLinkTarget(declaration.Target)
|
|
1189
|
+
if declaration.Type != artifactTypeScript {
|
|
1190
|
+
return "", "Inline link target '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context + ": only a TypeScript declaration can cite through an inline link, because resolution runs through the citing module's imports and a " + string(declaration.Type) + " comment has none. Use a path-addressed target selected by one of the named references; a TypeScript symbol must instead be cited from a TypeScript claim."
|
|
1191
|
+
}
|
|
1192
|
+
inventory := loader.inventory(declaration.Path)
|
|
1193
|
+
if inventory == nil {
|
|
1194
|
+
return "", "Inline link target '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context + ": the citing file is not part of the TypeScript program, so it has no import scope to resolve against. Include the file in the project or move the citation to a configured TypeScript claim file."
|
|
1195
|
+
}
|
|
1196
|
+
segments := strings.Split(target, ".")
|
|
1197
|
+
binding, imported := inventory.Imports[segments[0]]
|
|
1198
|
+
if !imported {
|
|
1199
|
+
return "", "Unimported evidence target '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context + ": '" + segments[0] + "' is not imported by this module, so the citation names a symbol this file does not reference. Import it; 'import type' is enough and is erased at emit."
|
|
1200
|
+
}
|
|
1201
|
+
// Resolution goes through the same loader the population uses, so a citation
|
|
1202
|
+
// can reach a package entry that never entered the Program — which is the
|
|
1203
|
+
// only way an import of an installed SDK resolves at all.
|
|
1204
|
+
resolvedPath := loader.resolve(declaration.Path, binding.Specifier)
|
|
1205
|
+
if resolvedPath == "" {
|
|
1206
|
+
return "", "Unresolved module '" + binding.Specifier + "' for evidence target '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context + ": the specifier resolves to no TypeScript file reachable from this project. Correct the import, or make the named reference reach the module."
|
|
1207
|
+
}
|
|
1208
|
+
remaining := segments[1:]
|
|
1209
|
+
if !binding.Namespace {
|
|
1210
|
+
remaining = append([]string{binding.Imported}, remaining...)
|
|
1211
|
+
}
|
|
1212
|
+
if len(remaining) == 0 {
|
|
1213
|
+
return "", "Incomplete evidence target '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context + ": a namespace import names a module rather than a unit. Name a symbol inside '" + binding.Specifier + "' that the named reference selects."
|
|
1214
|
+
}
|
|
1215
|
+
name := strings.Join(remaining, ".")
|
|
1216
|
+
candidates := scopedTargets[scopedTargetKey{path: resolvedPath, target: name}]
|
|
1217
|
+
switch len(candidates) {
|
|
1218
|
+
case 0:
|
|
1219
|
+
if hidden := scopedHidden[scopedTargetKey{
|
|
1220
|
+
path: resolvedPath,
|
|
1221
|
+
target: name,
|
|
1222
|
+
}]; hidden != nil {
|
|
1223
|
+
return "", hiddenTargetProblem(declaration, hidden, context)
|
|
1224
|
+
}
|
|
1225
|
+
return "", "Unreachable evidence target '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context + ": '" + resolvedPath + "' declares no selected unit named '" + name + "'. Correct the target, or widen the named reference's files and symbol selection so that unit is configured evidence."
|
|
1226
|
+
case 1:
|
|
1227
|
+
for _, unit := range candidates {
|
|
1228
|
+
return unit.ID, ""
|
|
1229
|
+
}
|
|
1230
|
+
return "", ""
|
|
1231
|
+
default:
|
|
1232
|
+
// One module may spell one name in two declaration spaces — a type and a
|
|
1233
|
+
// callable, say. Resolution landed in the right file and still cannot say
|
|
1234
|
+
// which unit was meant, and picking one silently would acknowledge an
|
|
1235
|
+
// obligation the author never cited.
|
|
1236
|
+
descriptions := make([]string, 0, len(candidates))
|
|
1237
|
+
for _, unit := range candidates {
|
|
1238
|
+
descriptions = append(descriptions, unit.Readable+" at "+unit.location())
|
|
1239
|
+
}
|
|
1240
|
+
sort.Strings(descriptions)
|
|
1241
|
+
return "", "Ambiguous evidence target '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context + ": '" + resolvedPath + "' declares " + strings.Join(descriptions, "; ") + " under that name. Narrow the named reference's symbol selection so the target has exactly one meaning."
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
func declarationCandidates(
|
|
1246
|
+
target string,
|
|
1247
|
+
targets map[string]map[string]*evidenceUnit,
|
|
1248
|
+
markdownTargets map[string]map[string]*evidenceUnit,
|
|
1249
|
+
) map[string]*evidenceUnit {
|
|
1250
|
+
candidates := map[string]*evidenceUnit{}
|
|
1251
|
+
for id, unit := range targets[target] {
|
|
1252
|
+
candidates[id] = unit
|
|
1253
|
+
}
|
|
1254
|
+
normalized := normalizeMarkdownTarget(target)
|
|
1255
|
+
if normalized != target {
|
|
1256
|
+
for id, unit := range markdownTargets[normalized] {
|
|
1257
|
+
candidates[id] = unit
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return candidates
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// splitCodeCandidates separates the units a non-TypeScript claim may address
|
|
1264
|
+
// from the ones only an inline link can reach.
|
|
1265
|
+
//
|
|
1266
|
+
// Both halves are returned because the caller has to tell "this target names
|
|
1267
|
+
// something else entirely" from "this target names a symbol, and that is the
|
|
1268
|
+
// problem". Reporting the second as an unresolved target would be true and
|
|
1269
|
+
// useless: the unit exists, and nothing in the message would say why naming it
|
|
1270
|
+
// here cannot work.
|
|
1271
|
+
func splitCodeCandidates(
|
|
1272
|
+
candidates map[string]*evidenceUnit,
|
|
1273
|
+
) (map[string]*evidenceUnit, map[string]*evidenceUnit) {
|
|
1274
|
+
addressable := map[string]*evidenceUnit{}
|
|
1275
|
+
code := map[string]*evidenceUnit{}
|
|
1276
|
+
for id, unit := range candidates {
|
|
1277
|
+
if unit.Type == artifactTypeScript {
|
|
1278
|
+
code[id] = unit
|
|
1279
|
+
continue
|
|
1280
|
+
}
|
|
1281
|
+
addressable[id] = unit
|
|
1282
|
+
}
|
|
1283
|
+
return addressable, code
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
func inventoriesOf(
|
|
1287
|
+
kind artifactKind,
|
|
1288
|
+
markdown map[string]*artifactInventory,
|
|
1289
|
+
prisma map[string]*artifactInventory,
|
|
1290
|
+
swagger map[string]*artifactInventory,
|
|
1291
|
+
typescript map[string]*artifactInventory,
|
|
1292
|
+
) map[string]*artifactInventory {
|
|
1293
|
+
switch kind {
|
|
1294
|
+
case artifactMarkdown:
|
|
1295
|
+
return markdown
|
|
1296
|
+
case artifactPrisma:
|
|
1297
|
+
return prisma
|
|
1298
|
+
case artifactSwagger:
|
|
1299
|
+
return swagger
|
|
1300
|
+
case artifactTypeScript:
|
|
1301
|
+
return typescript
|
|
1302
|
+
default:
|
|
1303
|
+
return map[string]*artifactInventory{}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
func matchingReferencePaths(
|
|
1308
|
+
inventories map[string]*artifactInventory,
|
|
1309
|
+
reference referenceSpec,
|
|
1310
|
+
) []string {
|
|
1311
|
+
if reference.Type != artifactSwagger {
|
|
1312
|
+
return matchingInventoryPaths(inventories, reference.Base, reference.Files)
|
|
1313
|
+
}
|
|
1314
|
+
if inventories[reference.Source] == nil {
|
|
1315
|
+
return nil
|
|
1316
|
+
}
|
|
1317
|
+
return []string{reference.Source}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// matchingInventoryPaths selects the files one population owns.
|
|
1321
|
+
//
|
|
1322
|
+
// Matching runs against the base-relative path rather than against the key,
|
|
1323
|
+
// which is what keeps a pattern written as `requirements/**/*.md` meaning the
|
|
1324
|
+
// same thing whether its root is the project or a directory two levels above
|
|
1325
|
+
// it. An address composed for another base is skipped outright, so a file
|
|
1326
|
+
// loaded for one root is never offered to a population that cannot address it.
|
|
1327
|
+
func matchingInventoryPaths(
|
|
1328
|
+
inventories map[string]*artifactInventory,
|
|
1329
|
+
base populationBase,
|
|
1330
|
+
globs globSet,
|
|
1331
|
+
) []string {
|
|
1332
|
+
paths := []string{}
|
|
1333
|
+
for key := range inventories {
|
|
1334
|
+
relative, owned := base.relativeOf(key)
|
|
1335
|
+
if !owned || !globs.matches(relative) {
|
|
1336
|
+
continue
|
|
1337
|
+
}
|
|
1338
|
+
paths = append(paths, key)
|
|
1339
|
+
}
|
|
1340
|
+
sort.Strings(paths)
|
|
1341
|
+
return paths
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
func sortUnits(units []*evidenceUnit) {
|
|
1345
|
+
sort.Slice(units, func(left int, right int) bool {
|
|
1346
|
+
if units[left].Target != units[right].Target {
|
|
1347
|
+
return units[left].Target < units[right].Target
|
|
1348
|
+
}
|
|
1349
|
+
return units[left].ID < units[right].ID
|
|
1350
|
+
})
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
func claimLabel(claim claimSpec) string {
|
|
1354
|
+
label := "Claim " + decimal(claim.Index+1)
|
|
1355
|
+
if claim.Name != "" {
|
|
1356
|
+
label += " ('" + claim.Name + "')"
|
|
1357
|
+
}
|
|
1358
|
+
return label
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
func referenceLabel(reference referenceSpec) string {
|
|
1362
|
+
if reference.Type == artifactSwagger {
|
|
1363
|
+
return "reference " + decimal(reference.Index+1) + " (swagger operations)"
|
|
1364
|
+
}
|
|
1365
|
+
return "reference " + decimal(reference.Index+1) + " (" + string(reference.Type) + ", symbols: " + reference.Symbols.names() + ")"
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
func declarationObligationContext(owners []claimState) string {
|
|
1369
|
+
groups := make([]string, 0, len(owners))
|
|
1370
|
+
for _, owner := range owners {
|
|
1371
|
+
references := make([]string, 0, len(owner.Spec.References))
|
|
1372
|
+
for _, reference := range owner.Spec.References {
|
|
1373
|
+
references = append(references, referenceLabel(reference))
|
|
1374
|
+
}
|
|
1375
|
+
if len(references) == 0 {
|
|
1376
|
+
groups = append(groups, claimLabel(owner.Spec))
|
|
1377
|
+
continue
|
|
1378
|
+
}
|
|
1379
|
+
groups = append(
|
|
1380
|
+
groups,
|
|
1381
|
+
claimLabel(owner.Spec)+" across "+strings.Join(references, ", "),
|
|
1382
|
+
)
|
|
1383
|
+
}
|
|
1384
|
+
if len(groups) == 0 {
|
|
1385
|
+
return "no matched claim"
|
|
1386
|
+
}
|
|
1387
|
+
return strings.Join(groups, "; ")
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
func appendUniqueString(values []string, candidate string) []string {
|
|
1391
|
+
for _, value := range values {
|
|
1392
|
+
if value == candidate {
|
|
1393
|
+
return values
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return append(values, candidate)
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
func reportProblems(ctx *rule.ProjectContext, problems []string) {
|
|
1400
|
+
sort.Strings(problems)
|
|
1401
|
+
previous := ""
|
|
1402
|
+
for _, problem := range problems {
|
|
1403
|
+
if problem == "" || problem == previous {
|
|
1404
|
+
continue
|
|
1405
|
+
}
|
|
1406
|
+
ctx.Report(problem)
|
|
1407
|
+
previous = problem
|
|
1408
|
+
}
|
|
1409
|
+
}
|