From 28bea40038d83ef8f2cd9382d8f4ebeb2af370b5 Mon Sep 17 00:00:00 2001 From: vickydotbat Date: Fri, 15 May 2026 23:02:33 +0200 Subject: [PATCH] Error on duplicate keys --- internal/topdata/topdata.go | 59 ++++++++++++++++++++++++++++++++ internal/topdata/topdata_test.go | 33 ++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/internal/topdata/topdata.go b/internal/topdata/topdata.go index ce270e4..54aaf08 100644 --- a/internal/topdata/topdata.go +++ b/internal/topdata/topdata.go @@ -264,6 +264,7 @@ func ValidateProject(p *project.Project) ValidationReport { }) validateTopPackageAssets(sourceDir, dataDir, &report) validateNativeOutputCatalog(dataDir, &report) + validateNativeEntryKeyUniqueness(dataDir, &report) validateNativeLockAllocation(dataDir, &report) validateGeneratedFeatFamilies(dataDir, &report) validateItempropsRegistryGraph(dataDir, &report) @@ -1001,6 +1002,64 @@ func validateNativeOutputCatalog(dataDir string, report *ValidationReport) { } } +func validateNativeEntryKeyUniqueness(dataDir string, report *ValidationReport) { + datasets, err := discoverNativeDatasets(dataDir) + if err != nil { + report.Diagnostics = append(report.Diagnostics, Diagnostic{ + Severity: SeverityError, + Path: dataDir, + Message: fmt.Sprintf("discover native datasets for entry key uniqueness validation: %v", err), + }) + return + } + for _, dataset := range datasets { + if dataset.Kind != nativeDatasetBase { + continue + } + entryKeyPath := map[string]string{} + for _, dir := range []string{dataset.ModulesDir, dataset.GeneratedDir} { + paths, err := collectModulePaths(dir) + if err != nil { + report.Diagnostics = append(report.Diagnostics, Diagnostic{ + Severity: SeverityError, + Path: dir, + Message: fmt.Sprintf("collect module paths for entry key uniqueness validation: %v", err), + }) + continue + } + for _, path := range paths { + obj, err := loadJSONObject(path) + if err != nil { + report.Diagnostics = append(report.Diagnostics, Diagnostic{ + Severity: SeverityError, + Path: path, + Message: fmt.Sprintf("load module for entry key uniqueness validation: %v", err), + }) + continue + } + entries, ok := obj["entries"].(map[string]any) + if !ok { + continue + } + for _, key := range sortedKeys(entries) { + if key == "" { + continue + } + if previousPath, exists := entryKeyPath[key]; exists { + report.Diagnostics = append(report.Diagnostics, Diagnostic{ + Severity: SeverityError, + Path: path, + Message: fmt.Sprintf("duplicate entries key %q declared by %s and %s", key, previousPath, path), + }) + continue + } + entryKeyPath[key] = path + } + } + } + } +} + func validateNativeLockAllocation(dataDir string, report *ValidationReport) { datasets, err := discoverNativeDatasets(dataDir) if err != nil { diff --git a/internal/topdata/topdata_test.go b/internal/topdata/topdata_test.go index 74eb9c3..60fb58d 100644 --- a/internal/topdata/topdata_test.go +++ b/internal/topdata/topdata_test.go @@ -134,6 +134,39 @@ func TestValidateProjectRejectsDuplicateJSONKeys(t *testing.T) { } } +func TestValidateProjectRejectsDuplicateEntryKeysAcrossModules(t *testing.T) { + root := testProjectRoot(t) + mkdirAll(t, filepath.Join(root, "topdata", "data", "baseitems", "modules")) + writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n") + writeFile(t, filepath.Join(root, "topdata", "data", "baseitems", "base.json"), `{ + "output": "baseitems.2da", + "columns": ["Label", "MaxRange"], + "rows": [] +}`+"\n") + writeFile(t, filepath.Join(root, "topdata", "data", "baseitems", "lock.json"), "{}\n") + writeFile(t, filepath.Join(root, "topdata", "data", "baseitems", "modules", "10_add_sign.json"), `{ + "entries": { + "baseitems:holdable_sign": {"Label": "Holdable Sign", "MaxRange": "100"} + } +}`+"\n") + writeFile(t, filepath.Join(root, "topdata", "data", "baseitems", "modules", "20_add_sign_again.json"), `{ + "entries": { + "baseitems:holdable_sign": {"Label": "Duplicate Sign", "MaxRange": "200"} + } +}`+"\n") + + report := ValidateProject(testProject(root)) + if !report.HasErrors() { + t.Fatal("expected duplicate entry key validation error") + } + text := diagnosticsText(report.Diagnostics) + if !strings.Contains(text, `duplicate entries key "baseitems:holdable_sign"`) || + !strings.Contains(text, "10_add_sign.json") || + !strings.Contains(text, "20_add_sign_again.json") { + t.Fatalf("expected duplicate entry key diagnostic with both module paths, got:\n%s", text) + } +} + func TestResolvedTableRegistryRegistersConsistentTables(t *testing.T) { collected := []nativeCollectedDataset{ {