ttsc 0.18.4 → 0.19.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/README.md +5 -6
- package/cmd/graphbench/main.go +1 -1
- package/cmd/platform/main.go +1 -48
- package/cmd/ttsc/build_branches_test.go +5 -1
- package/cmd/ttsc/main.go +0 -50
- package/cmd/ttsc/main_alias_branches_test.go +6 -12
- package/cmd/ttscgraph/dump.go +13 -3
- package/cmd/ttscserver/main.go +5 -3
- package/driver/lsp.go +13 -1
- package/driver/plugins.go +35 -0
- package/driver/program.go +7 -8
- package/driver/rewrite.go +381 -21
- package/go.mod +1 -0
- package/internal/graph/build.go +297 -3
- package/internal/graph/build_records_a_node_per_top_level_declaration_test.go +16 -2
- package/internal/graph/diagnostics.go +5 -4
- package/internal/graph/dump.go +60 -4
- package/internal/graph/dump_is_a_function_of_the_source_test.go +71 -0
- package/internal/graph/dump_serializes_the_full_graph_test.go +16 -4
- package/internal/graph/edges.go +122 -11
- package/internal/graph/exports.go +74 -17
- package/internal/graph/graph.go +33 -1
- package/internal/graph/node_identity_is_position_invariant_test.go +4 -2
- package/internal/graph/resolve.go +24 -0
- package/internal/graphsymbols/provider.go +50 -14
- package/internal/lspserver/lsp_native_plugin_source.go +77 -23
- package/internal/lspserver/lsp_plugin.go +19 -2
- package/internal/lspserver/lsp_proxy.go +244 -52
- package/internal/lspserver/lsp_server.go +35 -27
- package/internal/lspserver/lsp_symbols.go +25 -16
- package/lib/TtscCompiler.d.ts +10 -0
- package/lib/TtscCompiler.js +24 -6
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/TtscService.js +9 -7
- package/lib/TtscService.js.map +1 -1
- package/lib/compiler/internal/compileProjectInMemory.js +1 -0
- package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
- package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +3 -0
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js +14 -0
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -0
- package/lib/compiler/internal/project/readProjectConfig.js +53 -4
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/project/resolveProjectConfig.d.ts +9 -2
- package/lib/compiler/internal/project/resolveProjectConfig.js +45 -16
- package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
- package/lib/compiler/internal/residentTransformProcess.d.ts +16 -5
- package/lib/compiler/internal/residentTransformProcess.js +68 -20
- package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +11 -0
- package/lib/compiler/internal/runBuild.js +44 -8
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.js +7 -4
- package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
- package/lib/compiler/internal/sharedHostHelpers.d.ts +16 -0
- package/lib/compiler/internal/sharedHostHelpers.js +23 -0
- package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
- package/lib/compiler/internal/startResidentTransform.js +18 -1
- package/lib/compiler/internal/startResidentTransform.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.js +34 -8
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/flags/parser.d.ts +17 -0
- package/lib/flags/parser.js +11 -1
- package/lib/flags/parser.js.map +1 -1
- package/lib/flags/schema.js +7 -0
- package/lib/flags/schema.js.map +1 -1
- package/lib/launcher/internal/prepareExecution.js +21 -2
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/runTtsc.js +13 -53
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.js +11 -0
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +27 -11
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +81 -0
- package/lib/launcher/internal/runtimeHooks.js +632 -43
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/launcher/internal/servedSourceMap.d.ts +25 -0
- package/lib/launcher/internal/servedSourceMap.js +164 -0
- package/lib/launcher/internal/servedSourceMap.js.map +1 -0
- package/lib/plugin/internal/buildSourcePlugin.d.ts +146 -2
- package/lib/plugin/internal/buildSourcePlugin.js +651 -113
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +7 -0
- package/lib/plugin/internal/loadProjectPlugins.js +33 -10
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscCompilerContext.d.ts +16 -2
- package/lib/structures/ITtscPluginCapabilities.d.ts +8 -0
- package/lib/structures/ITtscPluginFactoryContext.d.ts +14 -0
- package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +3 -0
- package/lib/structures/internal/ITtscProjectIdentity.d.ts +17 -0
- package/lib/structures/internal/ITtscProjectIdentity.js +3 -0
- package/lib/structures/internal/ITtscProjectIdentity.js.map +1 -0
- package/lib/structures/internal/TtscCommonOptions.d.ts +8 -0
- package/package.json +11 -8
- package/shim/ast/lint.go +10 -0
- package/shim/ast/shim.go +44 -0
- package/shim/checker/enums_gen.go +19 -0
- package/shim/checker/shim.go +130 -0
- package/shim/diagnosticwriter/lint.go +30 -3
- package/shim/scanner/regexp.go +27 -0
- package/src/TtscCompiler.ts +25 -6
- package/src/TtscService.ts +15 -7
- package/src/compiler/internal/compileProjectInMemory.ts +1 -0
- package/src/compiler/internal/project/createNativeProjectContextArgs.ts +15 -0
- package/src/compiler/internal/project/readProjectConfig.ts +61 -5
- package/src/compiler/internal/project/resolveProjectConfig.ts +51 -21
- package/src/compiler/internal/residentTransformProcess.ts +95 -21
- package/src/compiler/internal/runBuild.ts +76 -9
- package/src/compiler/internal/runSingleFileEmit.ts +7 -4
- package/src/compiler/internal/sharedHostHelpers.ts +24 -0
- package/src/compiler/internal/startResidentTransform.ts +21 -1
- package/src/compiler/internal/transformProjectInMemory.ts +49 -6
- package/src/flags/parser.ts +28 -1
- package/src/flags/schema.ts +8 -0
- package/src/launcher/internal/prepareExecution.ts +22 -2
- package/src/launcher/internal/runTtsc.ts +13 -61
- package/src/launcher/internal/runTtscserver.ts +14 -0
- package/src/launcher/internal/runTtsx.ts +28 -13
- package/src/launcher/internal/runtimeHooks.ts +771 -53
- package/src/launcher/internal/servedSourceMap.ts +183 -0
- package/src/plugin/internal/buildSourcePlugin.ts +859 -122
- package/src/plugin/internal/loadProjectPlugins.ts +36 -6
- package/src/structures/ITtscCompilerContext.ts +17 -2
- package/src/structures/ITtscPluginCapabilities.ts +9 -0
- package/src/structures/ITtscPluginFactoryContext.ts +15 -0
- package/src/structures/internal/ITtscParsedProjectConfig.ts +3 -0
- package/src/structures/internal/ITtscProjectIdentity.ts +17 -0
- package/src/structures/internal/TtscCommonOptions.ts +8 -0
- package/tools/shim_audit/baseline.json +16 -7
- package/tools/shim_audit/main.go +777 -71
- package/tools/shim_audit/main_test.go +28 -1
- package/tools/shim_audit/producer_exemptions_require_rationale_test.go +42 -0
- package/tools/shim_audit/producer_surface_includes_exposed_methods_test.go +154 -0
- package/tools/shim_audit/producer_surface_rejects_unproducible_reference_test.go +142 -0
- package/utility/host.go +46 -20
- package/utility/serve.go +1 -1
- package/shim/checker/signature_api_completeness.go +0 -58
package/tools/shim_audit/main.go
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// The invariant. The shim should be TRANSITIVELY CLOSED over the operations of
|
|
11
11
|
// the types it already exposes. If the shim already aliases an upstream type T,
|
|
12
12
|
// then everything a plugin can *reach* starting from T should also be reachable
|
|
13
|
-
// through the shim.
|
|
13
|
+
// through the shim. Four closure rules are checked:
|
|
14
14
|
//
|
|
15
15
|
// - ENUM every exported const whose type is an already-exposed enum type
|
|
16
16
|
// must be re-exported. (This is the `SignatureKindConstruct` class —
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
// - ESCAPE every exported named type that appears in the signature of an
|
|
23
23
|
// already-exposed operation but is not itself aliased. (A plugin can
|
|
24
24
|
// obtain the value but cannot name its type.)
|
|
25
|
+
// - PRODUCER every pointer-like compiler object consumed by a public shim
|
|
26
|
+
// operation must be obtainable from an explicit root or a public operation
|
|
27
|
+
// whose compiler-object inputs and receiver are themselves obtainable. A
|
|
28
|
+
// type alias alone only makes the object nameable; it provides no value.
|
|
25
29
|
//
|
|
26
30
|
// What it deliberately does NOT find: UNEXPORTED helpers a plugin needs by name
|
|
27
31
|
// (e.g. `(*Checker).getMinArgumentCount`, #230 rule 2). Those are invisible to
|
|
@@ -101,6 +105,79 @@ func (r reachable) add(pkg, name string) {
|
|
|
101
105
|
r[pkg][name] = true
|
|
102
106
|
}
|
|
103
107
|
|
|
108
|
+
// flowType identifies an internal compiler object in the public wrapper flow.
|
|
109
|
+
// Package suffixes retain nested shim ownership (for example vfs/osvfs).
|
|
110
|
+
type flowType struct {
|
|
111
|
+
pkg string
|
|
112
|
+
name string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// producerSurface records the public shim operations that consume and
|
|
116
|
+
// produce pointer-like compiler objects. Values are operation names so a gap
|
|
117
|
+
// explains which public wrapper requires the missing producer.
|
|
118
|
+
type producerSurface struct {
|
|
119
|
+
consumed map[flowType]map[string]bool
|
|
120
|
+
produced map[flowType]map[string]bool
|
|
121
|
+
operations []operationFlow
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// operationFlow keeps results conditional on every compiler-object input and,
|
|
125
|
+
// for methods, an obtainable receiver. Flattening results into the unconditional
|
|
126
|
+
// producer set would let rootless function or method cycles satisfy the audit.
|
|
127
|
+
type operationFlow struct {
|
|
128
|
+
receiver *flowType
|
|
129
|
+
consumed map[flowType]map[string]bool
|
|
130
|
+
produced map[flowType]map[string]bool
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// localFlowDefinition resolves named callback and container contracts declared
|
|
134
|
+
// by the shim itself. Alias definitions preserve the surrounding pointer state;
|
|
135
|
+
// defined types expose their callable/container underlying shape but are not
|
|
136
|
+
// mistaken for the internal named type they were declared from.
|
|
137
|
+
type localFlowDefinition struct {
|
|
138
|
+
expression ast.Expr
|
|
139
|
+
aliases map[string]string
|
|
140
|
+
alias bool
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
type localFlowVisit struct {
|
|
144
|
+
name string
|
|
145
|
+
direction flowDirection
|
|
146
|
+
pointerLike bool
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
func newProducerSurface() producerSurface {
|
|
150
|
+
return producerSurface{
|
|
151
|
+
consumed: map[flowType]map[string]bool{},
|
|
152
|
+
produced: map[flowType]map[string]bool{},
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
func (s producerSurface) add(direction flowDirection, typ flowType, operation string) {
|
|
157
|
+
target := s.consumed
|
|
158
|
+
if direction == flowProduce {
|
|
159
|
+
target = s.produced
|
|
160
|
+
}
|
|
161
|
+
if target[typ] == nil {
|
|
162
|
+
target[typ] = map[string]bool{}
|
|
163
|
+
}
|
|
164
|
+
target[typ][operation] = true
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
type flowDirection uint8
|
|
168
|
+
|
|
169
|
+
const (
|
|
170
|
+
flowConsume flowDirection = iota
|
|
171
|
+
flowProduce
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
func opposite(direction flowDirection) flowDirection {
|
|
175
|
+
if direction == flowConsume {
|
|
176
|
+
return flowProduce
|
|
177
|
+
}
|
|
178
|
+
return flowConsume
|
|
179
|
+
}
|
|
180
|
+
|
|
104
181
|
// linknameRe captures the trailing symbol name of a go:linkname target like
|
|
105
182
|
// `...internal/checker.(*Checker).getMinArgumentCount` or `...checker.foo`.
|
|
106
183
|
var linknameRe = regexp.MustCompile(`(?m)^//go:linkname\s+\S+\s+` +
|
|
@@ -160,6 +237,231 @@ func scanShimReachable(shimRoot string) (reachable, error) {
|
|
|
160
237
|
return r, nil
|
|
161
238
|
}
|
|
162
239
|
|
|
240
|
+
// scanShimProducerSurface parses normal shim source and models value flow over
|
|
241
|
+
// exported package functions and methods. A pointer to an internal named type is a
|
|
242
|
+
// compiler-owned graph object: parameters consume one and results produce one.
|
|
243
|
+
// Callback variance is reversed for callback parameters and preserved for
|
|
244
|
+
// callback results, so an input callback's arguments count as values produced
|
|
245
|
+
// by the shim rather than values the plugin must somehow manufacture.
|
|
246
|
+
func scanShimProducerSurface(shimRoot string, inner map[string]*packages.Package) (producerSurface, error) {
|
|
247
|
+
surface := newProducerSurface()
|
|
248
|
+
localTypes := map[string]map[string]localFlowDefinition{}
|
|
249
|
+
sources := map[string]map[string][]byte{}
|
|
250
|
+
for dir, suffix := range shimDirs {
|
|
251
|
+
paths, _ := filepath.Glob(filepath.Join(shimRoot, dir, "*.go"))
|
|
252
|
+
for _, path := range paths {
|
|
253
|
+
if strings.HasSuffix(path, "_test.go") {
|
|
254
|
+
continue
|
|
255
|
+
}
|
|
256
|
+
src, err := os.ReadFile(path)
|
|
257
|
+
if err != nil {
|
|
258
|
+
return producerSurface{}, err
|
|
259
|
+
}
|
|
260
|
+
if sources[suffix] == nil {
|
|
261
|
+
sources[suffix] = map[string][]byte{}
|
|
262
|
+
}
|
|
263
|
+
sources[suffix][path] = src
|
|
264
|
+
imports, err := internalImportAliases(src, path)
|
|
265
|
+
if err != nil {
|
|
266
|
+
return producerSurface{}, err
|
|
267
|
+
}
|
|
268
|
+
for _, importedSuffix := range imports {
|
|
269
|
+
if inner[importedSuffix] == nil {
|
|
270
|
+
return producerSurface{}, fmt.Errorf("%s: internal package %s has no loaded type information", path, importedSuffix)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
definitions, err := scanLocalFlowDefinitions(src, path)
|
|
274
|
+
if err != nil {
|
|
275
|
+
return producerSurface{}, err
|
|
276
|
+
}
|
|
277
|
+
if localTypes[suffix] == nil {
|
|
278
|
+
localTypes[suffix] = map[string]localFlowDefinition{}
|
|
279
|
+
}
|
|
280
|
+
for name, definition := range definitions {
|
|
281
|
+
localTypes[suffix][name] = definition
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for suffix, files := range sources {
|
|
286
|
+
for path, src := range files {
|
|
287
|
+
if err := scanProducerFile(src, path, suffix, localTypes[suffix], inner, &surface); err != nil {
|
|
288
|
+
return producerSurface{}, err
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return surface, nil
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
func scanLocalFlowDefinitions(src []byte, filename string) (map[string]localFlowDefinition, error) {
|
|
296
|
+
aliases, err := internalImportAliases(src, filename)
|
|
297
|
+
if err != nil {
|
|
298
|
+
return nil, err
|
|
299
|
+
}
|
|
300
|
+
file, err := parser.ParseFile(token.NewFileSet(), filename, src, parser.SkipObjectResolution)
|
|
301
|
+
if err != nil {
|
|
302
|
+
return nil, fmt.Errorf("%s: %w", filename, err)
|
|
303
|
+
}
|
|
304
|
+
definitions := map[string]localFlowDefinition{}
|
|
305
|
+
for _, decl := range file.Decls {
|
|
306
|
+
gen, ok := decl.(*ast.GenDecl)
|
|
307
|
+
if !ok || gen.Tok != token.TYPE {
|
|
308
|
+
continue
|
|
309
|
+
}
|
|
310
|
+
for _, spec := range gen.Specs {
|
|
311
|
+
typ, ok := spec.(*ast.TypeSpec)
|
|
312
|
+
if !ok {
|
|
313
|
+
continue
|
|
314
|
+
}
|
|
315
|
+
definitions[typ.Name.Name] = localFlowDefinition{
|
|
316
|
+
expression: typ.Type,
|
|
317
|
+
aliases: aliases,
|
|
318
|
+
alias: typ.Assign.IsValid(),
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return definitions, nil
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
func internalImportAliases(src []byte, filename string) (map[string]string, error) {
|
|
326
|
+
file, err := parser.ParseFile(token.NewFileSet(), filename, src, parser.SkipObjectResolution)
|
|
327
|
+
if err != nil {
|
|
328
|
+
return nil, fmt.Errorf("%s: %w", filename, err)
|
|
329
|
+
}
|
|
330
|
+
aliases := map[string]string{}
|
|
331
|
+
for _, imp := range file.Imports {
|
|
332
|
+
path := strings.Trim(imp.Path.Value, `"`)
|
|
333
|
+
if !strings.HasPrefix(path, internalPrefix) {
|
|
334
|
+
continue
|
|
335
|
+
}
|
|
336
|
+
importedSuffix := strings.TrimPrefix(path, internalPrefix)
|
|
337
|
+
name := importedSuffix[strings.LastIndex(importedSuffix, "/")+1:]
|
|
338
|
+
if imp.Name != nil {
|
|
339
|
+
name = imp.Name.Name
|
|
340
|
+
}
|
|
341
|
+
aliases[name] = importedSuffix
|
|
342
|
+
}
|
|
343
|
+
return aliases, nil
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
func scanProducerFile(src []byte, filename, suffix string, localTypes map[string]localFlowDefinition, inner map[string]*packages.Package, surface *producerSurface) error {
|
|
347
|
+
file, err := parser.ParseFile(token.NewFileSet(), filename, src, parser.SkipObjectResolution)
|
|
348
|
+
if err != nil {
|
|
349
|
+
return fmt.Errorf("%s: %w", filename, err)
|
|
350
|
+
}
|
|
351
|
+
aliases, err := internalImportAliases(src, filename)
|
|
352
|
+
if err != nil {
|
|
353
|
+
return err
|
|
354
|
+
}
|
|
355
|
+
for _, decl := range file.Decls {
|
|
356
|
+
fn, ok := decl.(*ast.FuncDecl)
|
|
357
|
+
if !ok || !ast.IsExported(fn.Name.Name) {
|
|
358
|
+
continue
|
|
359
|
+
}
|
|
360
|
+
operation := suffix + "." + fn.Name.Name
|
|
361
|
+
if fn.Recv != nil && len(fn.Recv.List) > 0 {
|
|
362
|
+
operation = suffix + "." + receiverTypeName(fn.Recv.List[0].Type) + "." + fn.Name.Name
|
|
363
|
+
}
|
|
364
|
+
operationSurface := newProducerSurface()
|
|
365
|
+
collectFieldFlow(fn.Type.Params, aliases, localTypes, inner, flowConsume, false, operation, operationSurface)
|
|
366
|
+
collectFieldFlow(fn.Type.Results, aliases, localTypes, inner, flowProduce, false, operation, operationSurface)
|
|
367
|
+
if len(operationSurface.consumed) == 0 && len(operationSurface.produced) == 0 {
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
370
|
+
surface.operations = append(surface.operations, operationFlow{
|
|
371
|
+
consumed: operationSurface.consumed,
|
|
372
|
+
produced: operationSurface.produced,
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
return nil
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
func receiverTypeName(expr ast.Expr) string {
|
|
379
|
+
switch receiver := expr.(type) {
|
|
380
|
+
case *ast.Ident:
|
|
381
|
+
return receiver.Name
|
|
382
|
+
case *ast.StarExpr:
|
|
383
|
+
return receiverTypeName(receiver.X)
|
|
384
|
+
case *ast.IndexExpr:
|
|
385
|
+
return receiverTypeName(receiver.X)
|
|
386
|
+
case *ast.IndexListExpr:
|
|
387
|
+
return receiverTypeName(receiver.X)
|
|
388
|
+
default:
|
|
389
|
+
return "receiver"
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
func collectFieldFlow(fields *ast.FieldList, aliases map[string]string, localTypes map[string]localFlowDefinition, inner map[string]*packages.Package, direction flowDirection, pointerLike bool, operation string, surface producerSurface) {
|
|
394
|
+
collectFieldFlowSeen(fields, aliases, localTypes, inner, direction, pointerLike, operation, surface, map[localFlowVisit]bool{})
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
func collectFieldFlowSeen(fields *ast.FieldList, aliases map[string]string, localTypes map[string]localFlowDefinition, inner map[string]*packages.Package, direction flowDirection, pointerLike bool, operation string, surface producerSurface, seen map[localFlowVisit]bool) {
|
|
398
|
+
if fields == nil {
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
for _, field := range fields.List {
|
|
402
|
+
collectTypeFlowSeen(field.Type, aliases, localTypes, inner, direction, pointerLike, operation, surface, seen)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
func collectTypeFlowSeen(expr ast.Expr, aliases map[string]string, localTypes map[string]localFlowDefinition, inner map[string]*packages.Package, direction flowDirection, pointerLike bool, operation string, surface producerSurface, seen map[localFlowVisit]bool) {
|
|
407
|
+
switch node := expr.(type) {
|
|
408
|
+
case *ast.StarExpr:
|
|
409
|
+
collectTypeFlowSeen(node.X, aliases, localTypes, inner, direction, true, operation, surface, seen)
|
|
410
|
+
case *ast.ArrayType:
|
|
411
|
+
collectTypeFlowSeen(node.Elt, aliases, localTypes, inner, direction, false, operation, surface, seen)
|
|
412
|
+
case *ast.Ellipsis:
|
|
413
|
+
collectTypeFlowSeen(node.Elt, aliases, localTypes, inner, direction, false, operation, surface, seen)
|
|
414
|
+
case *ast.MapType:
|
|
415
|
+
collectTypeFlowSeen(node.Key, aliases, localTypes, inner, direction, false, operation, surface, seen)
|
|
416
|
+
collectTypeFlowSeen(node.Value, aliases, localTypes, inner, direction, false, operation, surface, seen)
|
|
417
|
+
case *ast.ChanType:
|
|
418
|
+
collectTypeFlowSeen(node.Value, aliases, localTypes, inner, direction, false, operation, surface, seen)
|
|
419
|
+
case *ast.ParenExpr:
|
|
420
|
+
collectTypeFlowSeen(node.X, aliases, localTypes, inner, direction, pointerLike, operation, surface, seen)
|
|
421
|
+
case *ast.IndexExpr:
|
|
422
|
+
collectTypeFlowSeen(node.X, aliases, localTypes, inner, direction, pointerLike, operation, surface, seen)
|
|
423
|
+
collectTypeFlowSeen(node.Index, aliases, localTypes, inner, direction, false, operation, surface, seen)
|
|
424
|
+
case *ast.IndexListExpr:
|
|
425
|
+
collectTypeFlowSeen(node.X, aliases, localTypes, inner, direction, pointerLike, operation, surface, seen)
|
|
426
|
+
for _, index := range node.Indices {
|
|
427
|
+
collectTypeFlowSeen(index, aliases, localTypes, inner, direction, false, operation, surface, seen)
|
|
428
|
+
}
|
|
429
|
+
case *ast.FuncType:
|
|
430
|
+
collectFieldFlowSeen(node.Params, aliases, localTypes, inner, opposite(direction), false, operation, surface, seen)
|
|
431
|
+
collectFieldFlowSeen(node.Results, aliases, localTypes, inner, direction, false, operation, surface, seen)
|
|
432
|
+
case *ast.InterfaceType:
|
|
433
|
+
collectFieldFlowSeen(node.Methods, aliases, localTypes, inner, direction, false, operation, surface, seen)
|
|
434
|
+
case *ast.Ident:
|
|
435
|
+
definition, local := localTypes[node.Name]
|
|
436
|
+
if !local {
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
visit := localFlowVisit{name: node.Name, direction: direction, pointerLike: pointerLike}
|
|
440
|
+
if seen[visit] {
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
seen[visit] = true
|
|
444
|
+
collectTypeFlowSeen(definition.expression, definition.aliases, localTypes, inner, direction, definition.alias && pointerLike, operation, surface, seen)
|
|
445
|
+
case *ast.SelectorExpr:
|
|
446
|
+
qualifier, ok := node.X.(*ast.Ident)
|
|
447
|
+
if !ok || !ast.IsExported(node.Sel.Name) {
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
importedSuffix, internal := aliases[qualifier.Name]
|
|
451
|
+
if !internal {
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
typ := flowType{pkg: importedSuffix, name: node.Sel.Name}
|
|
455
|
+
if pointerLike {
|
|
456
|
+
surface.add(direction, typ, operation)
|
|
457
|
+
} else if pkg := inner[importedSuffix]; pkg != nil && pkg.Types != nil {
|
|
458
|
+
if typeName, ok := pkg.Types.Scope().Lookup(node.Sel.Name).(*types.TypeName); ok {
|
|
459
|
+
collectGoTypeFlow(typeName.Type(), direction, false, operation, surface)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
163
465
|
// hasShimSource reports whether dir holds non-test Go source — i.e. an actual
|
|
164
466
|
// shim re-export package, not a test-only directory like ast/test (whose only
|
|
165
467
|
// *.go are *_test.go and which is not part of the re-export surface).
|
|
@@ -230,36 +532,69 @@ func loadInner(anchorDir string) (map[string]*packages.Package, error) {
|
|
|
230
532
|
if err != nil {
|
|
231
533
|
return nil, err
|
|
232
534
|
}
|
|
233
|
-
|
|
234
|
-
errored := map[string]string{}
|
|
235
|
-
for _, p := range loaded {
|
|
236
|
-
suffix := strings.TrimPrefix(p.PkgPath, internalPrefix)
|
|
237
|
-
if len(p.Errors) > 0 {
|
|
238
|
-
errored[suffix] = p.Errors[0].Error()
|
|
239
|
-
continue
|
|
240
|
-
}
|
|
241
|
-
out[suffix] = p
|
|
242
|
-
}
|
|
535
|
+
indexed, errored := indexInternalPackages(loaded)
|
|
243
536
|
// A package that fails to load (or is missing entirely) must FAIL the audit,
|
|
244
537
|
// never be silently skipped — otherwise a load error in any environment turns
|
|
245
538
|
// the gate into a no-op that passes blind.
|
|
246
|
-
|
|
539
|
+
failures := map[string]string{}
|
|
247
540
|
for suffix := range expected {
|
|
248
|
-
if _, ok :=
|
|
541
|
+
if _, ok := indexed[suffix]; ok {
|
|
249
542
|
continue
|
|
250
543
|
}
|
|
251
|
-
|
|
252
|
-
if
|
|
253
|
-
|
|
544
|
+
failures[suffix] = errored[suffix]
|
|
545
|
+
if failures[suffix] == "" {
|
|
546
|
+
failures[suffix] = "did not load"
|
|
254
547
|
}
|
|
255
|
-
failed = append(failed, suffix+": "+msg)
|
|
256
548
|
}
|
|
257
|
-
if len(
|
|
549
|
+
if len(failures) > 0 {
|
|
550
|
+
failed := make([]string, 0, len(failures))
|
|
551
|
+
for suffix, message := range failures {
|
|
552
|
+
failed = append(failed, suffix+": "+message)
|
|
553
|
+
}
|
|
258
554
|
sort.Strings(failed)
|
|
259
|
-
return nil, fmt.Errorf("%d
|
|
555
|
+
return nil, fmt.Errorf("%d internal package(s) failed to load (audit would be incomplete):\n %s",
|
|
260
556
|
len(failed), strings.Join(failed, "\n "))
|
|
261
557
|
}
|
|
262
|
-
|
|
558
|
+
roots := map[string]*packages.Package{}
|
|
559
|
+
for suffix := range expected {
|
|
560
|
+
roots[suffix] = indexed[suffix]
|
|
561
|
+
}
|
|
562
|
+
return roots, nil
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// indexInternalPackages includes dependency packages because public contracts
|
|
566
|
+
// in a registered shim package can name callback/container types declared in an
|
|
567
|
+
// unregistered internal dependency. Dropping such a package would make the AST
|
|
568
|
+
// producer scan silently stop at the selector.
|
|
569
|
+
func indexInternalPackages(roots []*packages.Package) (map[string]*packages.Package, map[string]string) {
|
|
570
|
+
indexed := map[string]*packages.Package{}
|
|
571
|
+
errored := map[string]string{}
|
|
572
|
+
visited := map[*packages.Package]bool{}
|
|
573
|
+
var visit func(*packages.Package)
|
|
574
|
+
visit = func(pkg *packages.Package) {
|
|
575
|
+
if pkg == nil || visited[pkg] {
|
|
576
|
+
return
|
|
577
|
+
}
|
|
578
|
+
visited[pkg] = true
|
|
579
|
+
if strings.HasPrefix(pkg.PkgPath, internalPrefix) {
|
|
580
|
+
suffix := strings.TrimPrefix(pkg.PkgPath, internalPrefix)
|
|
581
|
+
switch {
|
|
582
|
+
case len(pkg.Errors) > 0:
|
|
583
|
+
errored[suffix] = pkg.Errors[0].Error()
|
|
584
|
+
case pkg.Types == nil:
|
|
585
|
+
errored[suffix] = "type information did not load"
|
|
586
|
+
default:
|
|
587
|
+
indexed[suffix] = pkg
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
for _, imported := range pkg.Imports {
|
|
591
|
+
visit(imported)
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
for _, root := range roots {
|
|
595
|
+
visit(root)
|
|
596
|
+
}
|
|
597
|
+
return indexed, errored
|
|
263
598
|
}
|
|
264
599
|
|
|
265
600
|
// namedInfo returns the defining package suffix and name of a named OR
|
|
@@ -416,7 +751,7 @@ func attachUntypedConsts(enumNames []string, typedMembers map[string][]string, u
|
|
|
416
751
|
|
|
417
752
|
// Finding kinds.
|
|
418
753
|
type finding struct {
|
|
419
|
-
kind string // ENUM / FUNC / ESCAPE
|
|
754
|
+
kind string // ENUM / FUNC / ESCAPE / PRODUCER
|
|
420
755
|
pkg string
|
|
421
756
|
symbol string
|
|
422
757
|
detail string
|
|
@@ -449,8 +784,20 @@ func main() {
|
|
|
449
784
|
fmt.Fprintln(os.Stderr, "shim_audit:", err)
|
|
450
785
|
os.Exit(1)
|
|
451
786
|
}
|
|
787
|
+
rootPackages := make([]*packages.Package, 0, len(inner))
|
|
788
|
+
for _, pkg := range inner {
|
|
789
|
+
rootPackages = append(rootPackages, pkg)
|
|
790
|
+
}
|
|
791
|
+
typeGraph, _ := indexInternalPackages(rootPackages)
|
|
792
|
+
producerSurface, err := scanShimProducerSurface(*shimRoot, typeGraph)
|
|
793
|
+
if err != nil {
|
|
794
|
+
fmt.Fprintln(os.Stderr, "shim_audit:", err)
|
|
795
|
+
os.Exit(1)
|
|
796
|
+
}
|
|
452
797
|
|
|
453
798
|
findings, pool := analyze(r, inner)
|
|
799
|
+
addExposedMethodFlow(r, inner, &producerSurface)
|
|
800
|
+
producerSurface = canonicalizeProducerSurface(producerSurface, typeGraph)
|
|
454
801
|
|
|
455
802
|
switch {
|
|
456
803
|
case *fix:
|
|
@@ -458,13 +805,314 @@ func main() {
|
|
|
458
805
|
case *writeBaseline:
|
|
459
806
|
runWriteBaseline(findings, *baselinePath)
|
|
460
807
|
case *check:
|
|
461
|
-
runCheck(findings, *baselinePath)
|
|
808
|
+
runCheck(findings, producerSurface, *baselinePath)
|
|
462
809
|
default:
|
|
463
|
-
|
|
810
|
+
producerEvaluation := evaluateProducerSurface(producerSurface, nil)
|
|
811
|
+
report(dedupe(append(findings, producerEvaluation.gaps...)), pool, *md)
|
|
464
812
|
}
|
|
465
813
|
}
|
|
466
814
|
|
|
467
|
-
|
|
815
|
+
type producerEvaluation struct {
|
|
816
|
+
gaps []finding
|
|
817
|
+
usedRoots map[string]bool
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// evaluateProducerSurface computes the obtainable compiler-object set to a
|
|
821
|
+
// fixed point. Package operations are exposed immediately and methods after
|
|
822
|
+
// their receiver is obtainable, but either unlocks results only after every
|
|
823
|
+
// compiler-object input is obtainable. Explicit baseline roots participate in
|
|
824
|
+
// the same graph without exempting downstream objects.
|
|
825
|
+
func evaluateProducerSurface(surface producerSurface, roots map[string]string) producerEvaluation {
|
|
826
|
+
reachable := map[flowType]bool{}
|
|
827
|
+
for typ := range surface.produced {
|
|
828
|
+
reachable[typ] = true
|
|
829
|
+
}
|
|
830
|
+
allTypes := map[flowType]bool{}
|
|
831
|
+
for typ := range surface.consumed {
|
|
832
|
+
allTypes[typ] = true
|
|
833
|
+
}
|
|
834
|
+
for typ := range surface.produced {
|
|
835
|
+
allTypes[typ] = true
|
|
836
|
+
}
|
|
837
|
+
for _, operation := range surface.operations {
|
|
838
|
+
if operation.receiver != nil {
|
|
839
|
+
allTypes[*operation.receiver] = true
|
|
840
|
+
}
|
|
841
|
+
for typ := range operation.consumed {
|
|
842
|
+
allTypes[typ] = true
|
|
843
|
+
}
|
|
844
|
+
for typ := range operation.produced {
|
|
845
|
+
allTypes[typ] = true
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
usedRoots := map[string]bool{}
|
|
849
|
+
for key, rationale := range roots {
|
|
850
|
+
if strings.TrimSpace(rationale) == "" {
|
|
851
|
+
continue
|
|
852
|
+
}
|
|
853
|
+
typ, ok := parseProducerExemptionKey(key)
|
|
854
|
+
if !ok || !allTypes[typ] || reachable[typ] {
|
|
855
|
+
continue
|
|
856
|
+
}
|
|
857
|
+
reachable[typ] = true
|
|
858
|
+
usedRoots[key] = true
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
consumed := map[flowType]map[string]bool{}
|
|
862
|
+
mergeFlows := func(target map[flowType]map[string]bool, flows map[flowType]map[string]bool) {
|
|
863
|
+
for typ, operations := range flows {
|
|
864
|
+
if target[typ] == nil {
|
|
865
|
+
target[typ] = map[string]bool{}
|
|
866
|
+
}
|
|
867
|
+
for operation := range operations {
|
|
868
|
+
target[typ][operation] = true
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
mergeFlows(consumed, surface.consumed)
|
|
873
|
+
exposed := make([]bool, len(surface.operations))
|
|
874
|
+
callable := make([]bool, len(surface.operations))
|
|
875
|
+
for changed := true; changed; {
|
|
876
|
+
changed = false
|
|
877
|
+
for index, operation := range surface.operations {
|
|
878
|
+
if !exposed[index] {
|
|
879
|
+
if operation.receiver != nil && !reachable[*operation.receiver] {
|
|
880
|
+
continue
|
|
881
|
+
}
|
|
882
|
+
exposed[index] = true
|
|
883
|
+
changed = true
|
|
884
|
+
mergeFlows(consumed, operation.consumed)
|
|
885
|
+
}
|
|
886
|
+
if callable[index] {
|
|
887
|
+
continue
|
|
888
|
+
}
|
|
889
|
+
requirementsMet := true
|
|
890
|
+
for typ := range operation.consumed {
|
|
891
|
+
if !reachable[typ] {
|
|
892
|
+
requirementsMet = false
|
|
893
|
+
break
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if !requirementsMet {
|
|
897
|
+
continue
|
|
898
|
+
}
|
|
899
|
+
callable[index] = true
|
|
900
|
+
changed = true
|
|
901
|
+
for typ := range operation.produced {
|
|
902
|
+
if !reachable[typ] {
|
|
903
|
+
reachable[typ] = true
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
var findings []finding
|
|
910
|
+
for typ, operations := range consumed {
|
|
911
|
+
if reachable[typ] {
|
|
912
|
+
continue
|
|
913
|
+
}
|
|
914
|
+
names := make([]string, 0, len(operations))
|
|
915
|
+
for operation := range operations {
|
|
916
|
+
names = append(names, operation)
|
|
917
|
+
}
|
|
918
|
+
sort.Strings(names)
|
|
919
|
+
findings = append(findings, finding{
|
|
920
|
+
kind: "PRODUCER",
|
|
921
|
+
pkg: typ.pkg,
|
|
922
|
+
symbol: typ.name,
|
|
923
|
+
detail: "consumed by " + strings.Join(names, ", ") + " but no reachable public shim operation produces it",
|
|
924
|
+
})
|
|
925
|
+
}
|
|
926
|
+
return producerEvaluation{gaps: dedupe(findings), usedRoots: usedRoots}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
func parseProducerExemptionKey(key string) (flowType, bool) {
|
|
930
|
+
separator := strings.LastIndexByte(key, '.')
|
|
931
|
+
if separator <= 0 || separator == len(key)-1 {
|
|
932
|
+
return flowType{}, false
|
|
933
|
+
}
|
|
934
|
+
return flowType{pkg: key[:separator], name: key[separator+1:]}, true
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
func canonicalizeProducerSurface(surface producerSurface, inner map[string]*packages.Package) producerSurface {
|
|
938
|
+
canonical := newProducerSurface()
|
|
939
|
+
copyDirection := func(direction flowDirection, flows map[flowType]map[string]bool) {
|
|
940
|
+
for typ, operations := range flows {
|
|
941
|
+
typ = canonicalFlowType(typ, inner)
|
|
942
|
+
for operation := range operations {
|
|
943
|
+
canonical.add(direction, typ, operation)
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
copyDirection(flowConsume, surface.consumed)
|
|
948
|
+
copyDirection(flowProduce, surface.produced)
|
|
949
|
+
for _, operation := range surface.operations {
|
|
950
|
+
converted := operationFlow{
|
|
951
|
+
consumed: map[flowType]map[string]bool{},
|
|
952
|
+
produced: map[flowType]map[string]bool{},
|
|
953
|
+
}
|
|
954
|
+
if operation.receiver != nil {
|
|
955
|
+
receiver := canonicalFlowType(*operation.receiver, inner)
|
|
956
|
+
converted.receiver = &receiver
|
|
957
|
+
}
|
|
958
|
+
copyOperationDirection := func(target map[flowType]map[string]bool, flows map[flowType]map[string]bool) {
|
|
959
|
+
for typ, operations := range flows {
|
|
960
|
+
typ = canonicalFlowType(typ, inner)
|
|
961
|
+
if target[typ] == nil {
|
|
962
|
+
target[typ] = map[string]bool{}
|
|
963
|
+
}
|
|
964
|
+
for operation := range operations {
|
|
965
|
+
target[typ][operation] = true
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
copyOperationDirection(converted.consumed, operation.consumed)
|
|
970
|
+
copyOperationDirection(converted.produced, operation.produced)
|
|
971
|
+
canonical.operations = append(canonical.operations, converted)
|
|
972
|
+
}
|
|
973
|
+
return canonical
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
func canonicalFlowType(typ flowType, inner map[string]*packages.Package) flowType {
|
|
977
|
+
pkg := inner[typ.pkg]
|
|
978
|
+
if pkg == nil || pkg.Types == nil {
|
|
979
|
+
return typ
|
|
980
|
+
}
|
|
981
|
+
named, ok := pkg.Types.Scope().Lookup(typ.name).(*types.TypeName)
|
|
982
|
+
if !ok {
|
|
983
|
+
return typ
|
|
984
|
+
}
|
|
985
|
+
if pkgSuffix, name, ok := namedInfo(types.Unalias(named.Type())); ok {
|
|
986
|
+
return flowType{pkg: pkgSuffix, name: name}
|
|
987
|
+
}
|
|
988
|
+
return typ
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// addExposedMethodFlow covers the method set published automatically by each
|
|
992
|
+
// exposed type alias. Those methods do not appear as declarations in shim
|
|
993
|
+
// source, but their receiver, parameter, callback, and result flows are part of
|
|
994
|
+
// the same public surface as hand-written package wrappers.
|
|
995
|
+
func addExposedMethodFlow(r reachable, inner map[string]*packages.Package, surface *producerSurface) {
|
|
996
|
+
for suffix, pkg := range inner {
|
|
997
|
+
scope := pkg.Types.Scope()
|
|
998
|
+
for _, name := range scope.Names() {
|
|
999
|
+
typeName, ok := scope.Lookup(name).(*types.TypeName)
|
|
1000
|
+
if !ok || !r.has(suffix, name) {
|
|
1001
|
+
continue
|
|
1002
|
+
}
|
|
1003
|
+
exposed := types.Unalias(typeName.Type())
|
|
1004
|
+
named, ok := exposed.(*types.Named)
|
|
1005
|
+
if !ok {
|
|
1006
|
+
continue
|
|
1007
|
+
}
|
|
1008
|
+
receiver := types.Type(named)
|
|
1009
|
+
if _, isInterface := named.Underlying().(*types.Interface); !isInterface {
|
|
1010
|
+
receiver = types.NewPointer(named)
|
|
1011
|
+
}
|
|
1012
|
+
methods := types.NewMethodSet(receiver)
|
|
1013
|
+
for i := 0; i < methods.Len(); i++ {
|
|
1014
|
+
method, ok := methods.At(i).Obj().(*types.Func)
|
|
1015
|
+
if !ok || !method.Exported() {
|
|
1016
|
+
continue
|
|
1017
|
+
}
|
|
1018
|
+
signature := method.Signature()
|
|
1019
|
+
operation := suffix + "." + name + "." + method.Name()
|
|
1020
|
+
methodSurface := newProducerSurface()
|
|
1021
|
+
collectGoTupleFlow(signature.Params(), flowConsume, operation, methodSurface)
|
|
1022
|
+
collectGoTupleFlow(signature.Results(), flowProduce, operation, methodSurface)
|
|
1023
|
+
if len(methodSurface.consumed) == 0 && len(methodSurface.produced) == 0 {
|
|
1024
|
+
continue
|
|
1025
|
+
}
|
|
1026
|
+
// Key the dependency to the exposed outer type, not the declaring
|
|
1027
|
+
// receiver of a promoted method: callers need an outer value to invoke
|
|
1028
|
+
// the selected method and never manufacture an embedded implementation
|
|
1029
|
+
// receiver directly.
|
|
1030
|
+
receiverType := flowType{pkg: suffix, name: name}
|
|
1031
|
+
surface.operations = append(surface.operations, operationFlow{
|
|
1032
|
+
receiver: &receiverType,
|
|
1033
|
+
consumed: methodSurface.consumed,
|
|
1034
|
+
produced: methodSurface.produced,
|
|
1035
|
+
})
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
func collectGoTupleFlow(tuple *types.Tuple, direction flowDirection, operation string, surface producerSurface) {
|
|
1042
|
+
collectGoTupleFlowSeen(tuple, direction, operation, surface, map[flowVisit]bool{})
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
type flowVisit struct {
|
|
1046
|
+
typ types.Type
|
|
1047
|
+
direction flowDirection
|
|
1048
|
+
pointerLike bool
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
func collectGoTupleFlowSeen(tuple *types.Tuple, direction flowDirection, operation string, surface producerSurface, seen map[flowVisit]bool) {
|
|
1052
|
+
if tuple == nil {
|
|
1053
|
+
return
|
|
1054
|
+
}
|
|
1055
|
+
for i := 0; i < tuple.Len(); i++ {
|
|
1056
|
+
collectGoTypeFlowSeen(tuple.At(i).Type(), direction, false, operation, surface, seen)
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
func collectGoTypeFlow(typ types.Type, direction flowDirection, pointerLike bool, operation string, surface producerSurface) {
|
|
1061
|
+
collectGoTypeFlowSeen(typ, direction, pointerLike, operation, surface, map[flowVisit]bool{})
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
func collectGoTypeFlowSeen(typ types.Type, direction flowDirection, pointerLike bool, operation string, surface producerSurface, seen map[flowVisit]bool) {
|
|
1065
|
+
visit := flowVisit{typ: typ, direction: direction, pointerLike: pointerLike}
|
|
1066
|
+
if seen[visit] {
|
|
1067
|
+
return
|
|
1068
|
+
}
|
|
1069
|
+
seen[visit] = true
|
|
1070
|
+
switch current := typ.(type) {
|
|
1071
|
+
case *types.Alias:
|
|
1072
|
+
collectGoTypeFlowSeen(types.Unalias(current), direction, pointerLike, operation, surface, seen)
|
|
1073
|
+
case *types.Pointer:
|
|
1074
|
+
collectGoTypeFlowSeen(current.Elem(), direction, true, operation, surface, seen)
|
|
1075
|
+
case *types.Slice:
|
|
1076
|
+
collectGoTypeFlowSeen(current.Elem(), direction, false, operation, surface, seen)
|
|
1077
|
+
case *types.Array:
|
|
1078
|
+
collectGoTypeFlowSeen(current.Elem(), direction, false, operation, surface, seen)
|
|
1079
|
+
case *types.Map:
|
|
1080
|
+
collectGoTypeFlowSeen(current.Key(), direction, false, operation, surface, seen)
|
|
1081
|
+
collectGoTypeFlowSeen(current.Elem(), direction, false, operation, surface, seen)
|
|
1082
|
+
case *types.Chan:
|
|
1083
|
+
collectGoTypeFlowSeen(current.Elem(), direction, false, operation, surface, seen)
|
|
1084
|
+
case *types.Signature:
|
|
1085
|
+
collectGoTupleFlowSeen(current.Params(), opposite(direction), operation, surface, seen)
|
|
1086
|
+
collectGoTupleFlowSeen(current.Results(), direction, operation, surface, seen)
|
|
1087
|
+
case *types.Interface:
|
|
1088
|
+
current.Complete()
|
|
1089
|
+
for i := 0; i < current.NumMethods(); i++ {
|
|
1090
|
+
signature, ok := current.Method(i).Type().(*types.Signature)
|
|
1091
|
+
if !ok {
|
|
1092
|
+
continue
|
|
1093
|
+
}
|
|
1094
|
+
collectGoTupleFlowSeen(signature.Params(), opposite(direction), operation, surface, seen)
|
|
1095
|
+
collectGoTupleFlowSeen(signature.Results(), direction, operation, surface, seen)
|
|
1096
|
+
}
|
|
1097
|
+
case *types.Named:
|
|
1098
|
+
if pointerLike {
|
|
1099
|
+
if pkg, name, ok := namedInfo(current); ok {
|
|
1100
|
+
surface.add(direction, flowType{pkg: pkg, name: name}, operation)
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if args := current.TypeArgs(); args != nil {
|
|
1104
|
+
for i := 0; i < args.Len(); i++ {
|
|
1105
|
+
collectGoTypeFlowSeen(args.At(i), direction, false, operation, surface, seen)
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
switch underlying := current.Underlying().(type) {
|
|
1109
|
+
case *types.Interface, *types.Signature, *types.Slice, *types.Array, *types.Map, *types.Chan, *types.Pointer:
|
|
1110
|
+
collectGoTypeFlowSeen(underlying, direction, false, operation, surface, seen)
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// analyze runs the upstream closure checks plus the unexported demand-pool scan and
|
|
468
1116
|
// returns the deduped findings and pool.
|
|
469
1117
|
func analyze(r reachable, inner map[string]*packages.Package) (findings, unexportedPool []finding) {
|
|
470
1118
|
for suffix, pkg := range inner {
|
|
@@ -731,7 +1379,7 @@ func tierOf(kind string) int {
|
|
|
731
1379
|
switch kind {
|
|
732
1380
|
case "ENUM": // partial enum — some members exposed, siblings missing
|
|
733
1381
|
return 1
|
|
734
|
-
case "FUNC": //
|
|
1382
|
+
case "FUNC", "PRODUCER": // callable ops and constructible object flow
|
|
735
1383
|
return 2
|
|
736
1384
|
case "ESCAPE": // type returned by an exposed op but not aliased
|
|
737
1385
|
return 3
|
|
@@ -771,7 +1419,7 @@ func report(findings, pool []finding, md bool) {
|
|
|
771
1419
|
} else {
|
|
772
1420
|
fmt.Printf("=== Shim closure audit ===\n")
|
|
773
1421
|
}
|
|
774
|
-
fmt.Printf("Tier 1 PARTIAL-ENUM (near-certain bugs): %d | Tier 2 FUNC
|
|
1422
|
+
fmt.Printf("Tier 1 PARTIAL-ENUM (near-certain bugs): %d | Tier 2 FUNC/PRODUCER: %d | Tier 3 ESCAPE: %d | Tier 4 type-only enums: %d | Unexported demand pool: %d\n",
|
|
775
1423
|
len(tiers[1]), len(tiers[2]), len(tiers[3]), len(tiers[4]), len(pool))
|
|
776
1424
|
|
|
777
1425
|
h("TIER 1 — partial enums (a sibling const is missing; this is the #230 class)")
|
|
@@ -780,7 +1428,7 @@ func report(findings, pool []finding, md bool) {
|
|
|
780
1428
|
row(f)
|
|
781
1429
|
}
|
|
782
1430
|
|
|
783
|
-
h("TIER 2 —
|
|
1431
|
+
h("TIER 2 — reachable funcs and consumed compiler objects without producers")
|
|
784
1432
|
thead()
|
|
785
1433
|
for _, f := range tiers[2] {
|
|
786
1434
|
row(f)
|
|
@@ -825,10 +1473,60 @@ func countByPkg(in []finding) string {
|
|
|
825
1473
|
func findingKey(f finding) string { return f.kind + "|" + f.pkg + "|" + f.symbol }
|
|
826
1474
|
|
|
827
1475
|
// baselineFile is the on-disk schema of accepted TIER-2/3 gaps. TIER-1 enum
|
|
828
|
-
// gaps are never baselined
|
|
1476
|
+
// gaps are never baselined. Producer gaps require a separate, reasoned root or
|
|
1477
|
+
// ownership-boundary exemption because accepting an ordinary symbol gap must
|
|
1478
|
+
// not waive object constructibility.
|
|
829
1479
|
type baselineFile struct {
|
|
830
|
-
Note
|
|
831
|
-
Accepted
|
|
1480
|
+
Note string `json:"note"`
|
|
1481
|
+
Accepted []string `json:"accepted"`
|
|
1482
|
+
ProducerExemptions map[string]string `json:"producer_exemptions,omitempty"`
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
type baselineEvaluation struct {
|
|
1486
|
+
enumGaps []finding
|
|
1487
|
+
newGaps []finding
|
|
1488
|
+
producerGaps []finding
|
|
1489
|
+
invalidReasons []string
|
|
1490
|
+
staleAccepted int
|
|
1491
|
+
staleProducers int
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
func evaluateBaseline(findings []finding, baseline baselineFile, usedProducerRoots map[string]bool) baselineEvaluation {
|
|
1495
|
+
accepted := map[string]bool{}
|
|
1496
|
+
for _, key := range baseline.Accepted {
|
|
1497
|
+
accepted[key] = true
|
|
1498
|
+
}
|
|
1499
|
+
evaluation := baselineEvaluation{}
|
|
1500
|
+
liveAccepted := map[string]bool{}
|
|
1501
|
+
for _, f := range findings {
|
|
1502
|
+
switch f.kind {
|
|
1503
|
+
case "ENUM":
|
|
1504
|
+
evaluation.enumGaps = append(evaluation.enumGaps, f)
|
|
1505
|
+
case "FUNC", "ESCAPE":
|
|
1506
|
+
key := findingKey(f)
|
|
1507
|
+
liveAccepted[key] = true
|
|
1508
|
+
if !accepted[key] {
|
|
1509
|
+
evaluation.newGaps = append(evaluation.newGaps, f)
|
|
1510
|
+
}
|
|
1511
|
+
case "PRODUCER":
|
|
1512
|
+
evaluation.producerGaps = append(evaluation.producerGaps, f)
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
for key := range accepted {
|
|
1516
|
+
if !liveAccepted[key] {
|
|
1517
|
+
evaluation.staleAccepted++
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
for key, rationale := range baseline.ProducerExemptions {
|
|
1521
|
+
if strings.TrimSpace(rationale) == "" {
|
|
1522
|
+
evaluation.invalidReasons = append(evaluation.invalidReasons, key)
|
|
1523
|
+
}
|
|
1524
|
+
if !usedProducerRoots[key] {
|
|
1525
|
+
evaluation.staleProducers++
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
sort.Strings(evaluation.invalidReasons)
|
|
1529
|
+
return evaluation
|
|
832
1530
|
}
|
|
833
1531
|
|
|
834
1532
|
// shimPackageName reads the `package` clause from a .go file in dir.
|
|
@@ -906,7 +1604,8 @@ func runFix(findings []finding, shimRoot string) {
|
|
|
906
1604
|
}
|
|
907
1605
|
|
|
908
1606
|
// runWriteBaseline records the current TIER-2/3 gaps as accepted, so the gate
|
|
909
|
-
// fails only on NEW ones.
|
|
1607
|
+
// fails only on NEW ones. Producer exemptions are preserved rather than
|
|
1608
|
+
// inferred: each root needs an explicit human rationale.
|
|
910
1609
|
func runWriteBaseline(findings []finding, path string) {
|
|
911
1610
|
var keys []string
|
|
912
1611
|
for _, f := range findings {
|
|
@@ -915,9 +1614,19 @@ func runWriteBaseline(findings []finding, path string) {
|
|
|
915
1614
|
}
|
|
916
1615
|
}
|
|
917
1616
|
sort.Strings(keys)
|
|
1617
|
+
producerExemptions := map[string]string{}
|
|
1618
|
+
if existing, err := os.ReadFile(path); err == nil {
|
|
1619
|
+
var baseline baselineFile
|
|
1620
|
+
if err := json.Unmarshal(existing, &baseline); err != nil {
|
|
1621
|
+
fmt.Fprintf(os.Stderr, "shim_audit: parse %s: %v\n", path, err)
|
|
1622
|
+
os.Exit(2)
|
|
1623
|
+
}
|
|
1624
|
+
producerExemptions = baseline.ProducerExemptions
|
|
1625
|
+
}
|
|
918
1626
|
data, err := json.MarshalIndent(baselineFile{
|
|
919
|
-
Note:
|
|
920
|
-
Accepted:
|
|
1627
|
+
Note: "Accepted TIER-2 (reachable funcs) and TIER-3 (escaping types) shim gaps. The gate fails on any gap NOT listed here; expose a symbol and remove its line, or run -write-baseline to accept new ones deliberately. TIER-1 enum and PRODUCER gaps are zero-tolerance; producer_exemptions must name genuine roots or ownership boundaries with non-empty rationales.",
|
|
1628
|
+
Accepted: keys,
|
|
1629
|
+
ProducerExemptions: producerExemptions,
|
|
921
1630
|
}, "", " ")
|
|
922
1631
|
if err != nil {
|
|
923
1632
|
fmt.Fprintln(os.Stderr, "shim_audit:", err)
|
|
@@ -930,66 +1639,63 @@ func runWriteBaseline(findings []finding, path string) {
|
|
|
930
1639
|
fmt.Printf("shim_audit: wrote %s (%d accepted gaps)\n", path, len(keys))
|
|
931
1640
|
}
|
|
932
1641
|
|
|
933
|
-
// runCheck is the CI gate. It fails on any TIER-1 enum gap
|
|
934
|
-
// any TIER-2/3 gap not present in the baseline.
|
|
935
|
-
func runCheck(findings []finding, path string) {
|
|
936
|
-
|
|
1642
|
+
// runCheck is the CI gate. It fails on any TIER-1 enum gap, any unreasoned
|
|
1643
|
+
// producer gap, or any TIER-2/3 gap not present in the baseline.
|
|
1644
|
+
func runCheck(findings []finding, surface producerSurface, path string) {
|
|
1645
|
+
baseline := baselineFile{}
|
|
937
1646
|
if data, err := os.ReadFile(path); err == nil {
|
|
938
|
-
|
|
939
|
-
if err := json.Unmarshal(data, &bf); err != nil {
|
|
1647
|
+
if err := json.Unmarshal(data, &baseline); err != nil {
|
|
940
1648
|
fmt.Fprintf(os.Stderr, "shim_audit: parse %s: %v\n", path, err)
|
|
941
1649
|
os.Exit(2)
|
|
942
1650
|
}
|
|
943
|
-
for _, k := range bf.Accepted {
|
|
944
|
-
accepted[k] = true
|
|
945
|
-
}
|
|
946
1651
|
} else {
|
|
947
|
-
fmt.Fprintf(os.Stderr, "shim_audit: WARNING no baseline at %s; treating all func/type gaps as new\n", path)
|
|
1652
|
+
fmt.Fprintf(os.Stderr, "shim_audit: WARNING no baseline at %s; treating all func/type and producer gaps as new\n", path)
|
|
948
1653
|
}
|
|
949
1654
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
switch f.kind {
|
|
954
|
-
case "ENUM":
|
|
955
|
-
enumGaps = append(enumGaps, f)
|
|
956
|
-
case "FUNC", "ESCAPE":
|
|
957
|
-
live[findingKey(f)] = true
|
|
958
|
-
if !accepted[findingKey(f)] {
|
|
959
|
-
newGaps = append(newGaps, f)
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
}
|
|
1655
|
+
producerEvaluation := evaluateProducerSurface(surface, baseline.ProducerExemptions)
|
|
1656
|
+
findings = dedupe(append(findings, producerEvaluation.gaps...))
|
|
1657
|
+
evaluation := evaluateBaseline(findings, baseline, producerEvaluation.usedRoots)
|
|
963
1658
|
// Non-failing hygiene note: baseline lines that no longer match a gap (the
|
|
964
1659
|
// symbol got exposed) are dead weight; the ratchet stays safe but suggest a prune.
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
if !live[k] {
|
|
968
|
-
stale++
|
|
969
|
-
}
|
|
1660
|
+
if evaluation.staleAccepted > 0 {
|
|
1661
|
+
fmt.Fprintf(os.Stderr, "shim_audit: note: %d baseline entr(y/ies) no longer match a gap; prune with -write-baseline\n", evaluation.staleAccepted)
|
|
970
1662
|
}
|
|
971
|
-
if
|
|
972
|
-
fmt.Fprintf(os.Stderr, "shim_audit: note: %d
|
|
1663
|
+
if evaluation.staleProducers > 0 {
|
|
1664
|
+
fmt.Fprintf(os.Stderr, "shim_audit: note: %d producer exemption(s) no longer match a gap; remove them\n", evaluation.staleProducers)
|
|
973
1665
|
}
|
|
974
1666
|
|
|
975
|
-
if len(enumGaps) == 0 && len(newGaps) == 0
|
|
976
|
-
|
|
1667
|
+
if len(evaluation.enumGaps) == 0 && len(evaluation.newGaps) == 0 &&
|
|
1668
|
+
len(evaluation.producerGaps) == 0 && len(evaluation.invalidReasons) == 0 {
|
|
1669
|
+
fmt.Println("shim_audit: OK — exposed enums, reachable symbols, and compiler-object producers are closed")
|
|
977
1670
|
return
|
|
978
1671
|
}
|
|
979
|
-
if len(enumGaps) > 0 {
|
|
980
|
-
fmt.Fprintf(os.Stderr, "\nshim_audit: FAIL — %d enum-family member(s) of an EXPOSED enum are not re-exported.\n", len(enumGaps))
|
|
1672
|
+
if len(evaluation.enumGaps) > 0 {
|
|
1673
|
+
fmt.Fprintf(os.Stderr, "\nshim_audit: FAIL — %d enum-family member(s) of an EXPOSED enum are not re-exported.\n", len(evaluation.enumGaps))
|
|
981
1674
|
fmt.Fprintf(os.Stderr, " This is the #230 class. Fix mechanically: `pnpm --filter ttsc shim:audit -fix`.\n")
|
|
982
|
-
for _, f := range enumGaps {
|
|
1675
|
+
for _, f := range evaluation.enumGaps {
|
|
983
1676
|
fmt.Fprintf(os.Stderr, " ENUM %s.%s\n", f.pkg, f.symbol)
|
|
984
1677
|
}
|
|
985
1678
|
}
|
|
986
|
-
if len(newGaps) > 0 {
|
|
987
|
-
fmt.Fprintf(os.Stderr, "\nshim_audit: FAIL — %d new reachable gap(s) not in the baseline.\n", len(newGaps))
|
|
1679
|
+
if len(evaluation.newGaps) > 0 {
|
|
1680
|
+
fmt.Fprintf(os.Stderr, "\nshim_audit: FAIL — %d new reachable gap(s) not in the baseline.\n", len(evaluation.newGaps))
|
|
988
1681
|
fmt.Fprintf(os.Stderr, " Expose them in the shim, or accept deliberately: `pnpm --filter ttsc shim:audit -write-baseline`.\n")
|
|
989
|
-
for _, f := range newGaps {
|
|
1682
|
+
for _, f := range evaluation.newGaps {
|
|
990
1683
|
fmt.Fprintf(os.Stderr, " %-6s %s.%s — %s\n", f.kind, f.pkg, f.symbol, f.detail)
|
|
991
1684
|
}
|
|
992
1685
|
}
|
|
1686
|
+
if len(evaluation.producerGaps) > 0 {
|
|
1687
|
+
fmt.Fprintf(os.Stderr, "\nshim_audit: FAIL — %d consumed compiler object(s) have no public producer.\n", len(evaluation.producerGaps))
|
|
1688
|
+
fmt.Fprintf(os.Stderr, " Add a general exported producer, or document a genuine root or ownership boundary in producer_exemptions.\n")
|
|
1689
|
+
for _, f := range evaluation.producerGaps {
|
|
1690
|
+
fmt.Fprintf(os.Stderr, " PRODUCER %s.%s — %s\n", f.pkg, f.symbol, f.detail)
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
if len(evaluation.invalidReasons) > 0 {
|
|
1694
|
+
fmt.Fprintf(os.Stderr, "\nshim_audit: FAIL — %d producer exemption(s) have an empty rationale.\n", len(evaluation.invalidReasons))
|
|
1695
|
+
for _, key := range evaluation.invalidReasons {
|
|
1696
|
+
fmt.Fprintf(os.Stderr, " PRODUCER-EXEMPTION %s\n", key)
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
993
1699
|
fmt.Fprintln(os.Stderr)
|
|
994
1700
|
os.Exit(1)
|
|
995
1701
|
}
|