{"record":{"id":"fc25afa5cfc29d03","repo":"charmbracelet/vhs","slug":"nested-source-detected","errorCode":null,"errorMessage":"Nested Source detected","messagePattern":"Nested Source detected","errorType":"validation","errorClass":"parser.Error","httpStatus":null,"severity":"error","filePath":"parser/parser.go","lineNumber":724,"sourceCode":"\t}\n\n\tsrcTape := string(d)\n\t// Check source tape is NOT empty\n\tif len(srcTape) == 0 {\n\t\treadErr := fmt.Sprintf(\"Source tape: %s is empty\", srcPath)\n\t\tp.errors = append(p.errors, NewError(p.peek, readErr))\n\t\tp.nextToken()\n\t\treturn []Command{cmd}\n\t}\n\n\tsrcLexer := lexer.New(srcTape)\n\tsrcParser := New(srcLexer)\n\n\t// Check not nested source\n\tsrcCmds := srcParser.Parse()\n\tfor _, cmd := range srcCmds {\n\t\tif cmd.Type == token.SOURCE {\n\t\t\tp.errors = append(p.errors, NewError(p.peek, \"Nested Source detected\"))\n\t\t\tp.nextToken()\n\t\t\treturn []Command{cmd}\n\t\t}\n\t}\n\n\t// Check src errors\n\tsrcErrors := srcParser.Errors()\n\tif len(srcErrors) > 0 {\n\t\tp.errors = append(p.errors, NewError(p.peek, fmt.Sprintf(\"%s has %d errors\", srcPath, len(srcErrors))))\n\t\tp.nextToken()\n\t\treturn []Command{cmd}\n\t}\n\n\tfiltered := make([]Command, 0)\n\tfor _, srcCmd := range srcCmds {\n\t\t// Output have to be avoid in order to not overwrite output of the original tape.\n\t\tif srcCmd.Type == token.SOURCE ||\n\t\t\tsrcCmd.Type == token.OUTPUT {","sourceCodeStart":706,"sourceCodeEnd":742,"githubUrl":"https://github.com/charmbracelet/vhs/blob/c073383b5de0b1f57bf514113029c306bc986539/parser/parser.go#L706-L742","documentation":"parseSource parses the included tape with a sub-parser and scans its commands; if any command is itself a SOURCE, it reports \"Nested Source detected\" and aborts. This library forbids source-within-source to keep include resolution one level deep and avoid recursion.","triggerScenarios":"Parse() on a tape whose Source target itself contains a `Source \"...\"` line — the sub-parser emits a SOURCE command and the outer parser flags nesting.","commonSituations":"Refactoring tapes into shared partials where a partial also sources a common base; trying to build an include tree; combining generated tapes that contain Source lines.","solutions":["Flatten the nested include: inline the grandchild tape's contents into the child tape","Remove the Source line from the included tape","Restructure so each top-level tape sources only leaf tapes with no Source commands"],"exampleFix":"// child.tape before\nSource \"base.tape\"\nType \"hi\"\n// after\n(base.tape commands pasted here)\nType \"hi\"","handlingStrategy":"validation","validationCode":"data, _ := os.ReadFile(\"child.tape\")\nfor i, line := range strings.Split(string(data), \"\\n\") {\n    if strings.HasPrefix(strings.TrimSpace(line), \"Source \") {\n        return fmt.Errorf(\"child.tape line %d contains nested Source; flatten it\", i+1)\n    }\n}","typeGuard":"func hasSourceCmd(cmds []Command) bool {\n    for _, c := range cmds {\n        if c.Type == token.SOURCE { return true }\n    }\n    return false\n}","tryCatchPattern":"if err := parse(tape); err != nil && strings.Contains(err.Error(), \"Nested Source\") {\n    return fmt.Errorf(\"flatten includes in %s: %w\", tape, err)\n}","preventionTips":["Keep include graphs one level deep: top-level tapes source only leaf tapes","Grep shared partials for Source before including them","Document that Source cannot be nested in your tape conventions"],"tags":["parser","source","recursion"],"backgroundTag":"nested-source-detected","analyzedSha":"c073383b5de0b1f57bf514113029c306bc986539","analyzedAt":"2026-09-02T01:53:05.682Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-09T06:17:21.866Z"}