{"record":{"id":"82309c1309c660e3","repo":"microsoft/typescript-go","slug":"expected-sourcefile-root-got-v","errorCode":null,"errorMessage":"expected SourceFile root, got %v","messagePattern":"expected SourceFile root, got (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/api/encoder/decoder.go","lineNumber":39,"sourceCode":"\tfactory   *ast.NodeFactory\n\tchildBuf  []int\n\t// Single Go string covering all string data; substrings are zero-alloc slices.\n\tallStringData string\n\t// Arena for batch-allocating []*ast.Node slices used by NodeLists.\n\tnodeArena []*ast.Node\n\t// Results\n\tnodes     []*ast.Node\n\tnodeLists []*ast.NodeList\n}\n\n// DecodeSourceFile decodes binary-encoded data into an *ast.SourceFile.\nfunc DecodeSourceFile(data []byte) (*ast.SourceFile, error) {\n\tnode, err := DecodeNodes(data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif node.Kind != ast.KindSourceFile {\n\t\treturn nil, fmt.Errorf(\"expected SourceFile root, got %v\", node.Kind)\n\t}\n\treturn node.AsSourceFile(), nil\n}\n\n// DecodeNodes decodes binary-encoded AST data into a tree of *ast.Node objects.\nfunc DecodeNodes(data []byte) (*ast.Node, error) {\n\td, err := newASTDecoder(data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn d.decode()\n}\n\nfunc newASTDecoder(data []byte) (*astDecoder, error) {\n\tif len(data) < HeaderSize {\n\t\treturn nil, fmt.Errorf(\"data too short for header: %d bytes\", len(data))\n\t}\n\tversion := data[HeaderOffsetMetadata+3]","sourceCodeStart":21,"sourceCodeEnd":57,"githubUrl":"https://github.com/microsoft/typescript-go/blob/1bcfa18d79a3be41772223d5c05dfe4480e614ff/internal/api/encoder/decoder.go#L21-L57","documentation":"DecodeSourceFile decoded the binary buffer into a node tree, but the root node's syntax kind is not ast.KindSourceFile. The decoder always returns node index 1 as the root, so the buffer encodes a different AST shape, or corruption altered the kind field while still passing header validation.","triggerScenarios":"Feeding DecodeSourceFile data produced by encoding a non-SourceFile root via EncodeNodes; a truncated or garbled buffer whose header fields happen to validate; two encoded blobs concatenated or sliced wrongly.","commonSituations":"Caching layers that store arbitrary encoded subtrees under keys meant for whole files; partial writes of encoded artifacts; reusing one buffer for multiple purposes.","solutions":["Confirm the blob was produced by encoding a whole SourceFile (same typescript-go version) and regenerate it","If you encode arbitrary nodes, call DecodeNodes and switch on node.Kind instead of DecodeSourceFile","Store an integrity hash next to cached blobs and verify before decoding","Check for truncation by comparing the stored blob length against the encoder's output length"],"exampleFix":"// before\nsf := encoder.DecodeSourceFile(data) // panics-ish on non-file roots\n\n// after\nn, err := encoder.DecodeNodes(data)\nif err != nil { return err }\nif n.Kind != ast.KindSourceFile { return fmt.Errorf(\"not a source file: %v\", n.Kind) }\nsf := n.AsSourceFile()","handlingStrategy":"validation","validationCode":"// Accept any encoded tree, then narrow to SourceFile yourself.\nn, err := encoder.DecodeNodes(data)\nif err != nil { return nil, err }\nif n.Kind != ast.KindSourceFile {\n\treturn nil, fmt.Errorf(\"not a source file blob: root kind %v\", n.Kind)\n}\nreturn n.AsSourceFile(), nil","typeGuard":"func isSourceFileBlobRoot(n *ast.Node) bool { return n != nil && n.Kind == ast.KindSourceFile }","tryCatchPattern":null,"preventionTips":["Only store whole-file encodes under source-file cache keys","Use DecodeNodes when handling arbitrary subtrees","Hash cached blobs and verify before decoding"],"tags":["go","ast","encoder","binary","decoding","root-kind"],"backgroundTag":null,"analyzedSha":"1bcfa18d79a3be41772223d5c05dfe4480e614ff","analyzedAt":"2026-08-16T02:12:00.115Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}