Skip to content

Commit 45ae48b

Browse files
Smaug123claude
andcommitted
Scope generated opens to the tagged type's namespace block
Every generator previously emitted every `open` from the whole input file into every generated namespace (via Whippet's AstHelper.extractOpens, which shadows the identically-named dead local helper). A relative open that is valid in one namespace block is not necessarily valid in another, so generated code for a type in a different block could fail to compile. extractOpensForNamespace collects opens only from the namespace blocks sharing the tagged type's name (same-named blocks share a resolution context, so unioning those is sound). The dead local extractOpens is replaced. ConsumePlugin/OpensLeakRegression.fs is a compile-time regression test: its bait block contains a relative `open Sub` which does not resolve in the victim namespace, so the pre-fix generator produces uncompilable output for it (verified against the pre-fix plugin). No checked-in generated code changes: no existing input relied on the leak. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 78bf71d commit 45ae48b

10 files changed

Lines changed: 217 additions & 9 deletions

ConsumePlugin/ConsumePlugin.fsproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@
100100
<Compile Include="GeneratedArgParserNegationTests.fs">
101101
<MyriadFile>ArgParserNegationTests.fs</MyriadFile>
102102
</Compile>
103+
<Compile Include="OpensLeakRegression.fs" />
104+
<Compile Include="GeneratedOpensLeakRegression.fs">
105+
<MyriadFile>OpensLeakRegression.fs</MyriadFile>
106+
</Compile>
103107
<!-- Not compiled, because by design they *don't* compile. That makes them very hard to test in an automated way! -->
104108
<None Include="ArgParserConflictTests.fs" />
105109
<!-- To run the conflict tests:
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
//------------------------------------------------------------------------------
2+
// This code was generated by myriad.
3+
// Changes to this file will be lost when the code is regenerated.
4+
//------------------------------------------------------------------------------
5+
6+
7+
8+
9+
10+
11+
12+
namespace ConsumePlugin.OpensLeakVictim
13+
14+
open WoofWare.Myriad.Plugins
15+
16+
/// Methods to parse arguments for the type LeakArgs
17+
[<RequireQualifiedAccess ; CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
18+
module LeakArgs =
19+
type private ParseState_LeakArgs =
20+
/// Ready to consume a key or positional arg
21+
| AwaitingKey
22+
/// Waiting to receive a value for the key we've already consumed
23+
| AwaitingValue of key : string
24+
25+
let parse' (getEnvironmentVariable : string -> string option) (args : string list) : LeakArgs =
26+
let ArgParser_errors = ResizeArray ()
27+
28+
let helpText () =
29+
[ (sprintf "%s int32%s%s" (sprintf "--%s" "foo") "" "") ] |> String.concat "\n"
30+
31+
let parser_LeftoverArgs : string ResizeArray = ResizeArray ()
32+
let mutable arg_0 : int option = None
33+
34+
/// Processes the key-value pair, returning Error if no key was matched.
35+
/// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(<the message>).
36+
/// This can nevertheless be a successful parse, e.g. when the key may have arity 0.
37+
let processKeyValue (key : string) (value : string) : Result<unit, string option> =
38+
if System.String.Equals (key, sprintf "--%s" "foo", System.StringComparison.OrdinalIgnoreCase) then
39+
match arg_0 with
40+
| Some x ->
41+
sprintf
42+
"Argument '%s' was supplied multiple times: %s and %s"
43+
(sprintf "--%s" "foo")
44+
(x.ToString ())
45+
(value.ToString ())
46+
|> ArgParser_errors.Add
47+
48+
Ok ()
49+
| None ->
50+
try
51+
arg_0 <- value |> (fun x -> System.Int32.Parse x) |> Some
52+
Ok ()
53+
with _ as exc ->
54+
exc.Message |> Some |> Error
55+
else
56+
Error None
57+
58+
/// Returns false if we didn't set a value.
59+
let setFlagValue (key : string) : bool = false
60+
61+
let rec go (state : ParseState_LeakArgs) (args : string list) =
62+
match args with
63+
| [] ->
64+
match state with
65+
| ParseState_LeakArgs.AwaitingKey -> ()
66+
| ParseState_LeakArgs.AwaitingValue key ->
67+
if setFlagValue key then
68+
()
69+
else
70+
sprintf
71+
"Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args."
72+
key
73+
|> ArgParser_errors.Add
74+
| "--" :: rest ->
75+
match state with
76+
| ParseState_LeakArgs.AwaitingKey -> ()
77+
| ParseState_LeakArgs.AwaitingValue key ->
78+
if setFlagValue key then
79+
()
80+
else
81+
sprintf
82+
"Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args."
83+
key
84+
|> ArgParser_errors.Add
85+
86+
parser_LeftoverArgs.AddRange (rest |> Seq.map (fun x -> x))
87+
| arg :: args ->
88+
match state with
89+
| ParseState_LeakArgs.AwaitingKey ->
90+
if arg.StartsWith ("--", System.StringComparison.Ordinal) then
91+
if arg = "--help" then
92+
helpText () |> failwithf "Help text requested.\n%s"
93+
else
94+
let equals = arg.IndexOf (char 61)
95+
96+
if equals < 0 then
97+
args |> go (ParseState_LeakArgs.AwaitingValue arg)
98+
else
99+
let key = arg.[0 .. equals - 1]
100+
let value = arg.[equals + 1 ..]
101+
102+
match processKeyValue key value with
103+
| Ok () -> go ParseState_LeakArgs.AwaitingKey args
104+
| Error x ->
105+
match x with
106+
| None ->
107+
failwithf "Unable to process argument %s as key %s and value %s" arg key value
108+
| Some msg ->
109+
sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add
110+
go ParseState_LeakArgs.AwaitingKey args
111+
else
112+
arg |> (fun x -> x) |> parser_LeftoverArgs.Add
113+
go ParseState_LeakArgs.AwaitingKey args
114+
| ParseState_LeakArgs.AwaitingValue key ->
115+
match processKeyValue key arg with
116+
| Ok () -> go ParseState_LeakArgs.AwaitingKey args
117+
| Error exc ->
118+
if setFlagValue key then
119+
go ParseState_LeakArgs.AwaitingKey (arg :: args)
120+
else
121+
match exc with
122+
| None ->
123+
failwithf "Unable to process supplied arg %s. Help text follows.\n%s" key (helpText ())
124+
| Some msg -> msg |> ArgParser_errors.Add
125+
126+
go ParseState_LeakArgs.AwaitingKey args
127+
128+
let parser_LeftoverArgs =
129+
if 0 = parser_LeftoverArgs.Count then
130+
()
131+
else
132+
parser_LeftoverArgs
133+
|> String.concat " "
134+
|> sprintf "There were leftover args: %s"
135+
|> ArgParser_errors.Add
136+
137+
Unchecked.defaultof<_>
138+
139+
let arg_0 =
140+
match arg_0 with
141+
| None ->
142+
sprintf "Required argument '%s' received no value" (sprintf "--%s" "foo")
143+
|> ArgParser_errors.Add
144+
145+
Unchecked.defaultof<_>
146+
| Some x -> x
147+
148+
if 0 = ArgParser_errors.Count then
149+
{
150+
Foo = arg_0
151+
}
152+
else
153+
ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s"
154+
155+
let parse (args : string list) : LeakArgs =
156+
parse' (System.Environment.GetEnvironmentVariable >> Option.ofObj) args
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Regression test for the opens leak: `open` declarations from one namespace block of an input
2+
// file must not appear in generated code for a *different* namespace block. The bait below is a
3+
// relative `open Sub`, which resolves only from within ConsumePlugin.OpensLeak; if it leaked into
4+
// the generated module for ConsumePlugin.OpensLeakVictim, this project would fail to compile.
5+
namespace ConsumePlugin.OpensLeak.Sub
6+
7+
module internal RelativeOpenBait =
8+
let internal bait : int = 1
9+
10+
namespace ConsumePlugin.OpensLeak
11+
12+
open Sub
13+
14+
type UsesTheBait =
15+
{
16+
Ignored : int
17+
}
18+
19+
static member internal Use () : int = RelativeOpenBait.bait
20+
21+
namespace ConsumePlugin.OpensLeakVictim
22+
23+
open WoofWare.Myriad.Plugins
24+
25+
[<ArgParser>]
26+
type LeakArgs =
27+
{
28+
Foo : int
29+
}

WoofWare.Myriad.Plugins/ArgParserGenerator.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2148,6 +2148,7 @@ type ArgParserGenerator () =
21482148
let modules =
21492149
namespaceAndTypes
21502150
|> List.map (fun (ns, taggedType, unions, records) ->
2151+
let opens = AstHelper.extractOpensForNamespace ns ast
21512152
ArgParserGenerator.createModule opens ns taggedType unions records
21522153
)
21532154

WoofWare.Myriad.Plugins/AstHelper.fs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,27 @@ module internal AstHelper =
5757
| _ -> None
5858
)
5959

60-
let extractOpens (ast : ParsedInput) : SynOpenDeclTarget list =
60+
/// Extract the `open` declarations from those top-level namespace/module blocks of the file
61+
/// whose name is `ns`. Opens from differently-named blocks must not leak into generated code
62+
/// for this namespace: a relative `open` which is valid in one namespace block can be invalid
63+
/// (or resolve to something else) in another. Two blocks with the same name share a resolution
64+
/// context, so it is sound to union their opens.
65+
///
66+
/// (Note: this deliberately does not share a name with WoofWare.Whippet.Fantomas's
67+
/// `AstHelper.extractOpens`, which shadows this module's members at generator call sites and
68+
/// extracts opens from *every* block in the file.)
69+
let extractOpensForNamespace (ns : LongIdent) (ast : ParsedInput) : SynOpenDeclTarget list =
70+
let nsName = ns |> List.map _.idText
71+
6172
match ast with
6273
| ParsedInput.ImplFile (ParsedImplFileInput (_, _, _, _, _, modules, _, _, _)) ->
6374
modules
64-
|> List.collect (fun (SynModuleOrNamespace (_, _, _, decls, _, _, _, _, _)) -> extractOpensFromDecl decls)
75+
|> List.collect (fun (SynModuleOrNamespace (longId, _, _, decls, _, _, _, _, _)) ->
76+
if (longId |> List.map _.idText) = nsName then
77+
extractOpensFromDecl decls
78+
else
79+
[]
80+
)
6581
| _ -> []
6682

6783
let rec convertSigParam (ty : SynType) : ParameterInfo * bool =

WoofWare.Myriad.Plugins/CapturingInterfaceMockGenerator.fs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -735,11 +735,10 @@ type CapturingInterfaceMockGenerator () =
735735
| ty -> Some (ns, ty)
736736
)
737737

738-
let opens = AstHelper.extractOpens ast
739-
740738
let modules =
741739
namespaceAndInterfaces
742740
|> List.collect (fun (ns, records) ->
741+
let opens = AstHelper.extractOpensForNamespace ns ast
743742
records |> List.map (CapturingInterfaceMockGenerator.createRecord ns opens)
744743
)
745744

WoofWare.Myriad.Plugins/CataGenerator.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,6 +1266,7 @@ type CreateCatamorphismGenerator () =
12661266
let modules =
12671267
namespaceAndTypes
12681268
|> List.map (fun (ns, taggedType, unions, records) ->
1269+
let opens = AstHelper.extractOpensForNamespace ns ast
12691270
CataGenerator.createModule opens ns taggedType unions records
12701271
)
12711272

WoofWare.Myriad.Plugins/HttpClientGenerator.fs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1294,6 +1294,9 @@ type HttpClientGenerator () =
12941294

12951295
let modules =
12961296
namespaceAndTypes
1297-
|> List.collect (fun (ns, types) -> types |> List.map (HttpClientGenerator.createModule opens ns))
1297+
|> List.collect (fun (ns, types) ->
1298+
let opens = AstHelper.extractOpensForNamespace ns ast
1299+
types |> List.map (HttpClientGenerator.createModule opens ns)
1300+
)
12981301

12991302
Output.Ast modules

WoofWare.Myriad.Plugins/InterfaceMockGenerator.fs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,11 +418,10 @@ type InterfaceMockGenerator () =
418418
| ty -> Some (ns, ty)
419419
)
420420

421-
let opens = AstHelper.extractOpens ast
422-
423421
let modules =
424422
namespaceAndInterfaces
425423
|> List.collect (fun (ns, records) ->
424+
let opens = AstHelper.extractOpensForNamespace ns ast
426425
records |> List.map (InterfaceMockGenerator.createRecord ns opens)
427426
)
428427

WoofWare.Myriad.Plugins/JsonSerializeGenerator.fs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -824,11 +824,11 @@ type JsonSerializeGenerator () =
824824
| ty -> Some (ns, ty)
825825
)
826826

827-
let opens = AstHelper.extractOpens ast
828-
829827
let modules =
830828
namespaceAndTypes
831829
|> List.collect (fun (ns, types) ->
830+
let opens = AstHelper.extractOpensForNamespace ns ast
831+
832832
types
833833
|> List.map (fun (ty, spec) -> JsonSerializeGenerator.createModule ns opens spec ty)
834834
)

0 commit comments

Comments
 (0)