-
Notifications
You must be signed in to change notification settings - Fork 0
Enable configurable cross-project parsing #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,6 +50,14 @@ import ( | |
| "github.com/smacker/go-tree-sitter/yaml" | ||
| ) | ||
|
|
||
| // NodeColors defines basic colors for node types used when creating nodes | ||
| var NodeColors = map[string]string{ | ||
| "Root": "orange", | ||
| "Namespace": "#f5a442", | ||
| "Type": "#4287f5", | ||
| "Function": "#42f54e", | ||
| } | ||
|
|
||
| // CallRelation struct to store pending function call relationships | ||
| type CallRelation struct { | ||
| CallerName string | ||
|
|
@@ -63,23 +71,44 @@ type TreeSitterParser struct { | |
| language *sitter.Language | ||
| code string | ||
| ext string | ||
| filePath string | ||
| currentFunc string | ||
| currentNamespace string | ||
| currentType string | ||
| driver neo4j.DriverWithContext | ||
| tree *sitter.Tree | ||
|
|
||
| rootName string | ||
| baseURL string | ||
| projectRoot string | ||
| rootID string | ||
|
|
||
| // Pending call relationships | ||
| pendingRelations []CallRelation | ||
| pendingMutex sync.Mutex | ||
| } | ||
|
|
||
| // NewTreeSitterParser initializes the TreeSitterParser and connects it to Neo4j. | ||
| func NewTreeSitterParser(driver neo4j.DriverWithContext) *TreeSitterParser { | ||
| // NewTreeSitterParser initializes the TreeSitterParser with configuration | ||
| func NewTreeSitterParser(driver neo4j.DriverWithContext, rootName, baseURL, projectRoot string) *TreeSitterParser { | ||
| return &TreeSitterParser{ | ||
| Handle: sitter.NewParser(), | ||
| driver: driver, | ||
| Handle: sitter.NewParser(), | ||
| driver: driver, | ||
| rootName: rootName, | ||
| baseURL: baseURL, | ||
| projectRoot: projectRoot, | ||
| rootID: fmt.Sprintf("root:%s", rootName), | ||
| } | ||
| } | ||
|
|
||
| // createURL builds a link to the source code line using BaseURL and project root | ||
| func (parser *TreeSitterParser) createURL(line int) string { | ||
| rel := strings.TrimPrefix(parser.filePath, parser.projectRoot) | ||
| base := parser.baseURL | ||
| if !strings.HasSuffix(base, "/") { | ||
| base += "/" | ||
| } | ||
| return fmt.Sprintf("%s%s#%d", base, strings.TrimPrefix(rel, "/"), line) | ||
| } | ||
|
|
||
| // ExtToLang sets the parser's language based on the file extension. | ||
|
|
@@ -259,9 +288,12 @@ func (parser *TreeSitterParser) traverseNode(node *sitter.Node, tx neo4j.Managed | |
|
|
||
| // cleanup handles database cleanup | ||
| func (parser *TreeSitterParser) cleanup() error { | ||
| neo4jURI := "bolt://host.docker.internal:7687" | ||
| neo4jUser := "neo4j" | ||
| neo4jPassword := "securepassword" | ||
| neo4jURI := os.Getenv("NEO4J_URI") | ||
| neo4jUser := os.Getenv("NEO4J_USER") | ||
| neo4jPassword := os.Getenv("NEO4J_PASSWORD") | ||
| if neo4jURI == "" || neo4jUser == "" || neo4jPassword == "" { | ||
| return fmt.Errorf("missing required environment variables: NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD") | ||
| } | ||
|
|
||
| log.Printf("Connecting to Neo4j at %s with user %s and password %s\n", neo4jURI, neo4jUser, neo4jPassword) | ||
| driver, err := neo4j.NewDriverWithContext(neo4jURI, neo4j.BasicAuth(neo4jUser, neo4jPassword, "")) | ||
|
|
@@ -282,8 +314,8 @@ func (parser *TreeSitterParser) cleanup() error { | |
|
|
||
| _, err = tx1.Run( | ||
| context.Background(), | ||
| "MATCH (n) DETACH DELETE n", | ||
| map[string]any{}, | ||
| "MATCH (n {project: $project}) DETACH DELETE n", | ||
| map[string]any{"project": parser.rootName}, | ||
| ) | ||
| if err != nil { | ||
| _ = tx1.Rollback(context.Background()) | ||
|
|
@@ -338,6 +370,18 @@ func (parser *TreeSitterParser) AnalyzeDirectory(dirPath string) error { | |
| log.Printf("Database cleanup completed successfully") | ||
| } | ||
|
|
||
| // Create or update root node | ||
| // Create or update root node | ||
| session := parser.driver.NewSession(context.Background(), neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite}) | ||
| defer session.Close(context.Background()) // Ensure session is closed | ||
| _, err := session.Run(context.Background(), | ||
| "MERGE (r:Root {id:$id}) ON CREATE SET r.name=$name, r.project=$project, r.color=$color", | ||
| map[string]any{"id": parser.rootID, "name": parser.rootName, "project": parser.rootName, "color": NodeColors["Root"]}) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to create root node: %v", err) | ||
| } | ||
| } | ||
|
|
||
| // Initialize analysis state | ||
| var ( | ||
| processedFiles atomic.Int32 | ||
|
|
@@ -518,10 +562,15 @@ func (parser *TreeSitterParser) processFile(ctx context.Context, path string) er | |
|
|
||
| // Create a new parser instance for this file | ||
| fileParser := &TreeSitterParser{ | ||
| Handle: sitter.NewParser(), | ||
| driver: parser.driver, | ||
| code: string(code), | ||
| ext: filepath.Ext(path)[1:], // This gets the extension without the dot | ||
| Handle: sitter.NewParser(), | ||
| driver: parser.driver, | ||
| code: string(code), | ||
| ext: filepath.Ext(path)[1:], // This gets the extension without the dot | ||
| filePath: path, | ||
| rootName: parser.rootName, | ||
| baseURL: parser.baseURL, | ||
| projectRoot: parser.projectRoot, | ||
| rootID: parser.rootID, | ||
| } | ||
|
|
||
| // Parse and analyze the file | ||
|
|
@@ -608,25 +657,32 @@ func (parser *TreeSitterParser) handleFunction(node *sitter.Node, tx neo4j.Manag | |
| params := parser.extractParameters(node) | ||
| returnType := parser.extractReturnType(node) | ||
|
|
||
| line := int(position.Row) + 1 | ||
| _, err := tx.Run( | ||
| context.Background(), | ||
| `MERGE (f:Function {id: $id}) | ||
| ON CREATE SET | ||
| f.name = $name, | ||
| f.qualifiedName = $qualifiedName, | ||
| f.file = $file, | ||
| f.position = $position, | ||
| f.parameters = $params, | ||
| f.returnType = $returnType | ||
| WITH f | ||
| OPTIONAL MATCH (t:Type {qualifiedName: $typeName}) | ||
| WHERE $typeName IS NOT NULL | ||
| MERGE (t)-[:HAS_MEMBER]->(f)`, | ||
| ON CREATE SET | ||
| f.name = $name, | ||
| f.qualifiedName = $qualifiedName, | ||
| f.file = $file, | ||
| f.project = $project, | ||
| f.color = $color, | ||
| f.url = $url, | ||
| f.position = $position, | ||
| f.parameters = $params, | ||
| f.returnType = $returnType | ||
| WITH f | ||
| OPTIONAL MATCH (t:Type {qualifiedName: $typeName}) | ||
| WHERE $typeName IS NOT NULL | ||
| MERGE (t)-[:HAS_MEMBER]->(f)`, | ||
| map[string]any{ | ||
| "id": fmt.Sprintf("%s:%s", parser.ext, qualifiedName), | ||
| "id": fmt.Sprintf("%s:%s", parser.rootName, qualifiedName), | ||
| "name": funcName, | ||
| "qualifiedName": qualifiedName, | ||
| "file": parser.ext, | ||
| "file": parser.filePath, | ||
| "project": parser.rootName, | ||
| "color": NodeColors["Function"], | ||
| "url": parser.createURL(line), | ||
| "position": fmt.Sprintf("%d:%d", position.Row, position.Column), | ||
| "params": params, | ||
| "returnType": returnType, | ||
|
|
@@ -636,6 +692,9 @@ func (parser *TreeSitterParser) handleFunction(node *sitter.Node, tx neo4j.Manag | |
|
|
||
| if err == nil { | ||
| parser.currentFunc = qualifiedName | ||
| _, _ = tx.Run(context.Background(), | ||
| `MATCH (r:Root {id:$rid}), (f:Function {id:$fid}) MERGE (r)-[:CONTAINS]->(f)`, | ||
| map[string]any{"rid": parser.rootID, "fid": fmt.Sprintf("%s:%s", parser.rootName, qualifiedName)}) | ||
|
Comment on lines
+695
to
+697
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error returned by _, linkErr := tx.Run(context.Background(),
`MATCH (r:Root {id:$rid}), (f:Function {id:$fid}) MERGE (r)-[:CONTAINS]->(f)`,
map[string]any{"rid": parser.rootID, "fid": fmt.Sprintf("%s:%s", parser.rootName, qualifiedName)})
if linkErr != nil {
log.Printf("Error linking function '%s' to root '%s': %v", qualifiedName, parser.rootID, linkErr)
// Consider returning linkErr to ensure transaction rollback if this link is critical
return linkErr
} |
||
| log.Printf("Info: Set currentFunc to '%s'", qualifiedName) | ||
| } else { | ||
| log.Printf("Error handling function '%s': %v", qualifiedName, err) | ||
|
|
@@ -867,18 +926,30 @@ func (parser *TreeSitterParser) handleNamespace(node *sitter.Node, tx neo4j.Mana | |
| namespaceName := nameNode.Content([]byte(parser.code)) | ||
| parser.currentNamespace = namespaceName | ||
|
|
||
| line := int(node.StartPoint().Row) + 1 | ||
| _, err := tx.Run( | ||
| context.Background(), | ||
| `MERGE (n:Namespace {id: $id}) | ||
| ON CREATE SET | ||
| n.name = $name, | ||
| n.file = $file`, | ||
| ON CREATE SET | ||
| n.name = $name, | ||
| n.file = $file, | ||
| n.project = $project, | ||
| n.color = $color, | ||
| n.url = $url`, | ||
| map[string]any{ | ||
| "id": fmt.Sprintf("%s:%s", parser.ext, namespaceName), | ||
| "name": namespaceName, | ||
| "file": parser.ext, | ||
| "id": fmt.Sprintf("%s:%s", parser.rootName, namespaceName), | ||
| "name": namespaceName, | ||
| "file": parser.filePath, | ||
| "project": parser.rootName, | ||
| "color": NodeColors["Namespace"], | ||
| "url": parser.createURL(line), | ||
| }, | ||
| ) | ||
| if err == nil { | ||
| _, _ = tx.Run(context.Background(), | ||
| `MATCH (r:Root {id:$rid}), (n:Namespace {id:$nid}) MERGE (r)-[:CONTAINS]->(n)`, | ||
| map[string]any{"rid": parser.rootID, "nid": fmt.Sprintf("%s:%s", parser.rootName, namespaceName)}) | ||
|
Comment on lines
+949
to
+951
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the _, linkErr := tx.Run(context.Background(),
`MATCH (r:Root {id:$rid}), (n:Namespace {id:$nid}) MERGE (r)-[:CONTAINS]->(n)`,
map[string]any{"rid": parser.rootID, "nid": fmt.Sprintf("%s:%s", parser.rootName, namespaceName)})
if linkErr != nil {
log.Printf("Error linking namespace '%s' to root '%s': %v", namespaceName, parser.rootID, linkErr)
// Consider returning linkErr
return linkErr
} |
||
| } | ||
| return err | ||
| } | ||
|
|
||
|
|
@@ -895,25 +966,37 @@ func (parser *TreeSitterParser) handleType(node *sitter.Node, tx neo4j.ManagedTr | |
| } | ||
| parser.currentType = qualifiedName | ||
|
|
||
| line := int(node.StartPoint().Row) + 1 | ||
| _, err := tx.Run( | ||
| context.Background(), | ||
| `MERGE (t:Type {id: $id}) | ||
| ON CREATE SET | ||
| t.name = $name, | ||
| t.qualifiedName = $qualifiedName, | ||
| t.file = $file | ||
| WITH t | ||
| OPTIONAL MATCH (n:Namespace {name: $namespace}) | ||
| WHERE $namespace IS NOT NULL | ||
| MERGE (n)-[:CONTAINS]->(t)`, | ||
| ON CREATE SET | ||
| t.name = $name, | ||
| t.qualifiedName = $qualifiedName, | ||
| t.file = $file, | ||
| t.project = $project, | ||
| t.color = $color, | ||
| t.url = $url | ||
| WITH t | ||
| OPTIONAL MATCH (n:Namespace {name: $namespace}) | ||
| WHERE $namespace IS NOT NULL | ||
| MERGE (n)-[:CONTAINS]->(t)`, | ||
| map[string]any{ | ||
| "id": fmt.Sprintf("%s:%s", parser.ext, qualifiedName), | ||
| "id": fmt.Sprintf("%s:%s", parser.rootName, qualifiedName), | ||
| "name": typeName, | ||
| "qualifiedName": qualifiedName, | ||
| "file": parser.ext, | ||
| "file": parser.filePath, | ||
| "project": parser.rootName, | ||
| "color": NodeColors["Type"], | ||
| "url": parser.createURL(line), | ||
| "namespace": parser.currentNamespace, | ||
| }, | ||
| ) | ||
| if err == nil { | ||
| _, _ = tx.Run(context.Background(), | ||
| `MATCH (r:Root {id:$rid}), (t:Type {id:$tid}) MERGE (r)-[:CONTAINS]->(t)`, | ||
| map[string]any{"rid": parser.rootID, "tid": fmt.Sprintf("%s:%s", parser.rootName, qualifiedName)}) | ||
|
Comment on lines
+996
to
+998
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error from _, linkErr := tx.Run(context.Background(),
`MATCH (r:Root {id:$rid}), (t:Type {id:$tid}) MERGE (r)-[:CONTAINS]->(t)`,
map[string]any{"rid": parser.rootID, "tid": fmt.Sprintf("%s:%s", parser.rootName, qualifiedName)})
if linkErr != nil {
log.Printf("Error linking type '%s' to root '%s': %v", qualifiedName, parser.rootID, linkErr)
// Consider returning linkErr
return linkErr
} |
||
| } | ||
| return err | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,16 +3,28 @@ package main | |
| import ( | ||
| "context" | ||
| "log" | ||
| "os" | ||
|
|
||
| "github.com/neo4j/neo4j-go-driver/v5/neo4j" | ||
| "github.com/theapemachine/platform-graph/graphlang" | ||
| ) | ||
|
|
||
| // Main function to demonstrate usage. | ||
| func main() { | ||
| neo4jURI := "bolt://host.docker.internal:7687" | ||
| neo4jUser := "neo4j" | ||
| neo4jPassword := "securepassword" | ||
| neo4jURI := os.Getenv("NEO4J_URI") | ||
| neo4jUser := os.Getenv("NEO4J_USER") | ||
| neo4jPassword := os.Getenv("NEO4J_PASSWORD") | ||
| rootName := os.Getenv("ROOT_NAME") | ||
| baseURL := os.Getenv("BASE_URL") | ||
| if neo4jURI == "" || neo4jUser == "" || neo4jPassword == "" { | ||
| log.Fatal("NEO4J_URI, NEO4J_USER and NEO4J_PASSWORD must be set") | ||
| } | ||
| if rootName == "" { | ||
| rootName = "UnknownRoot" | ||
| } | ||
| if baseURL == "" { | ||
| baseURL = "http://localhost" | ||
| } | ||
|
|
||
| log.Printf("Connecting to Neo4j at %s with user %s and password %s\n", neo4jURI, neo4jUser, neo4jPassword) | ||
| driver, err := neo4j.NewDriverWithContext(neo4jURI, neo4j.BasicAuth(neo4jUser, neo4jPassword, "")) | ||
|
|
@@ -21,11 +33,8 @@ func main() { | |
| } | ||
| defer driver.Close(context.Background()) | ||
|
|
||
| session := driver.NewSession(context.Background(), neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite}) | ||
| defer session.Close(context.Background()) | ||
|
|
||
| dirPath := "/app" // Replace with the actual directory path | ||
| dirPath := "/app" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
|
||
| parser := graphlang.NewTreeSitterParser(driver) | ||
| parser := graphlang.NewTreeSitterParser(driver, rootName, baseURL, dirPath) | ||
| parser.AnalyzeDirectory(dirPath) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
createURLbreaks on Windows paths and needs URL-escapingstrings.TrimPrefix(parser.filePath, parser.projectRoot)leaves back-slashes onWindows and doesn’t encode
#, spaces, etc. Usefilepath.ToSlash,path.Join, andurl.PathEscape(from net/url) to generate a portable,clickable link.
import ( "context" @@ + "net/url" "fmt" @@ -func (parser *TreeSitterParser) createURL(line int) string { - rel := strings.TrimPrefix(parser.filePath, parser.projectRoot) - base := parser.baseURL - if !strings.HasSuffix(base, "/") { - base += "/" - } - return fmt.Sprintf("%s%s#%d", base, strings.TrimPrefix(rel, "/"), line) +func (parser *TreeSitterParser) createURL(line int) string { + rel, _ := filepath.Rel(parser.projectRoot, parser.filePath) + rel = filepath.ToSlash(rel) + escaped := url.PathEscape(rel) + return fmt.Sprintf("%s/%s#L%d", strings.TrimRight(parser.baseURL, "/"), escaped, line) }🤖 Prompt for AI Agents