From e39f2190cee35058af1aef7ec2a6f3ebea780162 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 Aug 2025 14:26:05 +0000 Subject: [PATCH 1/2] Initial plan From b83e14141f213d18de8f2598bec5ae51393996ff Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 17 Oct 2025 20:26:27 +0200 Subject: [PATCH 2/2] Add new error for name-overlapping type extensions --- src/Compiler/Checking/PostInferenceChecks.fs | 30 ++++++++ src/Compiler/FSComp.txt | 1 + src/Compiler/TypedTree/TypedTreeOps.fs | 14 +++- src/Compiler/TypedTree/TypedTreeOps.fsi | 2 + src/Compiler/xlf/FSComp.txt.cs.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.de.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.es.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.fr.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.it.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.ja.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.ko.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.pl.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.pt-BR.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.ru.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.tr.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.zh-Hans.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.zh-Hant.xlf | 5 ++ .../Language/ExtensionMethodTests.fs | 71 +++++++++++++++++++ 18 files changed, 180 insertions(+), 3 deletions(-) diff --git a/src/Compiler/Checking/PostInferenceChecks.fs b/src/Compiler/Checking/PostInferenceChecks.fs index d72f21662b3..368c062571e 100644 --- a/src/Compiler/Checking/PostInferenceChecks.fs +++ b/src/Compiler/Checking/PostInferenceChecks.fs @@ -2640,6 +2640,35 @@ let CheckEntityDefns cenv env tycons = // check modules //-------------------------------------------------------------------------- +/// Check for duplicate extension member names that would cause IL conflicts. +/// Extension members for types with the same simple name but different fully qualified names +/// will be emitted into the same IL container type, causing a duplicate member error. +let CheckForDuplicateExtensionMemberNames (cenv: cenv) (vals: Val seq) = + if cenv.reportErrors then + // Group extension members by the simple name of the type they extend + let extensionMembers = + vals + |> Seq.filter (fun v -> v.IsExtensionMember && v.IsMember) + |> Seq.toList + + if not extensionMembers.IsEmpty then + let groupedBySimpleName = + extensionMembers + |> List.groupBy (fun v -> v.MemberApparentEntity.DisplayNameCore) + + for (simpleName, members) in groupedBySimpleName do + // Check if members extend different fully qualified types + let distinctTypes = + members + |> List.map (fun v -> v.MemberApparentEntity) + |> List.distinctBy (fun tcref -> tcref.Stamp) + + if distinctTypes.Length > 1 then + // Found extensions for types with same simple name but different fully qualified names + // Report error on the second (and subsequent) extensions + for v in members |> List.skip 1 do + errorR(Error(FSComp.SR.tcDuplicateExtensionMemberNames(simpleName), v.Range)) + let rec CheckDefnsInModule cenv env mdefs = for mdef in mdefs do CheckDefnInModule cenv env mdef @@ -2649,6 +2678,7 @@ and CheckNothingAfterEntryPoint cenv m = errorR(Error(FSComp.SR.chkEntryPointUsage(), m)) and CheckDefnInModule cenv env mdef = + CheckForDuplicateExtensionMemberNames cenv (allTopLevelValsOfModDef mdef) match mdef with | TMDefRec(isRec, _opens, tycons, mspecs, m) -> CheckNothingAfterEntryPoint cenv m diff --git a/src/Compiler/FSComp.txt b/src/Compiler/FSComp.txt index 512e9b4dca7..16fcce9de3c 100644 --- a/src/Compiler/FSComp.txt +++ b/src/Compiler/FSComp.txt @@ -1635,6 +1635,7 @@ featureLowerSimpleMappingsInComprehensionsToFastLoops,"Lowers [for x in xs -> f 3354,tcNotAFunctionButIndexerIndexingNotYetEnabled,"This expression supports indexing, e.g. 'expr.[index]'. The syntax 'expr[index]' requires /langversion:preview. See https://aka.ms/fsharp-index-notation." 3355,tcNotAnIndexerNamedIndexingNotYetEnabled,"The value '%s' is not a function and does not support index notation." 3355,tcNotAnIndexerIndexingNotYetEnabled,"This expression is not a function and does not support index notation." +3356,tcDuplicateExtensionMemberNames,"Extension members extending types with the same simple name '%s' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules." 3360,typrelInterfaceWithConcreteAndVariable,"'%s' cannot implement the interface '%s' with the two instantiations '%s' and '%s' because they may unify." 3361,typrelInterfaceWithConcreteAndVariableObjectExpression,"You cannot implement the interface '%s' with the two instantiations '%s' and '%s' because they may unify." featureInterfacesWithMultipleGenericInstantiation,"interfaces with multiple generic instantiation" diff --git a/src/Compiler/TypedTree/TypedTreeOps.fs b/src/Compiler/TypedTree/TypedTreeOps.fs index 6ad9c1fe727..bd3cb609547 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.fs +++ b/src/Compiler/TypedTree/TypedTreeOps.fs @@ -6445,23 +6445,31 @@ and allEntitiesOfModDef mdef = yield! allEntitiesOfModDef def } -and allValsOfModDef mdef = +and allValsOfModDefWithOption processNested mdef = seq { match mdef with | TMDefRec(_, _, tycons, mbinds, _) -> yield! abstractSlotValsOfTycons tycons for mbind in mbinds do match mbind with | ModuleOrNamespaceBinding.Binding bind -> yield bind.Var - | ModuleOrNamespaceBinding.Module(_, def) -> yield! allValsOfModDef def + | ModuleOrNamespaceBinding.Module(_, def) -> + if processNested then + yield! allValsOfModDefWithOption processNested def | TMDefLet(bind, _) -> yield bind.Var | TMDefDo _ -> () | TMDefOpens _ -> () | TMDefs defs -> for def in defs do - yield! allValsOfModDef def + yield! allValsOfModDefWithOption processNested def } +and allValsOfModDef mdef = + allValsOfModDefWithOption true mdef + +and allTopLevelValsOfModDef mdef = + allValsOfModDefWithOption false mdef + and copyAndRemapModDef ctxt compgen tmenv mdef = let tycons = allEntitiesOfModDef mdef |> List.ofSeq let vs = allValsOfModDef mdef |> List.ofSeq diff --git a/src/Compiler/TypedTree/TypedTreeOps.fsi b/src/Compiler/TypedTree/TypedTreeOps.fsi index ed847cc5839..3feff4d5f47 100755 --- a/src/Compiler/TypedTree/TypedTreeOps.fsi +++ b/src/Compiler/TypedTree/TypedTreeOps.fsi @@ -2724,6 +2724,8 @@ val (|InnerExprPat|): Expr -> Expr val allValsOfModDef: ModuleOrNamespaceContents -> seq +val allTopLevelValsOfModDef: ModuleOrNamespaceContents -> seq + val BindUnitVars: TcGlobals -> Val list * ArgReprInfo list * Expr -> Val list * Expr val isThreadOrContextStatic: TcGlobals -> Attrib list -> bool diff --git a/src/Compiler/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf index 244be90e142..e26121b7264 100644 --- a/src/Compiler/xlf/FSComp.txt.cs.xlf +++ b/src/Compiler/xlf/FSComp.txt.cs.xlf @@ -1467,6 +1467,11 @@ Nullness warning: Downcasting from '{0}' into '{1}' can introduce unexpected null values. Cast to '{2}|null' instead or handle the null before downcasting. + + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + + An empty body may only be used if the computation expression builder defines a 'Zero' method. An empty body may only be used if the computation expression builder defines a 'Zero' method. diff --git a/src/Compiler/xlf/FSComp.txt.de.xlf b/src/Compiler/xlf/FSComp.txt.de.xlf index d1b1782d093..e3df6e6006b 100644 --- a/src/Compiler/xlf/FSComp.txt.de.xlf +++ b/src/Compiler/xlf/FSComp.txt.de.xlf @@ -1467,6 +1467,11 @@ Nullness warning: Downcasting from '{0}' into '{1}' can introduce unexpected null values. Cast to '{2}|null' instead or handle the null before downcasting. + + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + + An empty body may only be used if the computation expression builder defines a 'Zero' method. An empty body may only be used if the computation expression builder defines a 'Zero' method. diff --git a/src/Compiler/xlf/FSComp.txt.es.xlf b/src/Compiler/xlf/FSComp.txt.es.xlf index cd311cc7fc5..dada53495cd 100644 --- a/src/Compiler/xlf/FSComp.txt.es.xlf +++ b/src/Compiler/xlf/FSComp.txt.es.xlf @@ -1467,6 +1467,11 @@ Nullness warning: Downcasting from '{0}' into '{1}' can introduce unexpected null values. Cast to '{2}|null' instead or handle the null before downcasting. + + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + + An empty body may only be used if the computation expression builder defines a 'Zero' method. An empty body may only be used if the computation expression builder defines a 'Zero' method. diff --git a/src/Compiler/xlf/FSComp.txt.fr.xlf b/src/Compiler/xlf/FSComp.txt.fr.xlf index e05e9ecdf6c..08540202573 100644 --- a/src/Compiler/xlf/FSComp.txt.fr.xlf +++ b/src/Compiler/xlf/FSComp.txt.fr.xlf @@ -1467,6 +1467,11 @@ Nullness warning: Downcasting from '{0}' into '{1}' can introduce unexpected null values. Cast to '{2}|null' instead or handle the null before downcasting. + + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + + An empty body may only be used if the computation expression builder defines a 'Zero' method. An empty body may only be used if the computation expression builder defines a 'Zero' method. diff --git a/src/Compiler/xlf/FSComp.txt.it.xlf b/src/Compiler/xlf/FSComp.txt.it.xlf index a25fd816046..b045ddf478a 100644 --- a/src/Compiler/xlf/FSComp.txt.it.xlf +++ b/src/Compiler/xlf/FSComp.txt.it.xlf @@ -1467,6 +1467,11 @@ Nullness warning: Downcasting from '{0}' into '{1}' can introduce unexpected null values. Cast to '{2}|null' instead or handle the null before downcasting. + + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + + An empty body may only be used if the computation expression builder defines a 'Zero' method. An empty body may only be used if the computation expression builder defines a 'Zero' method. diff --git a/src/Compiler/xlf/FSComp.txt.ja.xlf b/src/Compiler/xlf/FSComp.txt.ja.xlf index 87b7d40df1e..37b3cf384bf 100644 --- a/src/Compiler/xlf/FSComp.txt.ja.xlf +++ b/src/Compiler/xlf/FSComp.txt.ja.xlf @@ -1467,6 +1467,11 @@ Nullness warning: Downcasting from '{0}' into '{1}' can introduce unexpected null values. Cast to '{2}|null' instead or handle the null before downcasting. + + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + + An empty body may only be used if the computation expression builder defines a 'Zero' method. An empty body may only be used if the computation expression builder defines a 'Zero' method. diff --git a/src/Compiler/xlf/FSComp.txt.ko.xlf b/src/Compiler/xlf/FSComp.txt.ko.xlf index f2fe6e20f97..7b72ecf494e 100644 --- a/src/Compiler/xlf/FSComp.txt.ko.xlf +++ b/src/Compiler/xlf/FSComp.txt.ko.xlf @@ -1467,6 +1467,11 @@ Nullness warning: Downcasting from '{0}' into '{1}' can introduce unexpected null values. Cast to '{2}|null' instead or handle the null before downcasting. + + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + + An empty body may only be used if the computation expression builder defines a 'Zero' method. An empty body may only be used if the computation expression builder defines a 'Zero' method. diff --git a/src/Compiler/xlf/FSComp.txt.pl.xlf b/src/Compiler/xlf/FSComp.txt.pl.xlf index 23a194ff258..abb1a52693c 100644 --- a/src/Compiler/xlf/FSComp.txt.pl.xlf +++ b/src/Compiler/xlf/FSComp.txt.pl.xlf @@ -1467,6 +1467,11 @@ Nullness warning: Downcasting from '{0}' into '{1}' can introduce unexpected null values. Cast to '{2}|null' instead or handle the null before downcasting. + + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + + An empty body may only be used if the computation expression builder defines a 'Zero' method. An empty body may only be used if the computation expression builder defines a 'Zero' method. diff --git a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf index 503fc0f073f..6c8751827ae 100644 --- a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf +++ b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf @@ -1467,6 +1467,11 @@ Nullness warning: Downcasting from '{0}' into '{1}' can introduce unexpected null values. Cast to '{2}|null' instead or handle the null before downcasting. + + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + + An empty body may only be used if the computation expression builder defines a 'Zero' method. An empty body may only be used if the computation expression builder defines a 'Zero' method. diff --git a/src/Compiler/xlf/FSComp.txt.ru.xlf b/src/Compiler/xlf/FSComp.txt.ru.xlf index 7013fb0bc83..1b2695f4b9c 100644 --- a/src/Compiler/xlf/FSComp.txt.ru.xlf +++ b/src/Compiler/xlf/FSComp.txt.ru.xlf @@ -1467,6 +1467,11 @@ Nullness warning: Downcasting from '{0}' into '{1}' can introduce unexpected null values. Cast to '{2}|null' instead or handle the null before downcasting. + + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + + An empty body may only be used if the computation expression builder defines a 'Zero' method. An empty body may only be used if the computation expression builder defines a 'Zero' method. diff --git a/src/Compiler/xlf/FSComp.txt.tr.xlf b/src/Compiler/xlf/FSComp.txt.tr.xlf index 49d2a295b45..51ac2987763 100644 --- a/src/Compiler/xlf/FSComp.txt.tr.xlf +++ b/src/Compiler/xlf/FSComp.txt.tr.xlf @@ -1467,6 +1467,11 @@ Nullness warning: Downcasting from '{0}' into '{1}' can introduce unexpected null values. Cast to '{2}|null' instead or handle the null before downcasting. + + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + + An empty body may only be used if the computation expression builder defines a 'Zero' method. An empty body may only be used if the computation expression builder defines a 'Zero' method. diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf index 3fc65eebc96..90d2152cd96 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf @@ -1467,6 +1467,11 @@ Nullness warning: Downcasting from '{0}' into '{1}' can introduce unexpected null values. Cast to '{2}|null' instead or handle the null before downcasting. + + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + + An empty body may only be used if the computation expression builder defines a 'Zero' method. An empty body may only be used if the computation expression builder defines a 'Zero' method. diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf index 07fea0efd23..707effe2464 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf @@ -1467,6 +1467,11 @@ Nullness warning: Downcasting from '{0}' into '{1}' can introduce unexpected null values. Cast to '{2}|null' instead or handle the null before downcasting. + + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + Extension members extending types with the same simple name '{0}' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules. + + An empty body may only be used if the computation expression builder defines a 'Zero' method. An empty body may only be used if the computation expression builder defines a 'Zero' method. diff --git a/tests/FSharp.Compiler.ComponentTests/Language/ExtensionMethodTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/ExtensionMethodTests.fs index 60f902e9ef5..a53a5670130 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/ExtensionMethodTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/ExtensionMethodTests.fs @@ -685,3 +685,74 @@ module FSLibConsumer = |> withReferences [ producer ] fsharp2 |> compile |> shouldSucceed + + [] + let ``Static extension members for types with same simple name but different namespaces should error`` () = + Fsx + """ +module Compiled + +type Task = { F: int } + +module CompiledExtensions = + type System.Threading.Tasks.Task with + static member CompiledStaticExtension() = () + + type Task with + static member CompiledStaticExtension() = () + """ + |> compile + |> shouldFail + |> withDiagnostics [ + (Error 3356, Line 11, Col 23, Line 11, Col 46, "Extension members extending types with the same simple name 'Task' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules.") + ] + + [] + let ``Static extension members for types with same simple name in different modules should succeed`` () = + Fsx + """ +module Compiled + +type Task = { F: int } + +module CompiledExtensions1 = + type System.Threading.Tasks.Task with + static member CompiledStaticExtension() = () + +module CompiledExtensions2 = + type Task with + static member CompiledStaticExtension() = () + """ + |> compile + |> shouldSucceed + + [] + let ``Static extension members with nested module in between should error`` () = + Fsx + """ +module Compiled + +type Task = { F: int } + +module CompiledExtensions = + // First extension for System.Threading.Tasks.Task + type System.Threading.Tasks.Task with + static member Extension1() = () + + // Nested module - this is fine, shouldn't interfere with duplicate check + module Nested = + let someValue = 42 + type OtherType = { X: int } + + // Some other definition + let someBinding = 10 + + // Second extension for local Task type - this should clash with the first + type Task with + static member Extension2() = () + """ + |> compile + |> shouldFail + |> withDiagnostics [ + (Error 3356, Line 21, Col 23, Line 21, Col 33, "Extension members extending types with the same simple name 'Task' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules.") + ]