Skip to content

Commit b73d778

Browse files
committed
Consolidate select expressions
There should be no duplicates in attributes after selects are evaluated. Merge and split selects such that Bazel's select invariants are maintained. Remove values from selects that are already part of the non-configurable part of the attribute.
1 parent 763cc90 commit b73d778

2 files changed

Lines changed: 588 additions & 2 deletions

File tree

gazelle_cabal/lang.go

Lines changed: 297 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"path"
1111
"path/filepath"
1212
"sort"
13+
"strings"
1314

1415
"github.com/bazelbuild/bazel-gazelle/config"
1516
"github.com/bazelbuild/bazel-gazelle/label"
@@ -161,6 +162,11 @@ func (*gazelleCabalLang) Resolve(c *config.Config, ix *resolve.RuleIndex, rc *re
161162
toolRepo := packageRepo + "-exe"
162163
if imports != nil {
163164
importData := imports.(ImportData)
165+
166+
// Consolidate all ImportData fields to avoid duplicate selects
167+
importData.Deps = consolidateConfigurableList(importData.Deps)
168+
importData.GhcOpts = consolidateConfigurableList(importData.GhcOpts)
169+
importData.ExtraLibraries = consolidateConfigurableList(importData.ExtraLibraries)
164170

165171
libraryLabels, unresolvedExtraLibraries := getExtraLibraryLabels(c, importData.ExtraLibraries)
166172
setDepsAndPluginsAttributes(libraryLabels, packageRepo, ix, r, importData, from)
@@ -313,9 +319,10 @@ func infoToRules(repoRoot string, flags []Flag, ruleInfos []RuleInfo, configGrou
313319
// Generate rules for each component
314320
for _, ruleInfo := range ruleInfos {
315321
r := rule.NewRule(ruleInfo.Kind, ruleInfo.Name)
316-
r.SetAttr("srcs", ruleInfo.Srcs)
322+
323+
r.SetAttr("srcs", consolidateConfigurableList(ruleInfo.Srcs))
317324
if ruleInfo.HiddenModules != nil {
318-
r.SetAttr("hidden_modules", ruleInfo.HiddenModules)
325+
r.SetAttr("hidden_modules", consolidateConfigurableList(ruleInfo.HiddenModules))
319326
}
320327
for k, v := range ruleInfo.Attrs {
321328
r.SetAttr(k, v)
@@ -695,3 +702,291 @@ func hasRuleInfo(ruleInfos []RuleInfo, kind string, name string) bool {
695702
}
696703
return false
697704
}
705+
706+
// consolidateConfigurableList merges ConfigurableList items to avoid duplicates
707+
// It combines values under identical condition keys across multiple selects,
708+
// but splits selects to avoid ambiguous matches when the same value appears in multiple conditions.
709+
// Values in the plain list are removed from all select conditions to avoid duplication.
710+
// This preserves Bazel's select semantics where conditions must not be ambiguous.
711+
func consolidateConfigurableList(cl ConfigurableList[string]) ConfigurableList[string] {
712+
if len(cl) == 0 {
713+
return cl
714+
}
715+
716+
// Separate plain values from selects
717+
var plainValues []string
718+
var selects []rule.SelectStringListValue
719+
720+
for _, item := range cl {
721+
switch v := item.(type) {
722+
case []string:
723+
plainValues = append(plainValues, v...)
724+
case map[string][]string:
725+
// Convert to SelectStringListValue
726+
selectVal := make(rule.SelectStringListValue)
727+
for k, vals := range v {
728+
selectVal[k] = vals
729+
}
730+
selects = append(selects, selectVal)
731+
case rule.SelectStringListValue:
732+
selects = append(selects, v)
733+
}
734+
}
735+
736+
// Build result
737+
result := make(ConfigurableList[string], 0, len(selects)+1)
738+
739+
// Deduplicate plain values
740+
plainValuesDedup := deduplicateStrings(plainValues)
741+
742+
// Create a set of plain values for fast lookup
743+
plainValuesSet := make(map[string]bool, len(plainValuesDedup))
744+
for _, v := range plainValuesDedup {
745+
plainValuesSet[v] = true
746+
}
747+
748+
// Add deduplicated plain values first
749+
if len(plainValuesDedup) > 0 {
750+
result = append(result, plainValuesDedup)
751+
}
752+
753+
// Merge all selects into a single select first
754+
if len(selects) > 0 {
755+
// Check if all selects have identical conditions (not just values)
756+
canMerge := true
757+
if len(selects) > 1 {
758+
var firstConditions []string
759+
for i, sel := range selects {
760+
var conditions []string
761+
for condition := range sel {
762+
conditions = append(conditions, condition)
763+
}
764+
sort.Strings(conditions)
765+
if i == 0 {
766+
firstConditions = conditions
767+
} else {
768+
if len(conditions) != len(firstConditions) {
769+
canMerge = false
770+
break
771+
}
772+
for j, cond := range conditions {
773+
if cond != firstConditions[j] {
774+
canMerge = false
775+
break
776+
}
777+
}
778+
if !canMerge {
779+
break
780+
}
781+
}
782+
}
783+
}
784+
785+
if !canMerge {
786+
// Cannot safely merge - keep as separate selects to avoid ambiguous matches
787+
for _, sel := range selects {
788+
newSelect := make(rule.SelectStringListValue)
789+
for condition, vals := range sel {
790+
// Filter out values that are in plain list
791+
filtered := make([]string, 0, len(vals))
792+
for _, v := range vals {
793+
if !plainValuesSet[v] {
794+
filtered = append(filtered, v)
795+
}
796+
}
797+
// Deduplicate
798+
filtered = deduplicateStrings(filtered)
799+
newSelect[condition] = filtered
800+
}
801+
// Ensure default is present
802+
if _, exists := newSelect["//conditions:default"]; !exists {
803+
newSelect["//conditions:default"] = []string{}
804+
}
805+
// Only add if there are non-default conditions with values
806+
hasNonDefaultValues := false
807+
for condition, vals := range newSelect {
808+
if condition != "//conditions:default" && len(vals) > 0 {
809+
hasNonDefaultValues = true
810+
break
811+
}
812+
}
813+
if hasNonDefaultValues {
814+
result = append(result, newSelect)
815+
}
816+
}
817+
return result
818+
}
819+
820+
// Safe to merge - all selects have identical conditions
821+
mergedSelect := make(rule.SelectStringListValue)
822+
823+
for _, sel := range selects {
824+
for condition, vals := range sel {
825+
mergedSelect[condition] = append(mergedSelect[condition], vals...)
826+
}
827+
}
828+
829+
// Deduplicate values within each condition and remove values that are in plain list
830+
for condition, vals := range mergedSelect {
831+
deduped := deduplicateStrings(vals)
832+
filtered := make([]string, 0, len(deduped))
833+
for _, v := range deduped {
834+
if !plainValuesSet[v] {
835+
filtered = append(filtered, v)
836+
}
837+
}
838+
mergedSelect[condition] = filtered
839+
}
840+
841+
// Remove empty conditions (but keep track of default)
842+
defaultVals, hasDefault := mergedSelect["//conditions:default"]
843+
for condition, vals := range mergedSelect {
844+
if len(vals) == 0 && condition != "//conditions:default" {
845+
delete(mergedSelect, condition)
846+
}
847+
}
848+
849+
// Now split the select by shared values to avoid ambiguous matches
850+
// For each value, find all conditions that include it and create a separate select
851+
valueToConditions := make(map[string][]string)
852+
for condition, vals := range mergedSelect {
853+
for _, v := range vals {
854+
valueToConditions[v] = append(valueToConditions[v], condition)
855+
}
856+
}
857+
858+
// Group values by their condition sets (values with same conditions go together)
859+
type conditionSet struct {
860+
conditions []string
861+
values []string
862+
}
863+
conditionSets := make(map[string]*conditionSet)
864+
865+
for value, conditions := range valueToConditions {
866+
sort.Strings(conditions)
867+
key := strings.Join(conditions, "|")
868+
if cs, exists := conditionSets[key]; exists {
869+
cs.values = append(cs.values, value)
870+
} else {
871+
conditionSets[key] = &conditionSet{
872+
conditions: conditions,
873+
values: []string{value},
874+
}
875+
}
876+
}
877+
878+
// Check if we need to split: only split if there are overlapping conditions
879+
// Overlapping means a condition appears in multiple condition sets
880+
conditionCounts := make(map[string]int)
881+
for _, cs := range conditionSets {
882+
for _, cond := range cs.conditions {
883+
conditionCounts[cond]++
884+
}
885+
}
886+
887+
hasOverlap := false
888+
for _, count := range conditionCounts {
889+
if count > 1 {
890+
hasOverlap = true
891+
break
892+
}
893+
}
894+
895+
if hasOverlap {
896+
// Split: create a separate select for each condition set
897+
for _, cs := range conditionSets {
898+
newSelect := make(rule.SelectStringListValue)
899+
for _, condition := range cs.conditions {
900+
newSelect[condition] = append([]string{}, cs.values...)
901+
}
902+
// Add empty default condition if not already present
903+
if _, hasDefault := newSelect["//conditions:default"]; !hasDefault {
904+
newSelect["//conditions:default"] = []string{}
905+
}
906+
result = append(result, newSelect)
907+
}
908+
} else {
909+
// No overlap: keep as single select (more compact)
910+
if len(mergedSelect) > 0 {
911+
// Ensure default is present (use existing or add empty)
912+
if _, exists := mergedSelect["//conditions:default"]; !exists {
913+
if hasDefault {
914+
mergedSelect["//conditions:default"] = defaultVals
915+
} else {
916+
mergedSelect["//conditions:default"] = []string{}
917+
}
918+
}
919+
result = append(result, mergedSelect)
920+
}
921+
}
922+
}
923+
924+
return result
925+
}
926+
927+
// deduplicateStrings removes duplicate strings while preserving order
928+
func deduplicateStrings(slice []string) []string {
929+
seen := make(map[string]bool, len(slice))
930+
result := make([]string, 0, len(slice))
931+
932+
for _, item := range slice {
933+
if !seen[item] {
934+
seen[item] = true
935+
result = append(result, item)
936+
}
937+
}
938+
939+
return result
940+
}
941+
942+
// generatePathsModuleContentLines generates the content for an autogenerated Paths_ module as an array of lines
943+
func generatePathsModuleContentLines(moduleName string, version string) []string {
944+
return []string{
945+
fmt.Sprintf("module %s where", moduleName),
946+
" ",
947+
"import Data.Version (Version, makeVersion)",
948+
" ",
949+
"version :: Version",
950+
fmt.Sprintf("version = makeVersion [%s]", convertVersionToList(version)),
951+
}
952+
}
953+
954+
// convertVersionToList converts a version string like "1.0.2.0" to "1, 0, 2, 0"
955+
func convertVersionToList(version string) string {
956+
// Simple conversion: replace dots with ", "
957+
result := ""
958+
for i, c := range version {
959+
if c == '.' {
960+
result += ", "
961+
} else {
962+
result += string(c)
963+
}
964+
if i == len(version)-1 {
965+
break
966+
}
967+
}
968+
return result
969+
}
970+
971+
// prependToConfigurableList prepends a value to a ConfigurableList
972+
func prependToConfigurableList(list ConfigurableList[string], value string) ConfigurableList[string] {
973+
newList := make(ConfigurableList[string], len(list))
974+
for i, item := range list {
975+
switch v := item.(type) {
976+
case []string:
977+
// For plain slice, prepend value
978+
newList[i] = append([]string{value}, v...)
979+
case map[string][]string:
980+
// For select/map, prepend to each conditional value
981+
newConditionals := make(map[string][]string)
982+
for k, vals := range v {
983+
newConditionals[k] = append([]string{value}, vals...)
984+
}
985+
newList[i] = newConditionals
986+
default:
987+
// Keep as is if unexpected type
988+
newList[i] = item
989+
}
990+
}
991+
return newList
992+
}

0 commit comments

Comments
 (0)