Skip to content

Commit 9dc4762

Browse files
committed
Build order
1 parent c8fe761 commit 9dc4762

File tree

3 files changed

+322
-0
lines changed

3 files changed

+322
-0
lines changed

internal/collections/syncset.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ func (s *SyncSet[T]) Has(key T) bool {
1111
return ok
1212
}
1313

14+
func (s *SyncSet[T]) AddIfAbsent(key T) bool {
15+
_, loaded := s.m.LoadOrStore(key, struct{}{})
16+
return !loaded
17+
}
18+
1419
func (s *SyncSet[T]) Add(key T) {
1520
s.m.Store(key, struct{}{})
1621
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package execute
2+
3+
import (
4+
"strings"
5+
6+
"github.com/microsoft/typescript-go/internal/ast"
7+
"github.com/microsoft/typescript-go/internal/collections"
8+
"github.com/microsoft/typescript-go/internal/compiler"
9+
"github.com/microsoft/typescript-go/internal/core"
10+
"github.com/microsoft/typescript-go/internal/diagnostics"
11+
"github.com/microsoft/typescript-go/internal/tsoptions"
12+
"github.com/microsoft/typescript-go/internal/tspath"
13+
)
14+
15+
type upToDateStatusType uint16
16+
17+
const (
18+
// building current project
19+
upToDateStatusTypeUnknown upToDateStatusType = iota
20+
// config file was not found
21+
upToDateStatusTypeConfigFileNotFound
22+
// upToDateStatusTypeUnbuildable
23+
// upToDateStatusTypeUpToDate
24+
// // The project appears out of date because its upstream inputs are newer than its outputs,
25+
// // but all of its outputs are actually newer than the previous identical outputs of its (.d.ts) inputs.
26+
// // This means we can Pseudo-build (just touch timestamps), as if we had actually built this project.
27+
// upToDateStatusTypeUpToDateWithUpstreamTypes
28+
// upToDateStatusTypeOutputMissing
29+
// upToDateStatusTypeErrorReadingFile
30+
// upToDateStatusTypeOutOfDateWithSelf
31+
// upToDateStatusTypeOutOfDateWithUpstream
32+
// upToDateStatusTypeOutOfDateBuildInfoWithPendingEmit
33+
// upToDateStatusTypeOutOfDateBuildInfoWithErrors
34+
// upToDateStatusTypeOutOfDateOptions
35+
// upToDateStatusTypeOutOfDateRoots
36+
// upToDateStatusTypeUpstreamOutOfDate
37+
// upToDateStatusTypeUpstreamBlocked
38+
// upToDateStatusTypeTsVersionOutputOfDate
39+
// upToDateStatusTypeUpToDateWithInputFileText
40+
// // solution file
41+
upToDateStatusTypeSolution
42+
// upToDateStatusTypeForceBuild
43+
)
44+
45+
type upToDateStatus struct {
46+
kind upToDateStatusType
47+
}
48+
49+
type statusTask struct {
50+
config string
51+
referencedBy string
52+
status chan *upToDateStatus
53+
}
54+
55+
type buildTask struct {
56+
config string
57+
resolved *tsoptions.ParsedCommandLine
58+
upStream []*statusTask
59+
downStream []*statusTask
60+
}
61+
62+
type buildOrderGenerator struct {
63+
host compiler.CompilerHost
64+
tasks collections.SyncMap[tspath.Path, *buildTask]
65+
order []string
66+
errors []*ast.Diagnostic
67+
}
68+
69+
func (b *buildOrderGenerator) Order() []string {
70+
return b.order
71+
}
72+
73+
func (b *buildOrderGenerator) Upstream(configName string) []string {
74+
path := b.toPath(configName)
75+
task, ok := b.tasks.Load(path)
76+
if !ok {
77+
panic("No build task found for " + configName)
78+
}
79+
return core.MapFiltered(task.upStream, func(t *statusTask) (string, bool) {
80+
return t.config, t.status != nil
81+
})
82+
}
83+
84+
func (b *buildOrderGenerator) Downstream(configName string) []string {
85+
path := b.toPath(configName)
86+
task, ok := b.tasks.Load(path)
87+
if !ok {
88+
panic("No build task found for " + configName)
89+
}
90+
return core.Map(task.downStream, func(t *statusTask) string {
91+
return t.referencedBy
92+
})
93+
}
94+
95+
func NewBuildOrderGenerator(command *tsoptions.ParsedBuildCommandLine, host compiler.CompilerHost, isSingleThreaded bool) *buildOrderGenerator {
96+
b := &buildOrderGenerator{host: host}
97+
98+
projects := command.ResolvedProjectPaths()
99+
// Parse all config files in parallel
100+
wg := core.NewWorkGroup(isSingleThreaded)
101+
b.createBuildTasks(projects, wg)
102+
wg.RunAndWait()
103+
104+
// Generate the order
105+
b.generateOrder(projects)
106+
107+
return b
108+
}
109+
110+
func (b *buildOrderGenerator) toPath(configName string) tspath.Path {
111+
return tspath.ToPath(configName, b.host.GetCurrentDirectory(), b.host.FS().UseCaseSensitiveFileNames())
112+
}
113+
114+
func (b *buildOrderGenerator) createBuildTasks(projects []string, wg core.WorkGroup) {
115+
for _, project := range projects {
116+
b.createBuildTask(project, wg)
117+
}
118+
}
119+
120+
func (b *buildOrderGenerator) createBuildTask(configName string, wg core.WorkGroup) {
121+
wg.Queue(func() {
122+
path := b.toPath(configName)
123+
task := &buildTask{config: configName}
124+
if _, loaded := b.tasks.LoadOrStore(path, task); loaded {
125+
return
126+
}
127+
task.resolved = b.host.GetResolvedProjectReference(configName, path)
128+
if task.resolved != nil {
129+
b.createBuildTasks(task.resolved.ResolvedProjectReferencePaths(), wg)
130+
}
131+
})
132+
}
133+
134+
func (b *buildOrderGenerator) generateOrder(projects []string) {
135+
completed := collections.Set[tspath.Path]{}
136+
analyzing := collections.Set[tspath.Path]{}
137+
circularityStack := []string{}
138+
for _, project := range projects {
139+
b.analyzeConfig(project, nil, false, &completed, &analyzing, circularityStack)
140+
}
141+
}
142+
143+
func (b *buildOrderGenerator) analyzeConfig(
144+
configName string,
145+
downStream *statusTask,
146+
inCircularContext bool,
147+
completed *collections.Set[tspath.Path],
148+
analyzing *collections.Set[tspath.Path],
149+
circularityStack []string,
150+
) {
151+
path := b.toPath(configName)
152+
task, ok := b.tasks.Load(path)
153+
if !ok {
154+
panic("No build task found for " + configName)
155+
}
156+
if !completed.Has(path) {
157+
if analyzing.Has(path) {
158+
if !inCircularContext {
159+
b.errors = append(b.errors, ast.NewCompilerDiagnostic(
160+
diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0,
161+
strings.Join(circularityStack, "\n"),
162+
))
163+
}
164+
return
165+
}
166+
analyzing.Add(path)
167+
circularityStack = append(circularityStack, configName)
168+
if task.resolved != nil {
169+
for index, subReference := range task.resolved.ResolvedProjectReferencePaths() {
170+
statusTask := statusTask{config: subReference, referencedBy: configName}
171+
task.upStream = append(task.upStream, &statusTask)
172+
b.analyzeConfig(subReference, &statusTask, inCircularContext || task.resolved.ProjectReferences()[index].Circular, completed, analyzing, circularityStack)
173+
}
174+
}
175+
circularityStack = circularityStack[:len(circularityStack)-1]
176+
completed.Add(path)
177+
b.order = append(b.order, configName)
178+
}
179+
if downStream != nil {
180+
task.downStream = append(task.downStream, downStream)
181+
downStream.status = make(chan *upToDateStatus, 1)
182+
}
183+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package execute_test
2+
3+
import (
4+
"fmt"
5+
"slices"
6+
"strings"
7+
"testing"
8+
9+
"github.com/microsoft/typescript-go/internal/compiler"
10+
"github.com/microsoft/typescript-go/internal/core"
11+
"github.com/microsoft/typescript-go/internal/execute"
12+
"github.com/microsoft/typescript-go/internal/tsoptions"
13+
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
14+
"gotest.tools/v3/assert"
15+
)
16+
17+
func TestBuildOrderGenerator(t *testing.T) {
18+
t.Parallel()
19+
testCases := []*buildOrderTestCase{
20+
{"specify two roots", []string{"A", "G"}, []string{"D", "E", "C", "B", "A", "G"}, false},
21+
{"multiple parts of the same graph in various orders", []string{"A"}, []string{"D", "E", "C", "B", "A"}, false},
22+
{"multiple parts of the same graph in various orders", []string{"A", "C", "D"}, []string{"D", "E", "C", "B", "A"}, false},
23+
{"multiple parts of the same graph in various orders", []string{"D", "C", "A"}, []string{"D", "E", "C", "B", "A"}, false},
24+
{"other orderings", []string{"F"}, []string{"E", "F"}, false},
25+
{"other orderings", []string{"E"}, []string{"E"}, false},
26+
{"other orderings", []string{"F", "C", "A"}, []string{"E", "F", "D", "C", "B", "A"}, false},
27+
{"returns circular order", []string{"H"}, []string{"E", "J", "I", "H"}, true},
28+
{"returns circular order", []string{"A", "H"}, []string{"D", "E", "C", "B", "A", "J", "I", "H"}, true},
29+
}
30+
for _, testcase := range testCases {
31+
testcase.run(t)
32+
}
33+
}
34+
35+
type buildOrderTestCase struct {
36+
name string
37+
projects []string
38+
expected []string
39+
circular bool
40+
}
41+
42+
func (b *buildOrderTestCase) configName(project string) string {
43+
return fmt.Sprintf("/home/src/workspaces/project/%s/tsconfig.json", project)
44+
}
45+
46+
func (b *buildOrderTestCase) projectName(config string) string {
47+
str := strings.TrimPrefix(config, "/home/src/workspaces/project/")
48+
str = strings.TrimSuffix(str, "/tsconfig.json")
49+
return str
50+
}
51+
52+
func (b *buildOrderTestCase) run(t *testing.T) {
53+
t.Helper()
54+
t.Run(b.name+" - "+strings.Join(b.projects, ","), func(t *testing.T) {
55+
t.Parallel()
56+
files := make(map[string]string)
57+
deps := map[string][]string{
58+
"A": {"B", "C"},
59+
"B": {"C", "D"},
60+
"C": {"D", "E"},
61+
"F": {"E"},
62+
"H": {"I"},
63+
"I": {"J"},
64+
"J": {"H", "E"},
65+
}
66+
reverseDeps := map[string][]string{}
67+
for project, deps := range deps {
68+
for _, dep := range deps {
69+
reverseDeps[dep] = append(reverseDeps[dep], project)
70+
}
71+
}
72+
for _, project := range []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J"} {
73+
files[fmt.Sprintf("/home/src/workspaces/project/%s/%s.ts", project, project)] = "export {}"
74+
referencesStr := ""
75+
if deps, ok := deps[project]; ok {
76+
referencesStr = fmt.Sprintf(`, "references": [%s]`, strings.Join(core.Map(deps, func(dep string) string {
77+
return fmt.Sprintf(`{ "path": "../%s" }`, dep)
78+
}), ","))
79+
}
80+
files[b.configName(project)] = fmt.Sprintf(`{
81+
"compilerOptions": { "composite": true },
82+
"files": ["./%s.ts"],
83+
%s
84+
}`, project, referencesStr)
85+
}
86+
87+
host := compiler.NewCompilerHost("/home/src/workspaces/project", vfstest.FromMap(files, true), "", nil)
88+
args := append([]string{"--build", "--dry"}, b.projects...)
89+
buildCommand := tsoptions.ParseBuildCommandLine(args, host)
90+
buildOrderGenerator := execute.NewBuildOrderGenerator(buildCommand, host, false)
91+
buildOrder := core.Map(buildOrderGenerator.Order(), b.projectName)
92+
assert.DeepEqual(t, buildOrder, b.expected)
93+
94+
for index, project := range buildOrder {
95+
upstream := core.Map(buildOrderGenerator.Upstream(b.configName(project)), b.projectName)
96+
expectedUpstream := deps[project]
97+
assert.Assert(t, len(upstream) <= len(expectedUpstream), fmt.Sprintf("Expected upstream for %s to be at most %d, got %d", project, len(expectedUpstream), len(upstream)))
98+
for _, expected := range expectedUpstream {
99+
if slices.Contains(buildOrder[:index], expected) {
100+
assert.Assert(t, slices.Contains(upstream, expected), fmt.Sprintf("Expected upstream for %s to contain %s", project, expected))
101+
} else {
102+
assert.Assert(t, !slices.Contains(upstream, expected), fmt.Sprintf("Expected upstream for %s to not contain %s", project, expected))
103+
}
104+
}
105+
106+
downstream := core.Map(buildOrderGenerator.Downstream(b.configName(project)), b.projectName)
107+
expectedDownstream := reverseDeps[project]
108+
assert.Assert(t, len(downstream) <= len(expectedDownstream), fmt.Sprintf("Expected downstream for %s to be at most %d, got %d", project, len(expectedDownstream), len(downstream)))
109+
for _, expected := range expectedDownstream {
110+
if slices.Contains(buildOrder[index+1:], expected) {
111+
assert.Assert(t, slices.Contains(downstream, expected), fmt.Sprintf("Expected downstream for %s to contain %s", project, expected))
112+
} else {
113+
assert.Assert(t, !slices.Contains(downstream, expected), fmt.Sprintf("Expected downstream for %s to not contain %s", project, expected))
114+
}
115+
}
116+
}
117+
118+
if !b.circular {
119+
for project, projectDeps := range deps {
120+
child := b.configName(project)
121+
childIndex := slices.Index(buildOrder, child)
122+
if childIndex == -1 {
123+
continue
124+
}
125+
for _, dep := range projectDeps {
126+
parent := b.configName(dep)
127+
parentIndex := slices.Index(buildOrder, parent)
128+
129+
assert.Assert(t, childIndex > parentIndex, fmt.Sprintf("Expecting child %s to be built after parent %s", project, dep))
130+
}
131+
}
132+
}
133+
})
134+
}

0 commit comments

Comments
 (0)