diff --git a/.structlint.yaml b/.structlint.yaml index a4466a4..51272e9 100644 --- a/.structlint.yaml +++ b/.structlint.yaml @@ -134,3 +134,6 @@ ignore: - "*.tmp" - "*.temp" - "structlint" + + # Test fixtures — deliberately violate rules to exercise the validator + - "test/testdata" diff --git a/internal/cli/validate.go b/internal/cli/validate.go index 9cce482..a67afff 100644 --- a/internal/cli/validate.go +++ b/internal/cli/validate.go @@ -95,13 +95,7 @@ func NewValidateCmd() *cli.Command { if staged || cmd.Bool("changed-only") { v.LoadChangedPathsMode(path, staged) } - v.ValidateDirStructure(path) - v.ValidateFileNaming(path) - v.ValidateRequiredPaths(path) - v.ValidateRequiredFiles(path) - v.ValidatePlacement(path) - v.ValidateRequiredGroups(path) - v.ValidateBoundaries(path) + v.Run(path) if baseline := cmd.String("baseline"); baseline != "" { if err := v.ApplyBaseline(baseline); err != nil { diff --git a/internal/validator/engine.go b/internal/validator/engine.go new file mode 100644 index 0000000..7566847 --- /dev/null +++ b/internal/validator/engine.go @@ -0,0 +1,115 @@ +package validator + +import "github.com/AxeForging/structlint/internal/config" + +// Rule is the pluggable unit of validation. Each family (dir structure, +// file naming, placement, ...) is a Rule. Rules run in the order Registry +// returns them; that order matches the legacy Validate* invocation +// sequence and is part of the observable contract (spec 005). +type Rule interface { + Name() string + Run(ctx *RunContext) +} + +// RunContext bundles the state a rule needs. +type RunContext struct { + Cfg *config.Config + Tree *Tree + V *Validator + // Skip returns true when the given relative path (dir or file) is out + // of scope for --changed-only / --staged. Rules that are meant to be + // global (required-* families) do not consult Skip. + Skip func(relPath string, isDir bool) bool +} + +// Registry returns the seven rules in legacy execution order: +// dir_structure, file_naming, required_paths, required_files, +// placement, required_groups, boundaries. Spec 010's suggest command +// enumerates rules through this registry. +func Registry(cfg *config.Config) []Rule { + return []Rule{ + dirStructureRule{}, + fileNamingRule{}, + requiredPathsRule{}, + requiredFilesRule{}, + placementRule{}, + requiredGroupsRule{}, + boundariesRule{}, + } +} + +// Run snapshots the tree once, then executes each rule in Registry order +// against the same snapshot. Wire from validate.go instead of calling the +// seven Validate* methods individually. +func (v *Validator) Run(path string) { + tree := Snapshot(path, v.Config.Ignore) + ctx := &RunContext{ + Cfg: v.Config, + Tree: tree, + V: v, + Skip: func(rel string, isDir bool) bool { + if isDir { + return v.shouldSkipChangedDir(rel) + } + return v.shouldSkipChanged(rel) + }, + } + for _, rule := range Registry(v.Config) { + rule.Run(ctx) + } +} + +// The seven rule types below are thin wrappers around the existing +// Validate* method bodies. They call the legacy method, which walks the +// tree itself. Consolidating the walks is a follow-up refactor (see spec +// 005's non-goals); this iteration lands the enumerable Registry / Tree +// surface that specs 009 and 010 depend on without touching output. + +type dirStructureRule struct{} + +func (dirStructureRule) Name() string { return "dir_structure" } +func (dirStructureRule) Run(ctx *RunContext) { + ctx.V.ValidateDirStructure(ctx.Tree.Root) +} + +type fileNamingRule struct{} + +func (fileNamingRule) Name() string { return "file_naming" } +func (fileNamingRule) Run(ctx *RunContext) { + ctx.V.ValidateFileNaming(ctx.Tree.Root) +} + +type requiredPathsRule struct{} + +func (requiredPathsRule) Name() string { return "required_paths" } +func (requiredPathsRule) Run(ctx *RunContext) { + ctx.V.ValidateRequiredPaths(ctx.Tree.Root) +} + +type requiredFilesRule struct{} + +func (requiredFilesRule) Name() string { return "required_files" } +func (requiredFilesRule) Run(ctx *RunContext) { + ctx.V.ValidateRequiredFiles(ctx.Tree.Root) +} + +type placementRule struct{} + +func (placementRule) Name() string { return "placement" } +func (placementRule) Run(ctx *RunContext) { + ctx.V.ValidatePlacement(ctx.Tree.Root) +} + +type requiredGroupsRule struct{} + +func (requiredGroupsRule) Name() string { return "required_groups" } +func (requiredGroupsRule) Run(ctx *RunContext) { + ctx.V.ValidateRequiredGroups(ctx.Tree.Root) +} + +type boundariesRule struct{} + +func (boundariesRule) Name() string { return "boundaries" } +func (boundariesRule) Run(ctx *RunContext) { + ctx.V.ValidateBoundaries(ctx.Tree.Root) +} diff --git a/internal/validator/walk.go b/internal/validator/walk.go new file mode 100644 index 0000000..b7801cf --- /dev/null +++ b/internal/validator/walk.go @@ -0,0 +1,91 @@ +package validator + +import ( + "os" + "path/filepath" +) + +// Entry describes a single filesystem entry visited by Snapshot. +type Entry struct { + RelPath string // slash-normalized; "." for the root + Name string + IsDir bool + Abs string +} + +// Tree is an ignore-filtered snapshot of a project root, ready for +// rule enumeration without another filesystem walk. +type Tree struct { + Root string // absolute root path + Entries []Entry // filepath.Walk lexical order, ignore-filtered + dirs map[string]struct{} + files map[string]struct{} + WalkErr error // first walk error, if any +} + +// HasDir reports whether the tree contains a directory at relPath. +func (t *Tree) HasDir(relPath string) bool { + if t == nil { + return false + } + _, ok := t.dirs[relPath] + return ok +} + +// HasFile reports whether the tree contains a file at relPath. +func (t *Tree) HasFile(relPath string) bool { + if t == nil { + return false + } + _, ok := t.files[relPath] + return ok +} + +// Snapshot walks root once and returns an ignore-filtered Tree. Ignore +// matching uses the same pathMatches helper the individual rule walks use, +// so the snapshot's visible set matches the union of what those walks would +// visit. Rules that intentionally look into ignored directories (see the +// requiredPathsRule os.Stat quirk documented in spec 005) do NOT consult +// the tree for those lookups. +func Snapshot(root string, ignore []string) *Tree { + cleaned := cleanRoot(root) + tree := &Tree{ + Root: cleaned, + dirs: map[string]struct{}{}, + files: map[string]struct{}{}, + } + err := filepath.Walk(root, func(currentPath string, info os.FileInfo, walkErr error) error { + if walkErr != nil { + if tree.WalkErr == nil { + tree.WalkErr = walkErr + } + return walkErr + } + rel := relativePath(cleaned, currentPath) + for _, pat := range ignore { + if pathMatches(rel, pat) { + if info.IsDir() { + return filepath.SkipDir + } + return nil + } + } + entry := Entry{ + RelPath: rel, + Name: info.Name(), + IsDir: info.IsDir(), + Abs: currentPath, + } + tree.Entries = append(tree.Entries, entry) + if info.IsDir() { + tree.dirs[rel] = struct{}{} + } else { + tree.files[rel] = struct{}{} + } + return nil + }) + if err != nil && tree.WalkErr == nil { + tree.WalkErr = err + } + return tree +} diff --git a/test/engine_parity_test.go b/test/engine_parity_test.go new file mode 100644 index 0000000..61cd7b2 --- /dev/null +++ b/test/engine_parity_test.go @@ -0,0 +1,102 @@ +package test + +import ( + "os" + "path/filepath" + "strconv" + "strings" + "testing" +) + +// parityFixtures returns the fixture names under test/testdata/parity/, +// excluding non-fixture entries. +func parityFixtures(t *testing.T) []string { + t.Helper() + dir := filepath.Join(repoRoot(t), "test", "testdata", "parity") + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("read parity dir: %v", err) + } + var out []string + for _, e := range entries { + if !e.IsDir() || e.Name() == "goldens" { + continue + } + if _, err := os.Stat(filepath.Join(dir, e.Name(), ".structlint.yaml")); err != nil { + continue + } + out = append(out, e.Name()) + } + return out +} + +// runFixture invokes the binary against a fixture and returns stdout+stderr +// combined and the exit code. Uses combined output because the pre-refactor +// binary intermixes summary text and error lines; goldens capture that mix. +func runFixture(t *testing.T, bin, name string, extra ...string) (string, int) { + t.Helper() + root := repoRoot(t) + fixture := filepath.Join(root, "test", "testdata", "parity", name) + config := filepath.Join(fixture, ".structlint.yaml") + args := []string{"validate", "--path", fixture, "--config", config} + args = append(args, extra...) + out, err := runBinary(t, bin, args...) + code := 0 + if err != nil { + code = 1 // urfave/cli returns 1 on Action error; goldens confirm this + } + return out, code +} + +func readGolden(t *testing.T, kind, name string) string { + t.Helper() + path := filepath.Join(repoRoot(t), "test", "testdata", "parity", "goldens", name+"."+kind) + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read golden %s: %v", path, err) + } + return string(data) +} + +func TestEngineParity_TextOutput(t *testing.T) { + bin := buildBinary(t) + for _, name := range parityFixtures(t) { + t.Run(name, func(t *testing.T) { + got, _ := runFixture(t, bin, name) + want := readGolden(t, "text", name) + if got != want { + t.Errorf("text output drift for fixture %q\n--- want ---\n%s\n--- got ---\n%s", name, want, got) + } + }) + } +} + +func TestEngineParity_JSONOutput(t *testing.T) { + bin := buildBinary(t) + for _, name := range parityFixtures(t) { + t.Run(name, func(t *testing.T) { + got, _ := runFixture(t, bin, name, "--format", "json") + want := readGolden(t, "json", name) + if got != want { + t.Errorf("json output drift for fixture %q\n--- want ---\n%s\n--- got ---\n%s", name, want, got) + } + }) + } +} + +func TestEngineParity_ExitCodes(t *testing.T) { + bin := buildBinary(t) + for _, name := range parityFixtures(t) { + t.Run(name, func(t *testing.T) { + _, code := runFixture(t, bin, name) + want := strings.TrimSpace(readGolden(t, "exit", name)) + wantCode, err := strconv.Atoi(want) + if err != nil { + t.Fatalf("bad exit golden: %v", err) + } + if code != wantCode { + t.Errorf("exit code drift for %q: got %d, want %d", name, code, wantCode) + } + }) + } +} diff --git a/test/project_validation_test.go b/test/project_validation_test.go index 93424a6..893d810 100644 --- a/test/project_validation_test.go +++ b/test/project_validation_test.go @@ -62,6 +62,7 @@ ignore: - "dist" - "*.log" - "*.tmp" + - "testdata" ` // Create temporary config file diff --git a/test/self_validation_test.go b/test/self_validation_test.go index 26ff9d1..6198b17 100644 --- a/test/self_validation_test.go +++ b/test/self_validation_test.go @@ -151,8 +151,11 @@ func TestProjectStandardsCompliance(t *testing.T) { return err } - // Skip directories + // Skip directories, but prune whole subtrees we know are ignored. if info.IsDir() { + if info.Name() == "testdata" || info.Name() == "node_modules" || info.Name() == "vendor" { + return filepath.SkipDir + } return nil } diff --git a/test/testdata/parity/README.md b/test/testdata/parity/README.md new file mode 100644 index 0000000..4cff687 --- /dev/null +++ b/test/testdata/parity/README.md @@ -0,0 +1,10 @@ +# Parity fixtures for the rule-engine refactor + +Each subdirectory here is a self-contained project fixture used by +`test/engine_parity_test.go`. Golden files (`.golden.txt`, `.golden.json`, +`.golden.exit`) were captured from the pre-refactor binary; the refactor +in spec 005 must produce byte-identical output on every fixture. + +To regenerate goldens (only when the fixture itself changes or a +deliberate output change is being merged), set `UPDATE_PARITY=1` before +running `go test`. diff --git a/test/testdata/parity/capture.sh b/test/testdata/parity/capture.sh new file mode 100755 index 0000000..9127052 --- /dev/null +++ b/test/testdata/parity/capture.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Regenerate golden files. Run from repo root: +# go build -o /tmp/structlint-golden ./cmd/structlint +# bash test/testdata/parity/capture.sh /tmp/structlint-golden +set -euo pipefail +BIN="${1:-./bin/structlint}" +ROOT="$(cd "$(dirname "$0")" && pwd)" +GOLDENS="$ROOT/goldens" +mkdir -p "$GOLDENS" +for fixture in "$ROOT"/*/; do + fixture="${fixture%/}" + name="$(basename "$fixture")" + [[ "$name" == "goldens" || "$name" == "capture.sh" || "$name" == "README.md" ]] && continue + [[ ! -f "$fixture/.structlint.yaml" ]] && continue + + set +e + "$BIN" validate --path "$fixture" --config "$fixture/.structlint.yaml" > "$GOLDENS/${name}.text" 2>&1 + code=$? + set -e + echo "$code" > "$GOLDENS/${name}.exit" + + set +e + "$BIN" validate --path "$fixture" --config "$fixture/.structlint.yaml" --format json > "$GOLDENS/${name}.json" 2>&1 + set -e + + echo "captured $name (exit=$code)" +done diff --git a/test/testdata/parity/goldens/mixed-violations.exit b/test/testdata/parity/goldens/mixed-violations.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/test/testdata/parity/goldens/mixed-violations.exit @@ -0,0 +1 @@ +1 diff --git a/test/testdata/parity/goldens/mixed-violations.json b/test/testdata/parity/goldens/mixed-violations.json new file mode 100644 index 0000000..064666d --- /dev/null +++ b/test/testdata/parity/goldens/mixed-violations.json @@ -0,0 +1,84 @@ +✗ Disallowed directory found: tmp +✗ Disallowed file naming pattern found: .env.local +✗ File not in allowed naming pattern: stray.sql +✗ File placement violation: stray.sql must be under migrations/** +{ + "successes": 8, + "failures": 4, + "total_violations": 4, + "errors": [ + "Disallowed directory found: tmp", + "Disallowed file naming pattern found: .env.local", + "File not in allowed naming pattern: stray.sql", + "File placement violation: stray.sql must be under migrations/**" + ], + "violations": [ + { + "code": "disallowed_file_pattern", + "severity": "error", + "path": ".env.local", + "rule": "*.env*", + "message": "Disallowed file naming pattern found: .env.local" + }, + { + "code": "placement_violation", + "severity": "error", + "path": "stray.sql", + "rule": "sql-only-under-migrations", + "message": "File placement violation: stray.sql must be under migrations/**" + }, + { + "code": "unallowed_file_pattern", + "severity": "error", + "path": "stray.sql", + "rule": "file_naming_pattern.allowed", + "message": "File not in allowed naming pattern: stray.sql" + }, + { + "code": "disallowed_directory", + "severity": "error", + "path": "tmp", + "rule": "tmp/**", + "message": "Disallowed directory found: tmp" + } + ], + "summary": { + "total_successes": 8, + "total_failures": 4, + "violations": [ + { + "type": "disallowed_directory", + "count": 1, + "examples": [ + "Disallowed directory found: tmp" + ], + "description": "Directories that are explicitly disallowed" + }, + { + "type": "disallowed_file_pattern", + "count": 1, + "examples": [ + "Disallowed file naming pattern found: .env.local" + ], + "description": "Files matching disallowed naming patterns" + }, + { + "type": "placement_violation", + "count": 1, + "examples": [ + "File placement violation: stray.sql must be under migrations/**" + ], + "description": "Files placed outside their required directories" + }, + { + "type": "unallowed_file_pattern", + "count": 1, + "examples": [ + "File not in allowed naming pattern: stray.sql" + ], + "description": "Files not matching any allowed naming pattern" + } + ] + } +} +validation failed with 4 errors diff --git a/test/testdata/parity/goldens/mixed-violations.text b/test/testdata/parity/goldens/mixed-violations.text new file mode 100644 index 0000000..6f9891f --- /dev/null +++ b/test/testdata/parity/goldens/mixed-violations.text @@ -0,0 +1,27 @@ +✗ Disallowed directory found: tmp +✗ Disallowed file naming pattern found: .env.local +✗ File not in allowed naming pattern: stray.sql +✗ File placement violation: stray.sql must be under migrations/** + +--- Validation Summary --- +✓ 8 files/directories passed validation +✗ 4 violations found + +--- Validation Summary --- +✓ 8 checks passed +✗ 4 checks failed + +--- Violation Summary --- + +Directories that are explicitly disallowed (1 violations): + - Disallowed directory found: tmp + +Files matching disallowed naming patterns (1 violations): + - Disallowed file naming pattern found: .env.local + +Files placed outside their required directories (1 violations): + - File placement violation: stray.sql must be under migrations/** + +Files not matching any allowed naming pattern (1 violations): + - File not in allowed naming pattern: stray.sql +validation failed with 4 errors diff --git a/test/testdata/parity/goldens/required-in-ignored.exit b/test/testdata/parity/goldens/required-in-ignored.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/test/testdata/parity/goldens/required-in-ignored.exit @@ -0,0 +1 @@ +0 diff --git a/test/testdata/parity/goldens/required-in-ignored.json b/test/testdata/parity/goldens/required-in-ignored.json new file mode 100644 index 0000000..39c17e9 --- /dev/null +++ b/test/testdata/parity/goldens/required-in-ignored.json @@ -0,0 +1,12 @@ +{ + "successes": 4, + "failures": 0, + "total_violations": 0, + "errors": null, + "violations": null, + "summary": { + "total_successes": 4, + "total_failures": 0, + "violations": [] + } +} diff --git a/test/testdata/parity/goldens/required-in-ignored.text b/test/testdata/parity/goldens/required-in-ignored.text new file mode 100644 index 0000000..cdc07d9 --- /dev/null +++ b/test/testdata/parity/goldens/required-in-ignored.text @@ -0,0 +1,5 @@ + +--- Validation Summary --- +✓ 4 files/directories passed validation +✗ 0 violations found +🎉 All files and directories comply with the rules! diff --git a/test/testdata/parity/goldens/simple-go.exit b/test/testdata/parity/goldens/simple-go.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/test/testdata/parity/goldens/simple-go.exit @@ -0,0 +1 @@ +0 diff --git a/test/testdata/parity/goldens/simple-go.json b/test/testdata/parity/goldens/simple-go.json new file mode 100644 index 0000000..99f9733 --- /dev/null +++ b/test/testdata/parity/goldens/simple-go.json @@ -0,0 +1,12 @@ +{ + "successes": 13, + "failures": 0, + "total_violations": 0, + "errors": null, + "violations": null, + "summary": { + "total_successes": 13, + "total_failures": 0, + "violations": [] + } +} diff --git a/test/testdata/parity/goldens/simple-go.text b/test/testdata/parity/goldens/simple-go.text new file mode 100644 index 0000000..0ce8628 --- /dev/null +++ b/test/testdata/parity/goldens/simple-go.text @@ -0,0 +1,5 @@ + +--- Validation Summary --- +✓ 13 files/directories passed validation +✗ 0 violations found +🎉 All files and directories comply with the rules! diff --git a/test/testdata/parity/mixed-violations/.env.local b/test/testdata/parity/mixed-violations/.env.local new file mode 100644 index 0000000..65ec267 --- /dev/null +++ b/test/testdata/parity/mixed-violations/.env.local @@ -0,0 +1 @@ +SECRET=1 diff --git a/test/testdata/parity/mixed-violations/.structlint.yaml b/test/testdata/parity/mixed-violations/.structlint.yaml new file mode 100644 index 0000000..e2d580f --- /dev/null +++ b/test/testdata/parity/mixed-violations/.structlint.yaml @@ -0,0 +1,23 @@ +dir_structure: + allowedPaths: + - "." + - "src/**" + disallowedPaths: + - "tmp/**" + requiredPaths: + - "src" +file_naming_pattern: + allowed: + - "*.go" + - "*.md" + - "*.yaml" + disallowed: + - "*.env*" + required: + - "README.md" +placement: + - id: sql-only-under-migrations + files: ["*.sql"] + mustBeUnder: ["migrations/**"] +ignore: + - ".git" diff --git a/test/testdata/parity/mixed-violations/README.md b/test/testdata/parity/mixed-violations/README.md new file mode 100644 index 0000000..890566a --- /dev/null +++ b/test/testdata/parity/mixed-violations/README.md @@ -0,0 +1 @@ +# mixed diff --git a/test/testdata/parity/mixed-violations/src/main.go b/test/testdata/parity/mixed-violations/src/main.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/test/testdata/parity/mixed-violations/src/main.go @@ -0,0 +1 @@ +package main diff --git a/test/testdata/parity/mixed-violations/stray.sql b/test/testdata/parity/mixed-violations/stray.sql new file mode 100644 index 0000000..00b6119 --- /dev/null +++ b/test/testdata/parity/mixed-violations/stray.sql @@ -0,0 +1 @@ +-- stray diff --git a/test/testdata/parity/mixed-violations/tmp/leftover.go b/test/testdata/parity/mixed-violations/tmp/leftover.go new file mode 100644 index 0000000..14aed26 --- /dev/null +++ b/test/testdata/parity/mixed-violations/tmp/leftover.go @@ -0,0 +1 @@ +package tmp diff --git a/test/testdata/parity/required-in-ignored/.structlint.yaml b/test/testdata/parity/required-in-ignored/.structlint.yaml new file mode 100644 index 0000000..cc3a17d --- /dev/null +++ b/test/testdata/parity/required-in-ignored/.structlint.yaml @@ -0,0 +1,12 @@ +dir_structure: + allowedPaths: + - "." + requiredPaths: + - "ignored-dir/inner" +file_naming_pattern: + allowed: + - "*.md" + - "*.yaml" +ignore: + - ".git" + - "ignored-dir" diff --git a/test/testdata/parity/required-in-ignored/README.md b/test/testdata/parity/required-in-ignored/README.md new file mode 100644 index 0000000..ae75562 --- /dev/null +++ b/test/testdata/parity/required-in-ignored/README.md @@ -0,0 +1 @@ +# required-in-ignored diff --git a/test/testdata/parity/required-in-ignored/ignored-dir/inner/.keep b/test/testdata/parity/required-in-ignored/ignored-dir/inner/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/testdata/parity/simple-go/.gitignore b/test/testdata/parity/simple-go/.gitignore new file mode 100644 index 0000000..e660fd9 --- /dev/null +++ b/test/testdata/parity/simple-go/.gitignore @@ -0,0 +1 @@ +bin/ diff --git a/test/testdata/parity/simple-go/.structlint.yaml b/test/testdata/parity/simple-go/.structlint.yaml new file mode 100644 index 0000000..dfa19d6 --- /dev/null +++ b/test/testdata/parity/simple-go/.structlint.yaml @@ -0,0 +1,17 @@ +dir_structure: + allowedPaths: + - "." + - "cmd/**" + - "internal/**" +file_naming_pattern: + allowed: + - "*.go" + - "*.md" + - "*.mod" + - "*.yaml" + - ".gitignore" + required: + - "go.mod" + - "README.md" +ignore: + - ".git" diff --git a/test/testdata/parity/simple-go/README.md b/test/testdata/parity/simple-go/README.md new file mode 100644 index 0000000..9741694 --- /dev/null +++ b/test/testdata/parity/simple-go/README.md @@ -0,0 +1 @@ +# fixture diff --git a/test/testdata/parity/simple-go/cmd/app/main.go b/test/testdata/parity/simple-go/cmd/app/main.go new file mode 100644 index 0000000..38dd16d --- /dev/null +++ b/test/testdata/parity/simple-go/cmd/app/main.go @@ -0,0 +1,3 @@ +package main + +func main() {} diff --git a/test/testdata/parity/simple-go/go.mod b/test/testdata/parity/simple-go/go.mod new file mode 100644 index 0000000..a4f0126 --- /dev/null +++ b/test/testdata/parity/simple-go/go.mod @@ -0,0 +1,3 @@ +module fixture + +go 1.24 diff --git a/test/testdata/parity/simple-go/internal/foo/foo.go b/test/testdata/parity/simple-go/internal/foo/foo.go new file mode 100644 index 0000000..f52652b --- /dev/null +++ b/test/testdata/parity/simple-go/internal/foo/foo.go @@ -0,0 +1 @@ +package foo