Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@ go.work.sum
corpus/**/*.json
parser/testdata/**/*.json
/corpus/parser/*

# IDE specific files
.idea/
2 changes: 1 addition & 1 deletion projects/ballerina_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func NewBallerinaBackend(compilation *PackageCompilation) *BallerinaBackend {

// performCodeGen generates BIR for all modules in topological order.
func (b *BallerinaBackend) performCodeGen() {
for _, moduleCtx := range b.packageCompilation.packageResolution.TopologicallySortedModuleList() {
for _, moduleCtx := range b.packageCompilation.Resolution().topologicallySortedModuleList {
if moduleCtx.getCompilationState() == moduleCompilationStateCompiled {
generateCodeInternal(moduleCtx)
}
Expand Down
24 changes: 24 additions & 0 deletions projects/build_project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -606,3 +606,27 @@ func TestAddModule(t *testing.T) {
require.NotNil(testDoc)
assert.Equal(testContent, testDoc.TextDocument().String())
}

// TestMultiModuleDependencyOrder tests that dependency resolution is performed for multi-module
func TestMultiModuleDependencyOrder(t *testing.T) {
assert := test_util.New(t)
require := test_util.NewRequire(t)

projectPath := filepath.Join("testdata", "multi-module-project")
absPath, err := filepath.Abs(projectPath)
require.NoError(err)

// Load the multi-module project
result, err := loadProject(absPath)
require.NoError(err, "Failed to load multi-module-project")

// Get package resolution which performs topological sorting
resolution := result.Project().CurrentPackage().Resolution()
sortedModuleNames := resolution.TopologicallySortedModuleNames()
require.Len(sortedModuleNames, 3)

// Verify the order
assert.Equal("multimoduleproject.storage", sortedModuleNames[0])
assert.Equal("multimoduleproject.services", sortedModuleNames[1])
assert.Equal("multimoduleproject", sortedModuleNames[2])
}
168 changes: 168 additions & 0 deletions projects/dependency_graph.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
* Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package projects

import (
"maps"
"slices"
"sync"
)

// DependencyGraph represents a directed graph of dependencies between nodes.
// It supports topological sorting, cycle detection, and dependency traversal.
type DependencyGraph[T comparable] struct {
rootNode *T
dependencies map[T]map[T]struct{}
topologicallySorted []T
cyclicDependencies [][]T
sortOnce sync.Once
}

// nodes returns all nodes in the graph.
func (g *DependencyGraph[T]) nodes() []T {
return slices.Collect(maps.Keys(g.dependencies))
}

// DirectDependencies returns the direct dependencies of the given node.
// Returns nil if the node does not exist in the graph.
func (g *DependencyGraph[T]) DirectDependencies(node T) []T {
deps, ok := g.dependencies[node]
if !ok {
return nil

Check warning on line 47 in projects/dependency_graph.go

View check run for this annotation

Codecov / codecov/patch

projects/dependency_graph.go#L44-L47

Added lines #L44 - L47 were not covered by tests
}
return slices.Collect(maps.Keys(deps))

Check warning on line 49 in projects/dependency_graph.go

View check run for this annotation

Codecov / codecov/patch

projects/dependency_graph.go#L49

Added line #L49 was not covered by tests
}

// ToTopologicallySortedList returns nodes in dependency order (dependencies first).
// The result is computed lazily and cached for subsequent calls.
func (g *DependencyGraph[T]) ToTopologicallySortedList() []T {
g.ensureSorted()
return slices.Clone(g.topologicallySorted)
}

// FindCycles detects and returns any cycles in the graph.
// Each cycle is represented as a slice of nodes forming the cycle.
// Returns nil if no cycles exist.
func (g *DependencyGraph[T]) FindCycles() [][]T {
g.ensureSorted()
if len(g.cyclicDependencies) == 0 {
return nil
}
result := make([][]T, len(g.cyclicDependencies))
for i, cycle := range g.cyclicDependencies {
result[i] = slices.Clone(cycle)

Check warning on line 69 in projects/dependency_graph.go

View check run for this annotation

Codecov / codecov/patch

projects/dependency_graph.go#L67-L69

Added lines #L67 - L69 were not covered by tests
}
return result

Check warning on line 71 in projects/dependency_graph.go

View check run for this annotation

Codecov / codecov/patch

projects/dependency_graph.go#L71

Added line #L71 was not covered by tests
}

func (g *DependencyGraph[T]) ensureSorted() {
g.sortOnce.Do(func() {
g.topologicallySorted, g.cyclicDependencies = g.computeTopologicalSort()
})
}

// computeTopologicalSort performs DFS-based topological sort on the graph.
// Returns nodes in dependency order (dependencies before dependents)
// and any cycles detected.
func (g *DependencyGraph[T]) computeTopologicalSort() ([]T, [][]T) {
nodes := g.nodes()
visited := make(map[T]bool, len(nodes))
stackPos := make(map[T]int, len(nodes))
var stack []T
sorted := make([]T, 0, len(nodes))
var cycles [][]T

var visit func(vertex T)
visit = func(vertex T) {
stackPos[vertex] = len(stack)
stack = append(stack, vertex)

for dep := range g.dependencies[vertex] {
if pos, inStack := stackPos[dep]; inStack {
// Found a cycle - extract it from the stack
cycles = append(cycles, slices.Clone(stack[pos:]))

Check warning on line 99 in projects/dependency_graph.go

View check run for this annotation

Codecov / codecov/patch

projects/dependency_graph.go#L99

Added line #L99 was not covered by tests
} else if !visited[dep] {
visit(dep)
}
}
// Post-order: add to sorted list after processing all dependencies
sorted = append(sorted, vertex)
visited[vertex] = true
delete(stackPos, vertex)
stack = stack[:len(stack)-1]
}

for _, node := range nodes {
if !visited[node] {
visit(node)
}
}

return sorted, cycles
}

type dependencyGraphBuilder[T comparable] struct {
rootNode *T
dependencies map[T]map[T]struct{}
}

func newDependencyGraphBuilder[T comparable]() *dependencyGraphBuilder[T] {
return &dependencyGraphBuilder[T]{
dependencies: make(map[T]map[T]struct{}),
}
}

func (b *dependencyGraphBuilder[T]) addNode(node T) *dependencyGraphBuilder[T] {
b.ensureNode(node)
return b
}

func (b *dependencyGraphBuilder[T]) addDependency(from, to T) *dependencyGraphBuilder[T] {
// Both nodes are added to the graph if they don't exist.
b.ensureNode(from)
b.ensureNode(to)
b.dependencies[from][to] = struct{}{}
return b
}

// build creates the immutable DependencyGraph from the builder's current state.
// The builder can continue to be used after build is called.
func (b *dependencyGraphBuilder[T]) build() *DependencyGraph[T] {
cloned := make(map[T]map[T]struct{}, len(b.dependencies))
for k, v := range b.dependencies {
cloned[k] = maps.Clone(v)
}

var rootCopy *T
if b.rootNode != nil {
r := *b.rootNode
rootCopy = &r

Check warning on line 155 in projects/dependency_graph.go

View check run for this annotation

Codecov / codecov/patch

projects/dependency_graph.go#L154-L155

Added lines #L154 - L155 were not covered by tests
}

return &DependencyGraph[T]{
rootNode: rootCopy,
dependencies: cloned,
}
}

func (b *dependencyGraphBuilder[T]) ensureNode(node T) {
if _, ok := b.dependencies[node]; !ok {
b.dependencies[node] = make(map[T]struct{})
}
}
62 changes: 62 additions & 0 deletions projects/document_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package projects

import (
"strings"
"sync"

"ballerina-lang-go/parser"
Expand Down Expand Up @@ -144,3 +145,64 @@
disableSyntaxTree: d.disableSyntaxTree,
}
}

func (d *documentContext) moduleLoadRequests() []*moduleLoadRequest {
syntaxTree := d.getSyntaxTree()
if syntaxTree == nil {
return nil

Check warning on line 152 in projects/document_context.go

View check run for this annotation

Codecov / codecov/patch

projects/document_context.go#L152

Added line #L152 was not covered by tests
}

rootNode := syntaxTree.RootNode
if rootNode == nil {
return nil

Check warning on line 157 in projects/document_context.go

View check run for this annotation

Codecov / codecov/patch

projects/document_context.go#L157

Added line #L157 was not covered by tests
}

modulePart, ok := rootNode.(*tree.ModulePart)
if !ok {
return nil

Check warning on line 162 in projects/document_context.go

View check run for this annotation

Codecov / codecov/patch

projects/document_context.go#L162

Added line #L162 was not covered by tests
}

var requests []*moduleLoadRequest
imports := modulePart.Imports()
for i := 0; i < imports.Size(); i++ {
importDecl := imports.Get(i)
request := extractModuleLoadRequest(importDecl)
if request != nil {
requests = append(requests, request)
}
}
return requests
}

func extractModuleLoadRequest(importDecl *tree.ImportDeclarationNode) *moduleLoadRequest {
// Get organization name (optional)
var orgName *PackageOrg
if importDecl.OrgName() != nil {
orgNameNode := importDecl.OrgName()
if orgNameNode.OrgName() != nil {
// Handle quoted identifiers - strip the leading ' character
text := orgNameNode.OrgName().Text()
if len(text) > 0 && text[0] == '\'' {
text = text[1:]

Check warning on line 186 in projects/document_context.go

View check run for this annotation

Codecov / codecov/patch

projects/document_context.go#L186

Added line #L186 was not covered by tests
}
org := NewPackageOrg(text)
orgName = &org
}
}

// Build module name by joining identifiers with "."
// Use Iterator which filters out separator tokens (dots)
moduleNameList := importDecl.ModuleName()
var moduleNameParts []string
for ident := range moduleNameList.Iterator() {
// Handle quoted identifiers - strip the leading ' character
text := ident.Text()
if len(text) > 0 && text[0] == '\'' {
text = text[1:]

Check warning on line 201 in projects/document_context.go

View check run for this annotation

Codecov / codecov/patch

projects/document_context.go#L201

Added line #L201 was not covered by tests
}
moduleNameParts = append(moduleNameParts, text)
}
moduleName := strings.Join(moduleNameParts, ".")

return newModuleLoadRequest(orgName, moduleName)
}
31 changes: 31 additions & 0 deletions projects/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package projects

// This file exports internal types for testing purposes only.
// It is only compiled during test runs.

// TopologicallySortedModuleNames returns module names in dependency order for testing.
func (r *PackageResolution) TopologicallySortedModuleNames() []string {
names := make([]string, len(r.topologicallySortedModuleList))
for i, modCtx := range r.topologicallySortedModuleList {
names[i] = modCtx.getModuleName().String()
}
return names
}
Loading