Files
pyscn/internal/parser/ast_builder.go
DaisukeYoda 01f3bfb764 fix: detect dead code after exhaustive if-elif-else chains
Fixes #117

## Problem
Dead code detection failed to identify unreachable code after exhaustive
if-elif-else chains where all branches terminate with return/raise/break/
continue statements.

## Root Cause
1. Parser wasn't properly handling elif_clause and else_clause nodes from
   tree-sitter, causing else clauses to be lost in elif chains
2. CFG builder always created fall-through edges to merge blocks even
   when all conditional branches terminated

## Solution

### Parser Fixes (ast_builder.go)
- Added buildElifClause() to properly parse elif nodes with Test/Body/Orelse
- Added buildElseClause() to parse else nodes with Body
- Modified buildIfStatement() to collect all alternative branches and chain
  them correctly via attachElseToElifChain()
- Tree-sitter returns both elif_clause and else_clause as "alternative"
  field siblings, now both are properly collected and linked

### CFG Builder Enhancements (cfg_builder.go)
- Added blockTerminates() to check if block ends with terminating statements
- Added allBranchesTerminate() to check if all conditional branches terminate
- Modified processIfStatement() to detect exhaustive termination and create
  unreachable blocks instead of connecting to merge
- Modified processIfStatementElif() with same termination detection
- Updated convertElifClauseToIf() to use parser-populated fields
- Both functions now handle else_clause nodes by extracting their Body

### Test Updates (dead_code_test.go)
- Updated UnreachableElif: expectedDead 0 → 1
- Added ExhaustiveIfElseReturn test case
- Added NestedExhaustiveReturn test case
- Added MixedTerminators test case (return/raise mix)
- Updated ComplexControlFlow: expectedDead 3 → 4 (more accurate)

## Testing
 All 18 dead code detection tests passing
 All analyzer tests passing (16.8s)
 All parser tests passing (0.26s)

## Pattern
Follows existing unreachable block creation pattern used for return/break/
continue statements (lines 323-324, 738-740, 759-761 in cfg_builder.go).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 09:29:37 +09:00

1678 lines
44 KiB
Go

package parser
import (
"fmt"
"strconv"
"strings"
sitter "github.com/smacker/go-tree-sitter"
)
// ASTBuilder converts tree-sitter parse trees to internal AST representation
type ASTBuilder struct {
source []byte
}
// NewASTBuilder creates a new AST builder
func NewASTBuilder(source []byte) *ASTBuilder {
return &ASTBuilder{
source: source,
}
}
// Helper to extract body from block and set parent references
func (b *ASTBuilder) extractBlockBody(blockNode *Node, parent *Node) []*Node {
if blockNode == nil {
return nil
}
if blockNode.Type == "block" {
// Update parent references when extracting from block
for _, stmt := range blockNode.Body {
if stmt != nil {
stmt.Parent = parent
}
}
return blockNode.Body
}
// Not a block, return as single element
blockNode.Parent = parent
return []*Node{blockNode}
}
// Build converts a tree-sitter tree to internal AST
func (b *ASTBuilder) Build(tree *sitter.Tree) (*Node, error) {
if tree == nil {
return nil, fmt.Errorf("tree is nil")
}
rootNode := tree.RootNode()
if rootNode == nil {
return nil, fmt.Errorf("root node is nil")
}
ast := b.buildNode(rootNode)
return ast, nil
}
// buildNode recursively builds AST nodes from tree-sitter nodes
func (b *ASTBuilder) buildNode(tsNode *sitter.Node) *Node {
if tsNode == nil {
return nil
}
nodeType := tsNode.Type()
// Create appropriate AST node based on tree-sitter node type
switch nodeType {
case "module":
return b.buildModule(tsNode)
case "function_definition":
return b.buildFunctionDef(tsNode)
case "class_definition":
return b.buildClassDef(tsNode)
case "if_statement":
return b.buildIfStatement(tsNode)
case "elif_clause":
return b.buildElifClause(tsNode)
case "else_clause":
return b.buildElseClause(tsNode)
case "for_statement":
return b.buildForStatement(tsNode)
case "while_statement":
return b.buildWhileStatement(tsNode)
case "with_statement":
return b.buildWithStatement(tsNode)
case "try_statement":
return b.buildTryStatement(tsNode)
case "match_statement":
return b.buildMatchStatement(tsNode)
case "return_statement":
return b.buildReturnStatement(tsNode)
case "delete_statement":
return b.buildDeleteStatement(tsNode)
case "raise_statement":
return b.buildRaiseStatement(tsNode)
case "assert_statement":
return b.buildAssertStatement(tsNode)
case "import_statement":
return b.buildImportStatement(tsNode)
case "import_from_statement":
return b.buildImportFromStatement(tsNode)
case "global_statement":
return b.buildGlobalStatement(tsNode)
case "nonlocal_statement":
return b.buildNonlocalStatement(tsNode)
case "expression_statement":
return b.buildExpressionStatement(tsNode)
case "assignment":
return b.buildAssignment(tsNode)
case "augmented_assignment":
return b.buildAugmentedAssignment(tsNode)
case "pass_statement":
return b.buildPassStatement(tsNode)
case "break_statement":
return b.buildBreakStatement(tsNode)
case "continue_statement":
return b.buildContinueStatement(tsNode)
case "decorated_definition":
return b.buildDecoratedDefinition(tsNode)
// Expressions
case "binary_operator":
return b.buildBinaryOp(tsNode)
case "unary_operator":
return b.buildUnaryOp(tsNode)
case "boolean_operator":
return b.buildBoolOp(tsNode)
case "comparison_operator":
return b.buildCompare(tsNode)
case "conditional_expression":
return b.buildIfExp(tsNode)
case "lambda":
return b.buildLambda(tsNode)
case "call":
return b.buildCall(tsNode)
case "attribute":
return b.buildAttribute(tsNode)
case "subscript":
return b.buildSubscript(tsNode)
case "slice":
return b.buildSlice(tsNode)
case "list":
return b.buildList(tsNode)
case "tuple":
return b.buildTuple(tsNode)
case "dictionary":
return b.buildDict(tsNode)
case "set":
return b.buildSet(tsNode)
case "list_comprehension":
return b.buildListComp(tsNode)
case "dictionary_comprehension":
return b.buildDictComp(tsNode)
case "set_comprehension":
return b.buildSetComp(tsNode)
case "generator_expression":
return b.buildGeneratorExp(tsNode)
case "yield":
return b.buildYield(tsNode)
case "yield_from":
return b.buildYieldFrom(tsNode)
case "await":
return b.buildAwait(tsNode)
case "identifier":
return b.buildName(tsNode)
case "integer", "float", "string", "concatenated_string", "true", "false", "none":
return b.buildConstant(tsNode)
case "formatted_string", "interpolation":
return b.buildFormattedString(tsNode)
// Handle compound statements
case "block":
return b.buildBlock(tsNode)
default:
// For unhandled types, create a generic node with children
node := NewNode(NodeType(nodeType))
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && !b.isTrivia(child) {
if childNode := b.buildNode(child); childNode != nil {
node.AddChild(childNode)
}
}
}
return node
}
}
// buildModule builds a module node
func (b *ASTBuilder) buildModule(tsNode *sitter.Node) *Node {
node := NewNode(NodeModule)
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && !b.isTrivia(child) {
if stmt := b.buildNode(child); stmt != nil {
node.AddToBody(stmt)
}
}
}
return node
}
// buildFunctionDef builds a function definition node
func (b *ASTBuilder) buildFunctionDef(tsNode *sitter.Node) *Node {
node := NewNode(NodeFunctionDef)
node.Location = b.getLocation(tsNode)
// Check if it's async
if b.hasChildOfType(tsNode, "async") {
node.Type = NodeAsyncFunctionDef
}
// Get function name
if nameNode := b.getChildByFieldName(tsNode, "name"); nameNode != nil {
node.Name = b.getNodeText(nameNode)
}
// Get parameters
if paramsNode := b.getChildByFieldName(tsNode, "parameters"); paramsNode != nil {
node.Args = b.buildParameters(paramsNode)
// Set parent for args
for _, arg := range node.Args {
if arg != nil {
arg.Parent = node
}
}
}
// Get body
if bodyNode := b.getChildByFieldName(tsNode, "body"); bodyNode != nil {
if body := b.buildNode(bodyNode); body != nil {
node.Body = b.extractBlockBody(body, node)
}
}
// Get return type annotation if present
if returnType := b.getChildByFieldName(tsNode, "return_type"); returnType != nil {
// Store return type in Value field
node.Value = b.getNodeText(returnType)
}
return node
}
// buildClassDef builds a class definition node
func (b *ASTBuilder) buildClassDef(tsNode *sitter.Node) *Node {
node := NewNode(NodeClassDef)
node.Location = b.getLocation(tsNode)
// Get class name
if nameNode := b.getChildByFieldName(tsNode, "name"); nameNode != nil {
node.Name = b.getNodeText(nameNode)
}
// Get base classes
if superclasses := b.getChildByFieldName(tsNode, "superclasses"); superclasses != nil {
node.Bases = b.buildArgumentList(superclasses)
}
// Get body
if bodyNode := b.getChildByFieldName(tsNode, "body"); bodyNode != nil {
if body := b.buildNode(bodyNode); body != nil {
node.Body = b.extractBlockBody(body, node)
}
}
return node
}
// buildIfStatement builds an if statement node
func (b *ASTBuilder) buildIfStatement(tsNode *sitter.Node) *Node {
node := NewNode(NodeIf)
node.Location = b.getLocation(tsNode)
// Get condition
if condition := b.getChildByFieldName(tsNode, "condition"); condition != nil {
node.Test = b.buildNode(condition)
}
// Get consequence
if consequence := b.getChildByFieldName(tsNode, "consequence"); consequence != nil {
if body := b.buildNode(consequence); body != nil {
node.Body = b.extractBlockBody(body, node)
}
}
// Get alternatives (else/elif) - there may be multiple with the same field name
// Tree-sitter can have both elif_clause and else_clause as "alternative"
var elifNode *Node
var elseNode *Node
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && tsNode.FieldNameForChild(i) == "alternative" {
alt := b.buildNode(child)
if alt != nil {
if alt.Type == NodeIf || alt.Type == NodeElifClause {
elifNode = alt
} else if alt.Type == NodeElseClause {
elseNode = alt
}
}
}
}
// If we have an elif, attach the else to it
if elifNode != nil {
if elseNode != nil {
// Attach else to the elif chain
b.attachElseToElifChain(elifNode, elseNode)
}
node.Orelse = []*Node{elifNode}
} else if elseNode != nil {
// Just an else clause, no elif
node.Orelse = b.extractBlockBody(elseNode, node)
}
return node
}
// attachElseToElifChain attaches an else clause to the end of an elif chain
func (b *ASTBuilder) attachElseToElifChain(elifNode *Node, elseNode *Node) {
current := elifNode
// Find the last elif in the chain
for len(current.Orelse) > 0 && current.Orelse[0].Type == NodeElifClause {
current = current.Orelse[0]
}
// Attach the else clause
current.Orelse = b.extractBlockBody(elseNode, current)
}
// buildElifClause builds an elif clause node (similar to if statement)
func (b *ASTBuilder) buildElifClause(tsNode *sitter.Node) *Node {
node := NewNode(NodeElifClause)
node.Location = b.getLocation(tsNode)
// Get condition
if condition := b.getChildByFieldName(tsNode, "condition"); condition != nil {
node.Test = b.buildNode(condition)
}
// Get consequence (body)
if consequence := b.getChildByFieldName(tsNode, "consequence"); consequence != nil {
if body := b.buildNode(consequence); body != nil {
node.Body = b.extractBlockBody(body, node)
}
}
// Note: elif_clause in tree-sitter doesn't have an alternative field
// The else clause is handled at the parent if_statement level
return node
}
// buildElseClause builds an else clause node
func (b *ASTBuilder) buildElseClause(tsNode *sitter.Node) *Node {
node := NewNode(NodeElseClause)
node.Location = b.getLocation(tsNode)
// Get body
if body := b.getChildByFieldName(tsNode, "body"); body != nil {
if bodyNode := b.buildNode(body); bodyNode != nil {
node.Body = b.extractBlockBody(bodyNode, node)
}
}
return node
}
// buildForStatement builds a for loop node
func (b *ASTBuilder) buildForStatement(tsNode *sitter.Node) *Node {
node := NewNode(NodeFor)
node.Location = b.getLocation(tsNode)
// Check if it's async
if b.hasChildOfType(tsNode, "async") {
node.Type = NodeAsyncFor
}
// Get target variable
if left := b.getChildByFieldName(tsNode, "left"); left != nil {
target := b.buildNode(left)
if target != nil {
node.Targets = []*Node{target}
}
}
// Get iterator
if right := b.getChildByFieldName(tsNode, "right"); right != nil {
node.Iter = b.buildNode(right)
}
// Get body
if bodyNode := b.getChildByFieldName(tsNode, "body"); bodyNode != nil {
if body := b.buildNode(bodyNode); body != nil {
node.Body = b.extractBlockBody(body, node)
}
}
// Get else clause if present
if alternative := b.getChildByFieldName(tsNode, "alternative"); alternative != nil {
if alt := b.buildNode(alternative); alt != nil {
node.Orelse = b.extractBlockBody(alt, node)
}
}
return node
}
// buildWhileStatement builds a while loop node
func (b *ASTBuilder) buildWhileStatement(tsNode *sitter.Node) *Node {
node := NewNode(NodeWhile)
node.Location = b.getLocation(tsNode)
// Get condition
if condition := b.getChildByFieldName(tsNode, "condition"); condition != nil {
node.Test = b.buildNode(condition)
}
// Get body
if bodyNode := b.getChildByFieldName(tsNode, "body"); bodyNode != nil {
if body := b.buildNode(bodyNode); body != nil {
node.Body = b.extractBlockBody(body, node)
}
}
// Get else clause if present
if alternative := b.getChildByFieldName(tsNode, "alternative"); alternative != nil {
if alt := b.buildNode(alternative); alt != nil {
node.Orelse = b.extractBlockBody(alt, node)
}
}
return node
}
// buildWithStatement builds a with statement node
func (b *ASTBuilder) buildWithStatement(tsNode *sitter.Node) *Node {
node := NewNode(NodeWith)
node.Location = b.getLocation(tsNode)
// Check if it's async
if b.hasChildOfType(tsNode, "async") {
node.Type = NodeAsyncWith
}
// Get with items
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() == "with_clause" {
if withItem := b.buildWithItem(child); withItem != nil {
node.AddChild(withItem)
}
}
}
// Get body
if bodyNode := b.getChildByFieldName(tsNode, "body"); bodyNode != nil {
if body := b.buildNode(bodyNode); body != nil {
node.Body = b.extractBlockBody(body, node)
}
}
return node
}
// buildTryStatement builds a try statement node
func (b *ASTBuilder) buildTryStatement(tsNode *sitter.Node) *Node {
node := NewNode(NodeTry)
node.Location = b.getLocation(tsNode)
// Get body
if bodyNode := b.getChildByFieldName(tsNode, "body"); bodyNode != nil {
if body := b.buildNode(bodyNode); body != nil {
node.Body = b.extractBlockBody(body, node)
}
}
// Get except handlers
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() == "except_clause" {
if handler := b.buildExceptHandler(child); handler != nil {
node.Handlers = append(node.Handlers, handler)
}
}
}
// Get else clause
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() == "else_clause" {
if bodyNode := b.getChildByFieldName(child, "body"); bodyNode != nil {
if body := b.buildNode(bodyNode); body != nil {
node.Orelse = b.extractBlockBody(body, node)
}
}
}
}
// Get finally clause
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() == "finally_clause" {
if bodyNode := b.getChildByFieldName(child, "body"); bodyNode != nil {
if body := b.buildNode(bodyNode); body != nil {
node.Finalbody = b.extractBlockBody(body, node)
}
}
}
}
return node
}
// buildMatchStatement builds a match statement node (Python 3.10+)
func (b *ASTBuilder) buildMatchStatement(tsNode *sitter.Node) *Node {
node := NewNode(NodeMatch)
node.Location = b.getLocation(tsNode)
// Get subject
if subject := b.getChildByFieldName(tsNode, "subject"); subject != nil {
node.Test = b.buildNode(subject)
}
// Get cases
if body := b.getChildByFieldName(tsNode, "body"); body != nil {
childCount := int(body.ChildCount())
for i := 0; i < childCount; i++ {
child := body.Child(i)
if child != nil && child.Type() == "case_clause" {
if caseNode := b.buildMatchCase(child); caseNode != nil {
node.AddToBody(caseNode)
}
}
}
}
return node
}
// Helper method implementations...
// buildReturnStatement builds a return statement node
func (b *ASTBuilder) buildReturnStatement(tsNode *sitter.Node) *Node {
node := NewNode(NodeReturn)
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() != "return" {
node.Value = b.buildNode(child)
break
}
}
return node
}
// buildDeleteStatement builds a delete statement node
func (b *ASTBuilder) buildDeleteStatement(tsNode *sitter.Node) *Node {
node := NewNode(NodeDelete)
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() != "del" {
if target := b.buildNode(child); target != nil {
node.Targets = append(node.Targets, target)
}
}
}
return node
}
// buildRaiseStatement builds a raise statement node
func (b *ASTBuilder) buildRaiseStatement(tsNode *sitter.Node) *Node {
node := NewNode(NodeRaise)
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() != "raise" {
// First non-raise child is the exception
if node.Value == nil {
node.Value = b.buildNode(child)
} else {
// Second is the cause (raise ... from ...)
node.AddChild(b.buildNode(child))
}
}
}
return node
}
// buildAssertStatement builds an assert statement node
func (b *ASTBuilder) buildAssertStatement(tsNode *sitter.Node) *Node {
node := NewNode(NodeAssert)
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
argCount := 0
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() != "assert" && child.Type() != "," {
if argCount == 0 {
node.Test = b.buildNode(child)
} else {
node.Value = b.buildNode(child)
}
argCount++
}
}
return node
}
// buildImportStatement builds an import statement node
func (b *ASTBuilder) buildImportStatement(tsNode *sitter.Node) *Node {
node := NewNode(NodeImport)
node.Location = b.getLocation(tsNode)
// Tree-sitter uses "name" field for import names
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
fieldName := tsNode.FieldNameForChild(i)
child := tsNode.Child(i)
if fieldName == "name" && child != nil {
if child.Type() == "dotted_name" {
// Simple import
node.Names = append(node.Names, b.getNodeText(child))
} else if child.Type() == "aliased_import" {
// Import with alias
if alias := b.buildAlias(child); alias != nil {
node.AddChild(alias)
// Also add the original name to Names
if nameChild := b.getChildByFieldName(child, "name"); nameChild != nil {
node.Names = append(node.Names, b.getNodeText(nameChild))
}
}
}
}
}
return node
}
// buildImportFromStatement builds an import from statement node
func (b *ASTBuilder) buildImportFromStatement(tsNode *sitter.Node) *Node {
node := NewNode(NodeImportFrom)
node.Location = b.getLocation(tsNode)
// Count leading dots for relative imports
text := b.getNodeText(tsNode)
if strings.HasPrefix(text, "from") {
afterFrom := text[4:]
trimmed := strings.TrimLeft(afterFrom, " ")
node.Level = len(trimmed) - len(strings.TrimLeft(trimmed, "."))
}
// Get module name
if moduleNode := b.getChildByFieldName(tsNode, "module_name"); moduleNode != nil {
// Handle relative imports
if moduleNode.Type() == "relative_import" {
// Count dots in import_prefix
for i := 0; i < int(moduleNode.ChildCount()); i++ {
child := moduleNode.Child(i)
if child != nil && child.Type() == "import_prefix" {
dots := b.getNodeText(child)
node.Level = len(dots)
} else if child != nil && child.Type() == "dotted_name" {
node.Module = b.getNodeText(child)
}
}
} else {
node.Module = b.getNodeText(moduleNode)
}
}
// Get imported names - tree-sitter uses "name" field directly
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
fieldName := tsNode.FieldNameForChild(i)
child := tsNode.Child(i)
if fieldName == "name" && child != nil {
// Handle each imported name
if child.Type() == "dotted_name" || child.Type() == "identifier" {
node.Names = append(node.Names, b.getNodeText(child))
} else if child.Type() == "aliased_import" {
// Handle aliased imports - extract the original name
if nameChild := b.getChildByFieldName(child, "name"); nameChild != nil {
node.Names = append(node.Names, b.getNodeText(nameChild))
}
// Also build alias for additional info
if alias := b.buildAlias(child); alias != nil {
node.AddChild(alias)
}
}
} else if child != nil && child.Type() == "wildcard_import" {
// Handle wildcard imports (from module import *)
node.Names = append(node.Names, "*")
}
}
return node
}
// buildGlobalStatement builds a global statement node
func (b *ASTBuilder) buildGlobalStatement(tsNode *sitter.Node) *Node {
node := NewNode(NodeGlobal)
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() == "identifier" {
node.Names = append(node.Names, b.getNodeText(child))
}
}
return node
}
// buildNonlocalStatement builds a nonlocal statement node
func (b *ASTBuilder) buildNonlocalStatement(tsNode *sitter.Node) *Node {
node := NewNode(NodeNonlocal)
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() == "identifier" {
node.Names = append(node.Names, b.getNodeText(child))
}
}
return node
}
// buildExpressionStatement builds an expression statement node
func (b *ASTBuilder) buildExpressionStatement(tsNode *sitter.Node) *Node {
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && !b.isTrivia(child) {
return b.buildNode(child)
}
}
node := NewNode(NodeExpr)
node.Location = b.getLocation(tsNode)
return node
}
// buildAssignment builds an assignment node
func (b *ASTBuilder) buildAssignment(tsNode *sitter.Node) *Node {
node := NewNode(NodeAssign)
node.Location = b.getLocation(tsNode)
// Get left-hand side (targets)
if left := b.getChildByFieldName(tsNode, "left"); left != nil {
if target := b.buildNode(left); target != nil {
node.Targets = []*Node{target}
}
}
// Get right-hand side (value)
if right := b.getChildByFieldName(tsNode, "right"); right != nil {
node.Value = b.buildNode(right)
}
// Check if it's an annotated assignment
if typeNode := b.getChildByFieldName(tsNode, "type"); typeNode != nil {
node.Type = NodeAnnAssign
// Store type annotation in the first child
node.AddChild(b.buildNode(typeNode))
}
return node
}
// buildAugmentedAssignment builds an augmented assignment node
func (b *ASTBuilder) buildAugmentedAssignment(tsNode *sitter.Node) *Node {
node := NewNode(NodeAugAssign)
node.Location = b.getLocation(tsNode)
// Get target
if left := b.getChildByFieldName(tsNode, "left"); left != nil {
if target := b.buildNode(left); target != nil {
node.Targets = []*Node{target}
}
}
// Get operator
if operator := b.getChildByFieldName(tsNode, "operator"); operator != nil {
node.Op = strings.TrimSuffix(b.getNodeText(operator), "=")
}
// Get value
if right := b.getChildByFieldName(tsNode, "right"); right != nil {
node.Value = b.buildNode(right)
}
return node
}
// buildPassStatement builds a pass statement node
func (b *ASTBuilder) buildPassStatement(tsNode *sitter.Node) *Node {
node := NewNode(NodePass)
node.Location = b.getLocation(tsNode)
return node
}
// buildBreakStatement builds a break statement node
func (b *ASTBuilder) buildBreakStatement(tsNode *sitter.Node) *Node {
node := NewNode(NodeBreak)
node.Location = b.getLocation(tsNode)
return node
}
// buildContinueStatement builds a continue statement node
func (b *ASTBuilder) buildContinueStatement(tsNode *sitter.Node) *Node {
node := NewNode(NodeContinue)
node.Location = b.getLocation(tsNode)
return node
}
// buildDecoratedDefinition builds a decorated function or class
func (b *ASTBuilder) buildDecoratedDefinition(tsNode *sitter.Node) *Node {
var defNode *Node
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil {
switch child.Type() {
case "decorator":
dec := b.buildDecorator(child)
if dec != nil && defNode != nil {
defNode.Decorator = append(defNode.Decorator, dec)
}
case "function_definition", "class_definition":
defNode = b.buildNode(child)
}
}
}
return defNode
}
// Expression builders...
// buildBinaryOp builds a binary operation node
func (b *ASTBuilder) buildBinaryOp(tsNode *sitter.Node) *Node {
node := NewNode(NodeBinOp)
node.Location = b.getLocation(tsNode)
if left := b.getChildByFieldName(tsNode, "left"); left != nil {
node.Left = b.buildNode(left)
}
if operator := b.getChildByFieldName(tsNode, "operator"); operator != nil {
node.Op = b.getNodeText(operator)
}
if right := b.getChildByFieldName(tsNode, "right"); right != nil {
node.Right = b.buildNode(right)
}
return node
}
// buildUnaryOp builds a unary operation node
func (b *ASTBuilder) buildUnaryOp(tsNode *sitter.Node) *Node {
node := NewNode(NodeUnaryOp)
node.Location = b.getLocation(tsNode)
if operator := b.getChildByFieldName(tsNode, "operator"); operator != nil {
node.Op = b.getNodeText(operator)
}
if operand := b.getChildByFieldName(tsNode, "operand"); operand != nil {
node.Value = b.buildNode(operand)
}
return node
}
// buildBoolOp builds a boolean operation node
func (b *ASTBuilder) buildBoolOp(tsNode *sitter.Node) *Node {
node := NewNode(NodeBoolOp)
node.Location = b.getLocation(tsNode)
if operator := b.getChildByFieldName(tsNode, "operator"); operator != nil {
node.Op = b.getNodeText(operator)
}
if left := b.getChildByFieldName(tsNode, "left"); left != nil {
node.AddChild(b.buildNode(left))
}
if right := b.getChildByFieldName(tsNode, "right"); right != nil {
node.AddChild(b.buildNode(right))
}
return node
}
// buildCompare builds a comparison operation node
func (b *ASTBuilder) buildCompare(tsNode *sitter.Node) *Node {
node := NewNode(NodeCompare)
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && !b.isTrivia(child) {
if i == 0 {
node.Left = b.buildNode(child)
} else if b.isComparisonOperator(child) {
node.Op = b.getNodeText(child)
} else {
node.AddChild(b.buildNode(child))
}
}
}
return node
}
// buildIfExp builds a conditional expression node
func (b *ASTBuilder) buildIfExp(tsNode *sitter.Node) *Node {
node := NewNode(NodeIfExp)
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
stage := 0 // 0: body, 1: test, 2: orelse
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil {
if child.Type() == "if" {
stage = 1
} else if child.Type() == "else" {
stage = 2
} else if !b.isTrivia(child) {
built := b.buildNode(child)
switch stage {
case 0:
node.AddToBody(built)
case 1:
node.Test = built
case 2:
node.Orelse = []*Node{built}
}
}
}
}
return node
}
// buildLambda builds a lambda expression node
func (b *ASTBuilder) buildLambda(tsNode *sitter.Node) *Node {
node := NewNode(NodeLambda)
node.Location = b.getLocation(tsNode)
if params := b.getChildByFieldName(tsNode, "parameters"); params != nil {
node.Args = b.buildParameters(params)
}
if body := b.getChildByFieldName(tsNode, "body"); body != nil {
node.AddToBody(b.buildNode(body))
}
return node
}
// buildCall builds a function call node
func (b *ASTBuilder) buildCall(tsNode *sitter.Node) *Node {
node := NewNode(NodeCall)
node.Location = b.getLocation(tsNode)
if function := b.getChildByFieldName(tsNode, "function"); function != nil {
node.Value = b.buildNode(function)
}
if arguments := b.getChildByFieldName(tsNode, "arguments"); arguments != nil {
node.Args, node.Keywords = b.buildCallArguments(arguments)
}
return node
}
// buildAttribute builds an attribute access node
func (b *ASTBuilder) buildAttribute(tsNode *sitter.Node) *Node {
node := NewNode(NodeAttribute)
node.Location = b.getLocation(tsNode)
if object := b.getChildByFieldName(tsNode, "object"); object != nil {
node.Value = b.buildNode(object)
}
if attr := b.getChildByFieldName(tsNode, "attribute"); attr != nil {
node.Name = b.getNodeText(attr)
}
return node
}
// buildSubscript builds a subscript node
func (b *ASTBuilder) buildSubscript(tsNode *sitter.Node) *Node {
node := NewNode(NodeSubscript)
node.Location = b.getLocation(tsNode)
if value := b.getChildByFieldName(tsNode, "object"); value != nil {
node.Value = b.buildNode(value)
}
if subscript := b.getChildByFieldName(tsNode, "subscript"); subscript != nil {
node.AddChild(b.buildNode(subscript))
}
return node
}
// buildSlice builds a slice node
func (b *ASTBuilder) buildSlice(tsNode *sitter.Node) *Node {
node := NewNode(NodeSlice)
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
sliceArgs := []*Node{nil, nil, nil} // lower, upper, step
argIndex := 0
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil {
if child.Type() == ":" {
argIndex++
} else if !b.isTrivia(child) && argIndex < 3 {
sliceArgs[argIndex] = b.buildNode(child)
}
}
}
// Store slice components
if sliceArgs[0] != nil {
node.AddChild(sliceArgs[0]) // lower
}
if sliceArgs[1] != nil {
node.AddChild(sliceArgs[1]) // upper
}
if sliceArgs[2] != nil {
node.AddChild(sliceArgs[2]) // step
}
return node
}
// Collection builders...
// buildList builds a list node
func (b *ASTBuilder) buildList(tsNode *sitter.Node) *Node {
node := NewNode(NodeList)
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() != "[" && child.Type() != "]" && child.Type() != "," {
node.AddChild(b.buildNode(child))
}
}
return node
}
// buildTuple builds a tuple node
func (b *ASTBuilder) buildTuple(tsNode *sitter.Node) *Node {
node := NewNode(NodeTuple)
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() != "(" && child.Type() != ")" && child.Type() != "," {
node.AddChild(b.buildNode(child))
}
}
return node
}
// buildDict builds a dictionary node
func (b *ASTBuilder) buildDict(tsNode *sitter.Node) *Node {
node := NewNode(NodeDict)
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() == "pair" {
if key := b.getChildByFieldName(child, "key"); key != nil {
node.AddChild(b.buildNode(key))
}
if value := b.getChildByFieldName(child, "value"); value != nil {
node.AddChild(b.buildNode(value))
}
}
}
return node
}
// buildSet builds a set node
func (b *ASTBuilder) buildSet(tsNode *sitter.Node) *Node {
node := NewNode(NodeSet)
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() != "{" && child.Type() != "}" && child.Type() != "," {
node.AddChild(b.buildNode(child))
}
}
return node
}
// Comprehension builders...
// buildListComp builds a list comprehension node
func (b *ASTBuilder) buildListComp(tsNode *sitter.Node) *Node {
node := NewNode(NodeListComp)
node.Location = b.getLocation(tsNode)
b.buildComprehension(tsNode, node)
return node
}
// buildDictComp builds a dictionary comprehension node
func (b *ASTBuilder) buildDictComp(tsNode *sitter.Node) *Node {
node := NewNode(NodeDictComp)
node.Location = b.getLocation(tsNode)
b.buildComprehension(tsNode, node)
return node
}
// buildSetComp builds a set comprehension node
func (b *ASTBuilder) buildSetComp(tsNode *sitter.Node) *Node {
node := NewNode(NodeSetComp)
node.Location = b.getLocation(tsNode)
b.buildComprehension(tsNode, node)
return node
}
// buildGeneratorExp builds a generator expression node
func (b *ASTBuilder) buildGeneratorExp(tsNode *sitter.Node) *Node {
node := NewNode(NodeGeneratorExp)
node.Location = b.getLocation(tsNode)
b.buildComprehension(tsNode, node)
return node
}
// buildComprehension extracts comprehension parts
func (b *ASTBuilder) buildComprehension(tsNode *sitter.Node, node *Node) {
// Extract the body (the expression being generated)
childCount := int(tsNode.ChildCount())
if childCount > 0 {
firstChild := tsNode.Child(0)
// Skip opening bracket/brace
if firstChild != nil && firstChild.Type() != "[" && firstChild.Type() != "{" && firstChild.Type() != "(" {
node.Value = b.buildNode(firstChild)
} else if childCount > 1 {
// Body is likely the second child after opening bracket
bodyChild := tsNode.Child(1)
if bodyChild != nil && bodyChild.Type() != "for_in_clause" {
node.Value = b.buildNode(bodyChild)
}
}
}
// Process for_in_clauses and if_clauses
var currentComp *Node
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child == nil {
continue
}
if child.Type() == "for_in_clause" {
// Create new comprehension node for each for clause
comp := NewNode(NodeComprehension)
// Extract target variable(s)
for j := 0; j < int(child.ChildCount()); j++ {
subChild := child.Child(j)
if subChild != nil && subChild.Type() == "identifier" {
// First identifier after "for" is the target
comp.Targets = []*Node{b.buildNode(subChild)}
break
}
}
// Extract iterator expression (after "in")
foundIn := false
for j := 0; j < int(child.ChildCount()); j++ {
subChild := child.Child(j)
if subChild != nil {
if subChild.Type() == "in" {
foundIn = true
} else if foundIn && subChild.Type() != "identifier" {
comp.Iter = b.buildNode(subChild)
break
}
}
}
node.AddChild(comp)
currentComp = comp
} else if child.Type() == "if_clause" && currentComp != nil {
// if_clause follows the for_in_clause it applies to
// Extract the condition expression
for j := 0; j < int(child.ChildCount()); j++ {
subChild := child.Child(j)
if subChild != nil && subChild.Type() != "if" {
currentComp.Test = b.buildNode(subChild)
break
}
}
}
}
}
// Yield and await builders...
// buildYield builds a yield expression node
func (b *ASTBuilder) buildYield(tsNode *sitter.Node) *Node {
node := NewNode(NodeYield)
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() != "yield" {
node.Value = b.buildNode(child)
break
}
}
return node
}
// buildYieldFrom builds a yield from expression node
func (b *ASTBuilder) buildYieldFrom(tsNode *sitter.Node) *Node {
node := NewNode(NodeYieldFrom)
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() != "yield" && child.Type() != "from" {
node.Value = b.buildNode(child)
break
}
}
return node
}
// buildAwait builds an await expression node
func (b *ASTBuilder) buildAwait(tsNode *sitter.Node) *Node {
node := NewNode(NodeAwait)
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() != "await" {
node.Value = b.buildNode(child)
break
}
}
return node
}
// Literal builders...
// buildName builds an identifier/name node
func (b *ASTBuilder) buildName(tsNode *sitter.Node) *Node {
node := NewNode(NodeName)
node.Location = b.getLocation(tsNode)
node.Name = b.getNodeText(tsNode)
return node
}
// buildConstant builds a constant value node
func (b *ASTBuilder) buildConstant(tsNode *sitter.Node) *Node {
node := NewNode(NodeConstant)
node.Location = b.getLocation(tsNode)
text := b.getNodeText(tsNode)
nodeType := tsNode.Type()
switch nodeType {
case "integer":
if val, err := strconv.ParseInt(text, 0, 64); err == nil {
node.Value = val
} else {
node.Value = text
}
case "float":
if val, err := strconv.ParseFloat(text, 64); err == nil {
node.Value = val
} else {
node.Value = text
}
case "string", "concatenated_string":
// Remove quotes
if len(text) >= 2 {
node.Value = text[1 : len(text)-1]
} else {
node.Value = text
}
case "true":
node.Value = true
case "false":
node.Value = false
case "none":
node.Value = nil
default:
node.Value = text
}
return node
}
// buildFormattedString builds a formatted string (f-string) node
func (b *ASTBuilder) buildFormattedString(tsNode *sitter.Node) *Node {
node := NewNode(NodeJoinedStr)
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil {
switch child.Type() {
case "interpolation":
// Extract the expression inside the interpolation
exprCount := int(child.ChildCount())
for j := 0; j < exprCount; j++ {
exprChild := child.Child(j)
if exprChild != nil && exprChild.Type() != "{" && exprChild.Type() != "}" {
fmtValue := NewNode(NodeFormattedValue)
fmtValue.Value = b.buildNode(exprChild)
node.AddChild(fmtValue)
}
}
case "string_content":
// Regular string content
strNode := NewNode(NodeConstant)
strNode.Value = b.getNodeText(child)
node.AddChild(strNode)
}
}
}
return node
}
// buildBlock builds a block of statements
func (b *ASTBuilder) buildBlock(tsNode *sitter.Node) *Node {
node := NewNode("block")
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && !b.isTrivia(child) {
if stmt := b.buildNode(child); stmt != nil {
node.AddToBody(stmt)
}
}
}
return node
}
// Helper methods...
// buildParameters builds function parameters
func (b *ASTBuilder) buildParameters(tsNode *sitter.Node) []*Node {
var params []*Node
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil {
switch child.Type() {
case "identifier":
arg := NewNode(NodeArg)
arg.Name = b.getNodeText(child)
params = append(params, arg)
case "default_parameter":
arg := NewNode(NodeArg)
if nameNode := b.getChildByFieldName(child, "name"); nameNode != nil {
arg.Name = b.getNodeText(nameNode)
}
if valueNode := b.getChildByFieldName(child, "value"); valueNode != nil {
arg.Value = b.buildNode(valueNode)
}
params = append(params, arg)
case "typed_parameter", "typed_default_parameter":
arg := NewNode(NodeArg)
if nameNode := b.getChildByFieldName(child, "name"); nameNode != nil {
arg.Name = b.getNodeText(nameNode)
}
// Store type annotation in Value field for now
if typeNode := b.getChildByFieldName(child, "type"); typeNode != nil {
arg.Value = b.getNodeText(typeNode)
}
params = append(params, arg)
case "list_splat_pattern":
arg := NewNode(NodeArg)
arg.Name = "*" + b.getNodeTextExcluding(child, "*")
params = append(params, arg)
case "dictionary_splat_pattern":
arg := NewNode(NodeArg)
arg.Name = "**" + b.getNodeTextExcluding(child, "**")
params = append(params, arg)
}
}
}
return params
}
// buildArgumentList builds a list of arguments (for base classes, etc.)
func (b *ASTBuilder) buildArgumentList(tsNode *sitter.Node) []*Node {
var args []*Node
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() != "(" && child.Type() != ")" && child.Type() != "," {
args = append(args, b.buildNode(child))
}
}
return args
}
// buildCallArguments builds call arguments and keywords
func (b *ASTBuilder) buildCallArguments(tsNode *sitter.Node) ([]*Node, []*Node) {
var args []*Node
var keywords []*Node
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil {
switch child.Type() {
case "keyword_argument":
kw := NewNode(NodeKeyword)
if nameNode := b.getChildByFieldName(child, "name"); nameNode != nil {
kw.Name = b.getNodeText(nameNode)
}
if valueNode := b.getChildByFieldName(child, "value"); valueNode != nil {
kw.Value = b.buildNode(valueNode)
}
keywords = append(keywords, kw)
case "(", ")", ",":
// Skip syntax elements
default:
args = append(args, b.buildNode(child))
}
}
}
return args, keywords
}
// buildWithItem builds a with item node
func (b *ASTBuilder) buildWithItem(tsNode *sitter.Node) *Node {
node := NewNode(NodeWithItem)
node.Location = b.getLocation(tsNode)
if item := b.getChildByFieldName(tsNode, "item"); item != nil {
node.Value = b.buildNode(item)
}
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() == "as_pattern" {
if alias := b.getChildByFieldName(child, "alias"); alias != nil {
node.Name = b.getNodeText(alias)
}
}
}
return node
}
// buildExceptHandler builds an except handler node
func (b *ASTBuilder) buildExceptHandler(tsNode *sitter.Node) *Node {
node := NewNode(NodeExceptHandler)
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil {
switch child.Type() {
case "as_pattern":
// Exception type and optional name
if exType := b.getChildByFieldName(child, "type"); exType != nil {
node.Value = b.buildNode(exType)
}
if alias := b.getChildByFieldName(child, "alias"); alias != nil {
node.Name = b.getNodeText(alias)
}
case "block":
// Handler body
if body := b.buildNode(child); body != nil {
node.Body = b.extractBlockBody(body, node)
}
default:
if child.Type() != "except" && child.Type() != ":" {
// Exception type without alias
node.Value = b.buildNode(child)
}
}
}
}
return node
}
// buildMatchCase builds a match case node
func (b *ASTBuilder) buildMatchCase(tsNode *sitter.Node) *Node {
node := NewNode(NodeMatchCase)
node.Location = b.getLocation(tsNode)
if pattern := b.getChildByFieldName(tsNode, "pattern"); pattern != nil {
node.Test = b.buildNode(pattern)
}
if guard := b.getChildByFieldName(tsNode, "guard"); guard != nil {
// Store guard in Value field
node.Value = b.buildNode(guard)
}
if consequence := b.getChildByFieldName(tsNode, "consequence"); consequence != nil {
if body := b.buildNode(consequence); body != nil {
node.Body = b.extractBlockBody(body, node)
}
}
return node
}
// buildDecorator builds a decorator node
func (b *ASTBuilder) buildDecorator(tsNode *sitter.Node) *Node {
node := NewNode(NodeDecorator)
node.Location = b.getLocation(tsNode)
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() != "@" {
node.Value = b.buildNode(child)
break
}
}
return node
}
// buildAlias builds an import alias node
func (b *ASTBuilder) buildAlias(tsNode *sitter.Node) *Node {
node := NewNode(NodeAlias)
node.Location = b.getLocation(tsNode)
if tsNode.Type() == "aliased_import" {
if nameNode := b.getChildByFieldName(tsNode, "name"); nameNode != nil {
node.Name = b.getNodeText(nameNode)
}
if aliasNode := b.getChildByFieldName(tsNode, "alias"); aliasNode != nil {
node.Value = b.getNodeText(aliasNode)
}
} else {
node.Name = b.getNodeText(tsNode)
}
return node
}
// Utility methods...
// getLocation extracts location information from a tree-sitter node
func (b *ASTBuilder) getLocation(tsNode *sitter.Node) Location {
startPoint := tsNode.StartPoint()
endPoint := tsNode.EndPoint()
return Location{
StartLine: int(startPoint.Row) + 1,
StartCol: int(startPoint.Column),
EndLine: int(endPoint.Row) + 1,
EndCol: int(endPoint.Column),
}
}
// getNodeText gets the text content of a node
func (b *ASTBuilder) getNodeText(tsNode *sitter.Node) string {
return tsNode.Content(b.source)
}
// getNodeTextExcluding gets node text excluding certain prefixes
func (b *ASTBuilder) getNodeTextExcluding(tsNode *sitter.Node, exclude string) string {
text := b.getNodeText(tsNode)
return strings.TrimPrefix(text, exclude)
}
// getChildByFieldName gets a child node by field name
func (b *ASTBuilder) getChildByFieldName(tsNode *sitter.Node, fieldName string) *sitter.Node {
return tsNode.ChildByFieldName(fieldName)
}
// hasChildOfType checks if a node has a child of a specific type
func (b *ASTBuilder) hasChildOfType(tsNode *sitter.Node, childType string) bool {
childCount := int(tsNode.ChildCount())
for i := 0; i < childCount; i++ {
child := tsNode.Child(i)
if child != nil && child.Type() == childType {
return true
}
}
return false
}
// isTrivia checks if a node is trivia (comments, whitespace)
func (b *ASTBuilder) isTrivia(tsNode *sitter.Node) bool {
nodeType := tsNode.Type()
return nodeType == "comment" || nodeType == "line_continuation"
}
// isComparisonOperator checks if a node is a comparison operator
func (b *ASTBuilder) isComparisonOperator(tsNode *sitter.Node) bool {
text := b.getNodeText(tsNode)
operators := []string{"<", ">", "==", ">=", "<=", "!=", "in", "not in", "is", "is not"}
for _, op := range operators {
if text == op {
return true
}
}
return false
}