From f8ceb8e1947114cb671c2f4a2c611ae14395be5f Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Mon, 4 Nov 2024 12:44:11 -0700 Subject: [PATCH] Implement naive resolution of local imports --- .github/workflows/lint.yaml | 4 +- .github/workflows/security.yaml | 4 +- .../caveatmultiplebranchessamerel.yaml | 1 + pkg/composableschemadsl/compiler/compiler.go | 21 +++++ .../compiler/importer-test/README.md | 5 + .../nested-local-with-hop/expected.zed | 6 ++ .../nested-local-with-hop/root.zed | 6 ++ .../transitive/transitive.zed | 1 + .../transitive/user/user.zed | 1 + .../importer-test/nested-local/expected.zed | 6 ++ .../importer-test/nested-local/root.zed | 6 ++ .../importer-test/nested-local/user/user.zed | 1 + .../definitions/user/user.zed | 1 + .../nested-two-layer-local/expected.zed | 6 ++ .../nested-two-layer-local/root.zed | 6 ++ .../simple-local-with-hop/expected.zed | 6 ++ .../simple-local-with-hop/indirection.zed | 1 + .../simple-local-with-hop/root.zed | 6 ++ .../simple-local-with-hop/user.zed | 1 + .../importer-test/simple-local/expected.zed | 6 ++ .../importer-test/simple-local/root.zed | 6 ++ .../importer-test/simple-local/user.zed | 1 + pkg/composableschemadsl/compiler/importer.go | 52 +++++++++++ .../compiler/importer_test.go | 91 +++++++++++++++++++ .../compiler/translator.go | 74 ++++++++++++--- 25 files changed, 301 insertions(+), 18 deletions(-) create mode 100644 pkg/composableschemadsl/compiler/importer-test/README.md create mode 100644 pkg/composableschemadsl/compiler/importer-test/nested-local-with-hop/expected.zed create mode 100644 pkg/composableschemadsl/compiler/importer-test/nested-local-with-hop/root.zed create mode 100644 pkg/composableschemadsl/compiler/importer-test/nested-local-with-hop/transitive/transitive.zed create mode 100644 pkg/composableschemadsl/compiler/importer-test/nested-local-with-hop/transitive/user/user.zed create mode 100644 pkg/composableschemadsl/compiler/importer-test/nested-local/expected.zed create mode 100644 pkg/composableschemadsl/compiler/importer-test/nested-local/root.zed create mode 100644 pkg/composableschemadsl/compiler/importer-test/nested-local/user/user.zed create mode 100644 pkg/composableschemadsl/compiler/importer-test/nested-two-layer-local/definitions/user/user.zed create mode 100644 pkg/composableschemadsl/compiler/importer-test/nested-two-layer-local/expected.zed create mode 100644 pkg/composableschemadsl/compiler/importer-test/nested-two-layer-local/root.zed create mode 100644 pkg/composableschemadsl/compiler/importer-test/simple-local-with-hop/expected.zed create mode 100644 pkg/composableschemadsl/compiler/importer-test/simple-local-with-hop/indirection.zed create mode 100644 pkg/composableschemadsl/compiler/importer-test/simple-local-with-hop/root.zed create mode 100644 pkg/composableschemadsl/compiler/importer-test/simple-local-with-hop/user.zed create mode 100644 pkg/composableschemadsl/compiler/importer-test/simple-local/expected.zed create mode 100644 pkg/composableschemadsl/compiler/importer-test/simple-local/root.zed create mode 100644 pkg/composableschemadsl/compiler/importer-test/simple-local/user.zed create mode 100644 pkg/composableschemadsl/compiler/importer.go create mode 100644 pkg/composableschemadsl/compiler/importer_test.go diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 30cead0369..f03f420c0c 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -1,6 +1,6 @@ --- name: "Lint" -on: # yamllint disable-line rule:truthy +on: # yamllint disable-line rule:truthy push: branches: - "!dependabot/*" @@ -20,7 +20,7 @@ jobs: - name: "Check Licenses" uses: "authzed/actions/go-license-check@main" with: - ignore: "buf.build" # Has no license information + ignore: "buf.build" # Has no license information go-lint: name: "Lint Go" diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml index 82edb83552..aadfcadeec 100644 --- a/.github/workflows/security.yaml +++ b/.github/workflows/security.yaml @@ -1,6 +1,6 @@ --- name: "Security" -on: # yamllint disable-line rule:truthy +on: # yamllint disable-line rule:truthy push: branches: - "!dependabot/*" @@ -16,7 +16,7 @@ env: jobs: codeql: name: "CodeQL Analyze" - if: "${{ github.event_name == 'pull_request' }}" # workaround to https://github.com/github/codeql-action/issues/1537 + if: "${{ github.event_name == 'pull_request' }}" # workaround to https://github.com/github/codeql-action/issues/1537 runs-on: "buildjet-8vcpu-ubuntu-2204" timeout-minutes: "${{ (matrix.language == 'swift' && 120) || 360 }}" permissions: diff --git a/internal/services/integrationtesting/testconfigs/caveatmultiplebranchessamerel.yaml b/internal/services/integrationtesting/testconfigs/caveatmultiplebranchessamerel.yaml index 74bd3912d9..79871d6f7d 100644 --- a/internal/services/integrationtesting/testconfigs/caveatmultiplebranchessamerel.yaml +++ b/internal/services/integrationtesting/testconfigs/caveatmultiplebranchessamerel.yaml @@ -1,3 +1,4 @@ +--- schema: >- definition user {} diff --git a/pkg/composableschemadsl/compiler/compiler.go b/pkg/composableschemadsl/compiler/compiler.go index 0570f91b3c..e238dbe4a8 100644 --- a/pkg/composableschemadsl/compiler/compiler.go +++ b/pkg/composableschemadsl/compiler/compiler.go @@ -9,6 +9,7 @@ import ( "github.com/authzed/spicedb/pkg/composableschemadsl/dslshape" "github.com/authzed/spicedb/pkg/composableschemadsl/input" "github.com/authzed/spicedb/pkg/composableschemadsl/parser" + "github.com/authzed/spicedb/pkg/genutil/mapz" core "github.com/authzed/spicedb/pkg/proto/core/v1" ) @@ -52,28 +53,46 @@ func (cs CompiledSchema) SourcePositionToRunePosition(source input.Source, posit type config struct { skipValidation bool objectTypePrefix *string + // In an import context, this is the folder containing + // the importing schema (as opposed to imported schemas) + sourceFolder string } func SkipValidation() Option { return func(cfg *config) { cfg.skipValidation = true } } +// Config for the prefix attached to all definitions, such as in Serverless +// where it's `someorganization/` in front of each definition. func ObjectTypePrefix(prefix string) ObjectPrefixOption { return func(cfg *config) { cfg.objectTypePrefix = &prefix } } +// Config that does not supply the prefix but requires the prefix on all objects. func RequirePrefixedObjectType() ObjectPrefixOption { return func(cfg *config) { cfg.objectTypePrefix = nil } } +// Config that allows for no prefix. This is the "normal" default. func AllowUnprefixedObjectType() ObjectPrefixOption { return func(cfg *config) { cfg.objectTypePrefix = new(string) } } +// Config that supplies the root source folder for compilation. Required +// for relative import syntax to work properly. +func SourceFolder(sourceFolder string) Option { + return func(cfg *config) { cfg.sourceFolder = sourceFolder } +} + type Option func(*config) type ObjectPrefixOption func(*config) // Compile compilers the input schema into a set of namespace definition protos. func Compile(schema InputSchema, prefix ObjectPrefixOption, opts ...Option) (*CompiledSchema, error) { + names := mapz.NewSet[string]() + return compileImpl(schema, names, prefix, opts...) +} + +func compileImpl(schema InputSchema, existingNames *mapz.Set[string], prefix ObjectPrefixOption, opts ...Option) (*CompiledSchema, error) { cfg := &config{} prefix(cfg) // required option @@ -94,6 +113,8 @@ func Compile(schema InputSchema, prefix ObjectPrefixOption, opts ...Option) (*Co mapper: mapper, schemaString: schema.SchemaString, skipValidate: cfg.skipValidation, + sourceFolder: cfg.sourceFolder, + existingNames: existingNames, }, root) if err != nil { var errorWithNode errorWithNode diff --git a/pkg/composableschemadsl/compiler/importer-test/README.md b/pkg/composableschemadsl/compiler/importer-test/README.md new file mode 100644 index 0000000000..0cd2f1fc86 --- /dev/null +++ b/pkg/composableschemadsl/compiler/importer-test/README.md @@ -0,0 +1,5 @@ +# Test Structure + +Every folder should have a `root.zed`. This is the target for compilation. + +Every folder will have an `expected.zed`, which is the output of the compilation process. diff --git a/pkg/composableschemadsl/compiler/importer-test/nested-local-with-hop/expected.zed b/pkg/composableschemadsl/compiler/importer-test/nested-local-with-hop/expected.zed new file mode 100644 index 0000000000..b7f82df684 --- /dev/null +++ b/pkg/composableschemadsl/compiler/importer-test/nested-local-with-hop/expected.zed @@ -0,0 +1,6 @@ +definition user {} + +definition resource { + relation viewer: user + permission view = viewer +} \ No newline at end of file diff --git a/pkg/composableschemadsl/compiler/importer-test/nested-local-with-hop/root.zed b/pkg/composableschemadsl/compiler/importer-test/nested-local-with-hop/root.zed new file mode 100644 index 0000000000..b46fc85bd4 --- /dev/null +++ b/pkg/composableschemadsl/compiler/importer-test/nested-local-with-hop/root.zed @@ -0,0 +1,6 @@ +from .transitive.transitive import user + +definition resource { + relation viewer: user + permission view = viewer +} diff --git a/pkg/composableschemadsl/compiler/importer-test/nested-local-with-hop/transitive/transitive.zed b/pkg/composableschemadsl/compiler/importer-test/nested-local-with-hop/transitive/transitive.zed new file mode 100644 index 0000000000..9e48edbbc6 --- /dev/null +++ b/pkg/composableschemadsl/compiler/importer-test/nested-local-with-hop/transitive/transitive.zed @@ -0,0 +1 @@ +from .user.user import user diff --git a/pkg/composableschemadsl/compiler/importer-test/nested-local-with-hop/transitive/user/user.zed b/pkg/composableschemadsl/compiler/importer-test/nested-local-with-hop/transitive/user/user.zed new file mode 100644 index 0000000000..16fbd0019d --- /dev/null +++ b/pkg/composableschemadsl/compiler/importer-test/nested-local-with-hop/transitive/user/user.zed @@ -0,0 +1 @@ +definition user {} diff --git a/pkg/composableschemadsl/compiler/importer-test/nested-local/expected.zed b/pkg/composableschemadsl/compiler/importer-test/nested-local/expected.zed new file mode 100644 index 0000000000..b7f82df684 --- /dev/null +++ b/pkg/composableschemadsl/compiler/importer-test/nested-local/expected.zed @@ -0,0 +1,6 @@ +definition user {} + +definition resource { + relation viewer: user + permission view = viewer +} \ No newline at end of file diff --git a/pkg/composableschemadsl/compiler/importer-test/nested-local/root.zed b/pkg/composableschemadsl/compiler/importer-test/nested-local/root.zed new file mode 100644 index 0000000000..73ecf0a3a5 --- /dev/null +++ b/pkg/composableschemadsl/compiler/importer-test/nested-local/root.zed @@ -0,0 +1,6 @@ +from .user.user import user + +definition resource { + relation viewer: user + permission view = viewer +} diff --git a/pkg/composableschemadsl/compiler/importer-test/nested-local/user/user.zed b/pkg/composableschemadsl/compiler/importer-test/nested-local/user/user.zed new file mode 100644 index 0000000000..16fbd0019d --- /dev/null +++ b/pkg/composableschemadsl/compiler/importer-test/nested-local/user/user.zed @@ -0,0 +1 @@ +definition user {} diff --git a/pkg/composableschemadsl/compiler/importer-test/nested-two-layer-local/definitions/user/user.zed b/pkg/composableschemadsl/compiler/importer-test/nested-two-layer-local/definitions/user/user.zed new file mode 100644 index 0000000000..16fbd0019d --- /dev/null +++ b/pkg/composableschemadsl/compiler/importer-test/nested-two-layer-local/definitions/user/user.zed @@ -0,0 +1 @@ +definition user {} diff --git a/pkg/composableschemadsl/compiler/importer-test/nested-two-layer-local/expected.zed b/pkg/composableschemadsl/compiler/importer-test/nested-two-layer-local/expected.zed new file mode 100644 index 0000000000..b7f82df684 --- /dev/null +++ b/pkg/composableschemadsl/compiler/importer-test/nested-two-layer-local/expected.zed @@ -0,0 +1,6 @@ +definition user {} + +definition resource { + relation viewer: user + permission view = viewer +} \ No newline at end of file diff --git a/pkg/composableschemadsl/compiler/importer-test/nested-two-layer-local/root.zed b/pkg/composableschemadsl/compiler/importer-test/nested-two-layer-local/root.zed new file mode 100644 index 0000000000..bd6130372b --- /dev/null +++ b/pkg/composableschemadsl/compiler/importer-test/nested-two-layer-local/root.zed @@ -0,0 +1,6 @@ +from .definitions.user.user import user + +definition resource { + relation viewer: user + permission view = viewer +} diff --git a/pkg/composableschemadsl/compiler/importer-test/simple-local-with-hop/expected.zed b/pkg/composableschemadsl/compiler/importer-test/simple-local-with-hop/expected.zed new file mode 100644 index 0000000000..b7f82df684 --- /dev/null +++ b/pkg/composableschemadsl/compiler/importer-test/simple-local-with-hop/expected.zed @@ -0,0 +1,6 @@ +definition user {} + +definition resource { + relation viewer: user + permission view = viewer +} \ No newline at end of file diff --git a/pkg/composableschemadsl/compiler/importer-test/simple-local-with-hop/indirection.zed b/pkg/composableschemadsl/compiler/importer-test/simple-local-with-hop/indirection.zed new file mode 100644 index 0000000000..a4b1c018cf --- /dev/null +++ b/pkg/composableschemadsl/compiler/importer-test/simple-local-with-hop/indirection.zed @@ -0,0 +1 @@ +from .user import user diff --git a/pkg/composableschemadsl/compiler/importer-test/simple-local-with-hop/root.zed b/pkg/composableschemadsl/compiler/importer-test/simple-local-with-hop/root.zed new file mode 100644 index 0000000000..ee43f81792 --- /dev/null +++ b/pkg/composableschemadsl/compiler/importer-test/simple-local-with-hop/root.zed @@ -0,0 +1,6 @@ +from .indirection import user + +definition resource { + relation viewer: user + permission view = viewer +} diff --git a/pkg/composableschemadsl/compiler/importer-test/simple-local-with-hop/user.zed b/pkg/composableschemadsl/compiler/importer-test/simple-local-with-hop/user.zed new file mode 100644 index 0000000000..16fbd0019d --- /dev/null +++ b/pkg/composableschemadsl/compiler/importer-test/simple-local-with-hop/user.zed @@ -0,0 +1 @@ +definition user {} diff --git a/pkg/composableschemadsl/compiler/importer-test/simple-local/expected.zed b/pkg/composableschemadsl/compiler/importer-test/simple-local/expected.zed new file mode 100644 index 0000000000..b7f82df684 --- /dev/null +++ b/pkg/composableschemadsl/compiler/importer-test/simple-local/expected.zed @@ -0,0 +1,6 @@ +definition user {} + +definition resource { + relation viewer: user + permission view = viewer +} \ No newline at end of file diff --git a/pkg/composableschemadsl/compiler/importer-test/simple-local/root.zed b/pkg/composableschemadsl/compiler/importer-test/simple-local/root.zed new file mode 100644 index 0000000000..321bccb119 --- /dev/null +++ b/pkg/composableschemadsl/compiler/importer-test/simple-local/root.zed @@ -0,0 +1,6 @@ +from .user import user + +definition resource { + relation viewer: user + permission view = viewer +} diff --git a/pkg/composableschemadsl/compiler/importer-test/simple-local/user.zed b/pkg/composableschemadsl/compiler/importer-test/simple-local/user.zed new file mode 100644 index 0000000000..16fbd0019d --- /dev/null +++ b/pkg/composableschemadsl/compiler/importer-test/simple-local/user.zed @@ -0,0 +1 @@ +definition user {} diff --git a/pkg/composableschemadsl/compiler/importer.go b/pkg/composableschemadsl/compiler/importer.go new file mode 100644 index 0000000000..4aad96ccc9 --- /dev/null +++ b/pkg/composableschemadsl/compiler/importer.go @@ -0,0 +1,52 @@ +package compiler + +import ( + "fmt" + "os" + "path" + "path/filepath" + + "github.com/rs/zerolog/log" + + "github.com/authzed/spicedb/pkg/composableschemadsl/input" + "github.com/authzed/spicedb/pkg/genutil/mapz" +) + +type importContext struct { + pathSegments []string + sourceFolder string + names *mapz.Set[string] +} + +const SchemaFileSuffix = ".zed" + +func importFile(importContext importContext) (*CompiledSchema, error) { + relativeFilepath := constructFilePath(importContext.pathSegments) + filePath := path.Join(importContext.sourceFolder, relativeFilepath) + + newSourceFolder := filepath.Dir(filePath) + + var schemaBytes []byte + schemaBytes, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read schema file: %w", err) + } + log.Trace().Str("schema", string(schemaBytes)).Str("file", filePath).Msg("read schema from file") + + compiled, err := compileImpl(InputSchema{ + Source: input.Source(filePath), + SchemaString: string(schemaBytes), + }, + importContext.names, + AllowUnprefixedObjectType(), + SourceFolder(newSourceFolder), + ) + if err != nil { + return nil, err + } + return compiled, nil +} + +func constructFilePath(segments []string) string { + return path.Join(segments...) + SchemaFileSuffix +} diff --git a/pkg/composableschemadsl/compiler/importer_test.go b/pkg/composableschemadsl/compiler/importer_test.go new file mode 100644 index 0000000000..03475359e4 --- /dev/null +++ b/pkg/composableschemadsl/compiler/importer_test.go @@ -0,0 +1,91 @@ +package compiler_test + +import ( + "fmt" + "os" + "path" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/authzed/spicedb/pkg/composableschemadsl/compiler" + "github.com/authzed/spicedb/pkg/composableschemadsl/generator" + "github.com/authzed/spicedb/pkg/composableschemadsl/input" +) + +type importerTest struct { + name string + folder string +} + +func (it *importerTest) input() string { + b, err := os.ReadFile(fmt.Sprintf("importer-test/%s/root.zed", it.folder)) + if err != nil { + panic(err) + } + + return string(b) +} + +func (it *importerTest) relativePath() string { + return fmt.Sprintf("importer-test/%s", it.folder) +} + +func (it *importerTest) expected() string { + b, err := os.ReadFile(fmt.Sprintf("importer-test/%s/expected.zed", it.folder)) + if err != nil { + panic(err) + } + + return string(b) +} + +func (it *importerTest) writeExpected(schema string) { + err := os.WriteFile(fmt.Sprintf("importer-test/%s/expected.zed", it.folder), []byte(schema), 0o_600) + if err != nil { + panic(err) + } +} + +func TestImporter(t *testing.T) { + workingDir, err := os.Getwd() + require.NoError(t, err) + + importerTests := []importerTest{ + {"simple local import", "simple-local"}, + {"simple local import with transitive hop", "simple-local-with-hop"}, + {"nested local import", "nested-local"}, + {"nested local import with transitive hop", "nested-local-with-hop"}, + {"nested local two layers deep import", "nested-two-layer-local"}, + } + + for _, test := range importerTests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + sourceFolder := path.Join(workingDir, test.relativePath()) + + inputSchema := test.input() + + compiled, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: inputSchema, + }, compiler.AllowUnprefixedObjectType(), + compiler.SourceFolder(sourceFolder)) + require.NoError(t, err) + + generated, _, err := generator.GenerateSchema(compiled.OrderedDefinitions) + require.NoError(t, err) + + if os.Getenv("REGEN") == "true" { + test.writeExpected(generated) + } else { + expected := test.expected() + if !assert.Equal(t, expected, generated, test.name) { + t.Log(generated) + } + } + }) + } +} diff --git a/pkg/composableschemadsl/compiler/translator.go b/pkg/composableschemadsl/compiler/translator.go index 638eb8033a..66fd63a810 100644 --- a/pkg/composableschemadsl/compiler/translator.go +++ b/pkg/composableschemadsl/compiler/translator.go @@ -7,6 +7,7 @@ import ( "github.com/ccoveille/go-safecast" "github.com/jzelinskie/stringz" + "github.com/rs/zerolog/log" "github.com/authzed/spicedb/pkg/caveats" caveattypes "github.com/authzed/spicedb/pkg/caveats/types" @@ -22,6 +23,8 @@ type translationContext struct { mapper input.PositionMapper schemaString string skipValidate bool + existingNames *mapz.Set[string] + sourceFolder string } func (tctx translationContext) prefixedPath(definitionName string) (string, error) { @@ -48,36 +51,56 @@ func translate(tctx translationContext, root *dslNode) (*CompiledSchema, error) var objectDefinitions []*core.NamespaceDefinition var caveatDefinitions []*core.CaveatDefinition - names := mapz.NewSet[string]() + // Copy the name set so that we're not mutating the parent's context + // as we do our walk + names := tctx.existingNames.Copy() - for _, definitionNode := range root.GetChildren() { - var definition SchemaDefinition - - switch definitionNode.GetType() { + for _, topLevelNode := range root.GetChildren() { + switch topLevelNode.GetType() { case dslshape.NodeTypeCaveatDefinition: - def, err := translateCaveatDefinition(tctx, definitionNode) + log.Trace().Msg("adding caveat definition") + // TODO: Maybe refactor these in terms of a generic function? + def, err := translateCaveatDefinition(tctx, topLevelNode) if err != nil { return nil, err } - definition = def caveatDefinitions = append(caveatDefinitions, def) + name := def.GetName() + if !names.Add(name) { + return nil, topLevelNode.ErrorWithSourcef(name, "found name reused between multiple definitions and/or caveats: %s", name) + } + + orderedDefinitions = append(orderedDefinitions, def) + case dslshape.NodeTypeDefinition: - def, err := translateObjectDefinition(tctx, definitionNode) + log.Trace().Msg("adding object definition") + def, err := translateObjectDefinition(tctx, topLevelNode) if err != nil { return nil, err } - definition = def objectDefinitions = append(objectDefinitions, def) - } - if !names.Add(definition.GetName()) { - return nil, definitionNode.ErrorWithSourcef(definition.GetName(), "found name reused between multiple definitions and/or caveats: %s", definition.GetName()) - } + name := def.GetName() + if !names.Add(name) { + return nil, topLevelNode.ErrorWithSourcef(name, "found name reused between multiple definitions and/or caveats: %s", name) + } + + orderedDefinitions = append(orderedDefinitions, def) - orderedDefinitions = append(orderedDefinitions, definition) + case dslshape.NodeTypeImport: + compiled, err := translateImport(tctx, topLevelNode, names) + if err != nil { + return nil, err + } + // NOTE: name collision validation happens in the recursive compilation process, + // so we don't need an explicit check here and we can just add our definitions. + caveatDefinitions = append(caveatDefinitions, compiled.CaveatDefinitions...) + objectDefinitions = append(objectDefinitions, compiled.ObjectDefinitions...) + orderedDefinitions = append(orderedDefinitions, compiled.OrderedDefinitions...) + } } return &CompiledSchema{ @@ -669,3 +692,26 @@ func addWithCaveats(tctx translationContext, typeRefNode *dslNode, ref *core.All } return nil } + +func translateImport(tctx translationContext, importNode *dslNode, names *mapz.Set[string]) (*CompiledSchema, error) { + // NOTE: this function currently just grabs everything that's in the target file. + // TODO: only grab the requested definitions + // TODO: import cycle tracking + pathNodes := importNode.List(dslshape.NodeImportPredicatePathSegment) + pathSegments := make([]string, 0, len(pathNodes)) + + // Get the filepath segments out of the AST nodes + for _, pathSegmentNode := range pathNodes { + segment, err := pathSegmentNode.GetString(dslshape.NodeIdentiferPredicateValue) + if err != nil { + return nil, err + } + pathSegments = append(pathSegments, segment) + } + + return importFile(importContext{ + names: names, + pathSegments: pathSegments, + sourceFolder: tctx.sourceFolder, + }) +}