diff --git a/d2ir/import.go b/d2ir/import.go index 800d5322e..91a6fb986 100644 --- a/d2ir/import.go +++ b/d2ir/import.go @@ -3,6 +3,7 @@ package d2ir import ( "bufio" "path" + "strings" "oss.terrastruct.com/d2/d2ast" "oss.terrastruct.com/d2/d2parser" @@ -15,9 +16,9 @@ func (c *compiler) pushImportStack(imp *d2ast.Import) bool { } newPath := imp.Path[0].Unbox().ScalarString() - for _, p := range c.importStack { + for i, p := range c.importStack { if newPath == p { - c.errorf(imp, "detected cyclic import of %q", newPath) + c.errorf(imp, "detected cyclic import chain: %s", formatCyclicChain(c.importStack[i:])) return false } } @@ -30,6 +31,16 @@ func (c *compiler) popImportStack() { c.importStack = c.importStack[:len(c.importStack)-1] } +func formatCyclicChain(cyclicChain []string) string { + var b strings.Builder + for _, p := range cyclicChain { + b.WriteString(p) + b.WriteString(" -> ") + } + b.WriteString(cyclicChain[0]) + return b.String() +} + // Returns either *Map or *Field. func (c *compiler) _import(imp *d2ast.Import) (Node, bool) { ir, ok := c.__import(imp) diff --git a/d2ir/import_test.go b/d2ir/import_test.go index 856975af4..1fc5caffb 100644 --- a/d2ir/import_test.go +++ b/d2ir/import_test.go @@ -3,8 +3,9 @@ package d2ir_test import ( "testing" - "oss.terrastruct.com/d2/d2ir" "oss.terrastruct.com/util-go/assert" + + "oss.terrastruct.com/d2/d2ir" ) func testCompileImports(t *testing.T) { @@ -89,7 +90,25 @@ label: meow`, t.Run("errors", func(t *testing.T) { tca := []testCase{ { - name: "parse_error", + name: "not_exist", + run: func(t testing.TB) { + _, err := compileFS(t, "index.d2", map[string]string{ + "index.d2": "...@x.d2", + }) + assert.ErrorString(t, err, `index.d2:1:1: failed to import "x.d2": open x.d2: no such file or directory`) + }, + }, + { + name: "absolute", + run: func(t testing.TB) { + _, err := compileFS(t, "index.d2", map[string]string{ + "index.d2": "...@/x.d2", + }) + assert.ErrorString(t, err, `index.d2:1:1: import paths must be relative`) + }, + }, + { + name: "parse", run: func(t testing.TB) { _, err := compileFS(t, "index.d2", map[string]string{ "index.d2": "...@x.d2", @@ -103,6 +122,18 @@ x.d2:1:6: connection missing destination x.d2:1:7: connection missing source`) }, }, + { + name: "cyclic", + run: func(t testing.TB) { + _, err := compileFS(t, "index.d2", map[string]string{ + "index.d2": "...@x", + "x.d2": "...@y", + "y.d2": "...@q", + "q.d2": "...@x", + }) + assert.ErrorString(t, err, `q.d2:1:1: detected cyclic import chain: x -> y -> q -> x`) + }, + }, } runa(t, tca) })