Skip to content

Commit ee7519d

Browse files
authored
Merge pull request #445 from fsprojects/repo-assist/docs-nonnullable-customattrs-measurebuilder-170-67-812d6974b0540f7c
[Repo Assist] Add docs and tests for nonNullable, hideObjectMethods, AddCustomAttribute and ProvidedMeasureBuilder
2 parents 2c88df8 + 7e0ce99 commit ee7519d

2 files changed

Lines changed: 277 additions & 1 deletion

File tree

docs/technical-notes.md

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,156 @@ or
128128
System.InvalidOperationException: the operation is not valid due to the current state of the object. at System.Reflection.MemberInfo.get_MetadataToken() in f:\dd\ndp\clr\src\BCL\system\reflection\memberinfo.cs:line 65
129129
```
130130

131+
## Type Definition Properties: nonNullable, hideObjectMethods, and Custom Attributes
132+
133+
### NonNullable
134+
135+
Set `nonNullable = true` when constructing a `ProvidedTypeDefinition` to mark the type with
136+
`[<AllowNullLiteral(false)>]`. This prevents the F# `null` literal from being used with the
137+
provided type, which is the standard F# behaviour for non-nullable reference types.
138+
139+
```fsharp
140+
let myType = ProvidedTypeDefinition(asm, ns, "MyType", Some typeof<obj>, nonNullable = true)
141+
// myType.NonNullable = true
142+
// myType.GetCustomAttributesData() contains AllowNullLiteralAttribute
143+
```
144+
145+
The `NonNullable` property can be read back to check whether the flag was set.
146+
147+
### HideObjectMethods
148+
149+
Set `hideObjectMethods = true` to suppress the standard .NET `Object` methods (`ToString`,
150+
`GetHashCode`, `Equals`) from appearing in IntelliSense for instances of the provided type.
151+
This is useful for simple record-like or data types where the inherited methods add noise.
152+
153+
```fsharp
154+
let myType = ProvidedTypeDefinition(asm, ns, "MyType", Some typeof<obj>, hideObjectMethods = true)
155+
// myType.HideObjectMethods = true
156+
```
157+
158+
### AddCustomAttribute
159+
160+
Use `AddCustomAttribute` to attach arbitrary custom attribute data to provided types, methods,
161+
properties, parameters, and constructors. Implement `CustomAttributeData` inline:
162+
163+
```fsharp
164+
open System
165+
open System.Reflection
166+
167+
// Add [<Obsolete("use something else")>] to a provided type
168+
let myType = ProvidedTypeDefinition(asm, ns, "MyType", Some typeof<obj>)
169+
myType.AddCustomAttribute {
170+
new CustomAttributeData() with
171+
member _.Constructor = typeof<ObsoleteAttribute>.GetConstructor([| typeof<string> |])
172+
member _.ConstructorArguments =
173+
[| CustomAttributeTypedArgument(typeof<string>, "use something else" :> obj) |] :> _
174+
member _.NamedArguments = [||] :> _
175+
}
176+
177+
// Add [<ReflectedDefinition>] to a method parameter
178+
let param = ProvidedParameter("p", typeof<Microsoft.FSharp.Quotations.Expr<int>>)
179+
param.AddCustomAttribute {
180+
new CustomAttributeData() with
181+
member _.Constructor = typeof<ReflectedDefinitionAttribute>.GetConstructor([||])
182+
member _.ConstructorArguments = [||] :> _
183+
member _.NamedArguments = [||] :> _
184+
}
185+
```
186+
187+
`AddCustomAttribute` is available on `ProvidedTypeDefinition`, `ProvidedProperty`,
188+
`ProvidedMethod`, and `ProvidedParameter`. Note that `ProvidedConstructor` does not support
189+
`AddCustomAttribute`; use `AddObsoleteAttribute` for the common case of marking a constructor
190+
as obsolete.
191+
192+
When using cross-targeting (the normal mode), custom attributes whose types exist in the
193+
target reference assemblies will appear in `GetCustomAttributesData()` on the translated
194+
(target) type. Attributes whose types are not present in the target assemblies are silently
195+
omitted from the cross-targeted model.
196+
197+
## Units of Measure with ProvidedMeasureBuilder
198+
199+
The `ProvidedMeasureBuilder` type allows type providers to create and compose F# units of measure.
200+
Units can be used to annotate numeric types (e.g. `float<kg>`) exposed by the type provider.
201+
202+
### Standard SI Units
203+
204+
Use `ProvidedMeasureBuilder.SI` to reference standard units from
205+
`Microsoft.FSharp.Data.UnitSystems.SI`. Pass either the unit symbol (e.g. `"kg"`) or the
206+
unit name in lowercase (e.g. `"kilogram"`):
207+
208+
```fsharp
209+
let kg = ProvidedMeasureBuilder.SI "kg" // UnitSymbols.kg
210+
let m = ProvidedMeasureBuilder.SI "m" // UnitSymbols.m
211+
let s = ProvidedMeasureBuilder.SI "s" // UnitSymbols.s
212+
let kelvin = ProvidedMeasureBuilder.SI "kelvin" // UnitNames.kelvin
213+
```
214+
215+
### Annotating Types with Units
216+
217+
Use `ProvidedMeasureBuilder.AnnotateType` to produce annotated numeric types such as `float<kg>`:
218+
219+
```fsharp
220+
let kg = ProvidedMeasureBuilder.SI "kg"
221+
let floatKg = ProvidedMeasureBuilder.AnnotateType(typeof<float>, [ kg ])
222+
// floatKg represents float<kg>
223+
224+
let weightProp = ProvidedProperty("WeightKg", floatKg, getterCode = fun _ -> <@@ 70.0 @@>)
225+
myType.AddMember weightProp
226+
```
227+
228+
### Compound Units
229+
230+
`ProvidedMeasureBuilder` provides combinators for composing units:
231+
232+
```fsharp
233+
let kg = ProvidedMeasureBuilder.SI "kg"
234+
let m = ProvidedMeasureBuilder.SI "m"
235+
let s = ProvidedMeasureBuilder.SI "s"
236+
237+
// Product: kg·m
238+
let kgTimesM = ProvidedMeasureBuilder.Product(kg, m)
239+
240+
// Ratio: kg/m
241+
let kgPerM = ProvidedMeasureBuilder.Ratio(kg, m)
242+
243+
// Square: m²
244+
let mSquared = ProvidedMeasureBuilder.Square(m)
245+
246+
// Acceleration: m/s²
247+
let accel = ProvidedMeasureBuilder.Ratio(m, ProvidedMeasureBuilder.Square(s))
248+
249+
// Inverse: 1/s (frequency in Hz)
250+
let perSecond = ProvidedMeasureBuilder.Inverse(s)
251+
252+
// Dimensionless
253+
let one = ProvidedMeasureBuilder.One
254+
```
255+
256+
### Non-Standard (Custom) Units
257+
258+
To expose a custom unit of measure (not from the SI library), define an erased
259+
`ProvidedTypeDefinition` and use it directly as a measure argument to `AnnotateType`:
260+
261+
```fsharp
262+
// Define a custom unit "USD" (US dollars)
263+
let dollar = ProvidedTypeDefinition(asm, ns, "USD", None, isErased = true)
264+
this.AddNamespace(ns, [ dollar ])
265+
266+
// Use the custom unit to annotate a type
267+
let floatDollar = ProvidedMeasureBuilder.AnnotateType(typeof<float>, [ dollar ])
268+
269+
let priceProp = ProvidedProperty("Price", floatDollar, getterCode = fun _ -> <@@ 9.99 @@>)
270+
myType.AddMember priceProp
271+
```
272+
273+
### Multiple Unit Arguments (Generic Types)
274+
275+
`AnnotateType` also supports generic types with multiple type arguments. For example, to produce
276+
`Vector<float, kg>`:
277+
278+
```fsharp
279+
let kg = ProvidedMeasureBuilder.SI "kg"
280+
// Suppose vectorType is the generic definition of Vector<'T, 'U>
281+
let annotated = ProvidedMeasureBuilder.AnnotateType(vectorType, [ typeof<float>; kg ])
282+
```
283+

tests/BasicErasedProvisionTests.fs

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,4 +593,127 @@ let ``check on-demand production of members``() =
593593
Assert.Equal(5, containersType.GetMethods(bindAll).Length) // 5 properties, 5 getters for properties
594594
Assert.Equal(5, containersType.GetProperties(bindAll).Length) // 5 properties, 5 getters for properties
595595
Assert.Equal(0, containersType.GetFields(bindAll).Length) // 5 properties, 5 getters for properties
596-
Assert.Equal(0, containersType.GetEvents(bindAll).Length) // 5 properties, 5 getters for properties
596+
Assert.Equal(0, containersType.GetEvents(bindAll).Length) // 5 properties, 5 getters for properties
597+
598+
// ---------------------------------------------------------------------------
599+
// Tests for type definition properties: nonNullable, hideObjectMethods
600+
// Addresses https://github.com/fsprojects/FSharp.TypeProviders.SDK/issues/170
601+
// ---------------------------------------------------------------------------
602+
603+
[<Fact>]
604+
let ``nonNullable=true sets NonNullable property and adds AllowNullLiteralAttribute``() =
605+
let refs = Targets.DotNetStandard20FSharpRefs()
606+
let config = Testing.MakeSimulatedTypeProviderConfig(resolutionFolder=__SOURCE_DIRECTORY__, runtimeAssembly="whatever.dll", runtimeAssemblyRefs=refs)
607+
use _tp = new TypeProviderForNamespaces(config)
608+
let asm = Assembly.GetExecutingAssembly()
609+
610+
let t = ProvidedTypeDefinition(asm, "Test.Namespace", "NonNullableType", Some typeof<obj>, nonNullable = true)
611+
Assert.True(t.NonNullable, "NonNullable property should be true")
612+
let attrs = t.GetCustomAttributesData()
613+
Assert.True(
614+
attrs |> Seq.exists (fun a -> a.Constructor.DeclaringType.Name = typeof<AllowNullLiteralAttribute>.Name),
615+
"Expected AllowNullLiteralAttribute to be present when nonNullable=true")
616+
617+
[<Fact>]
618+
let ``nonNullable=false (default) does not add AllowNullLiteralAttribute``() =
619+
let refs = Targets.DotNetStandard20FSharpRefs()
620+
let config = Testing.MakeSimulatedTypeProviderConfig(resolutionFolder=__SOURCE_DIRECTORY__, runtimeAssembly="whatever.dll", runtimeAssemblyRefs=refs)
621+
use _tp = new TypeProviderForNamespaces(config)
622+
let asm = Assembly.GetExecutingAssembly()
623+
624+
let t = ProvidedTypeDefinition(asm, "Test.Namespace", "NullableType", Some typeof<obj>)
625+
Assert.False(t.NonNullable, "NonNullable property should default to false")
626+
let attrs = t.GetCustomAttributesData()
627+
Assert.False(
628+
attrs |> Seq.exists (fun a -> a.Constructor.DeclaringType.Name = typeof<AllowNullLiteralAttribute>.Name),
629+
"AllowNullLiteralAttribute should not be present when nonNullable is not set")
630+
631+
[<Fact>]
632+
let ``hideObjectMethods=true sets HideObjectMethods property``() =
633+
let refs = Targets.DotNetStandard20FSharpRefs()
634+
let config = Testing.MakeSimulatedTypeProviderConfig(resolutionFolder=__SOURCE_DIRECTORY__, runtimeAssembly="whatever.dll", runtimeAssemblyRefs=refs)
635+
use _tp = new TypeProviderForNamespaces(config)
636+
let asm = Assembly.GetExecutingAssembly()
637+
638+
let t = ProvidedTypeDefinition(asm, "Test.Namespace", "HiddenObjectType", Some typeof<obj>, hideObjectMethods = true)
639+
Assert.True(t.HideObjectMethods, "HideObjectMethods should be true when hideObjectMethods=true")
640+
641+
[<Fact>]
642+
let ``hideObjectMethods=false (default) leaves HideObjectMethods false``() =
643+
let refs = Targets.DotNetStandard20FSharpRefs()
644+
let config = Testing.MakeSimulatedTypeProviderConfig(resolutionFolder=__SOURCE_DIRECTORY__, runtimeAssembly="whatever.dll", runtimeAssemblyRefs=refs)
645+
use _tp = new TypeProviderForNamespaces(config)
646+
let asm = Assembly.GetExecutingAssembly()
647+
648+
let t = ProvidedTypeDefinition(asm, "Test.Namespace", "NormalType", Some typeof<obj>)
649+
Assert.False(t.HideObjectMethods, "HideObjectMethods should default to false")
650+
651+
[<Fact>]
652+
let ``AddCustomAttribute on method is visible``() =
653+
let asm = Assembly.GetExecutingAssembly()
654+
let t = ProvidedTypeDefinition(asm, "Test.Namespace", "TypeWithAttrMethod", Some typeof<obj>)
655+
let m = ProvidedMethod("OldMethod", [], typeof<unit>, invokeCode = fun _ -> <@@ () @@>)
656+
m.AddCustomAttribute {
657+
new CustomAttributeData() with
658+
member _.Constructor = typeof<ObsoleteAttribute>.GetConstructor([| typeof<string> |])
659+
member _.ConstructorArguments = [| CustomAttributeTypedArgument(typeof<string>, "use NewMethod" :> obj) |] :> _
660+
member _.NamedArguments = [||] :> _
661+
}
662+
t.AddMember m
663+
let mi = t.GetMethod("OldMethod")
664+
Assert.NotNull(mi)
665+
let methodAttrs = mi.GetCustomAttributesData()
666+
Assert.True(
667+
methodAttrs |> Seq.exists (fun a -> a.Constructor.DeclaringType.Name = typeof<ObsoleteAttribute>.Name),
668+
"Expected ObsoleteAttribute on method")
669+
670+
// ---------------------------------------------------------------------------
671+
// Tests for ProvidedMeasureBuilder: SI units, compound units, annotation
672+
// Addresses https://github.com/fsprojects/FSharp.TypeProviders.SDK/issues/67
673+
// ---------------------------------------------------------------------------
674+
675+
[<Fact>]
676+
let ``ProvidedMeasureBuilder SI symbol creates a FSharpTypeAbbreviation``() =
677+
let kg = ProvidedMeasureBuilder.SI "kg"
678+
match kg with
679+
| :? ProvidedTypeSymbol as sym ->
680+
Assert.True(sym.IsFSharpTypeAbbreviation, "SI 'kg' should be a FSharpTypeAbbreviation")
681+
| _ -> failwith "Expected ProvidedTypeSymbol for 'kg'"
682+
683+
[<Fact>]
684+
let ``ProvidedMeasureBuilder AnnotateType produces IsFSharpUnitAnnotated``() =
685+
let kg = ProvidedMeasureBuilder.SI "kg"
686+
let floatKg = ProvidedMeasureBuilder.AnnotateType(typeof<float>, [ kg ])
687+
match floatKg with
688+
| :? ProvidedTypeSymbol as sym ->
689+
Assert.True(sym.IsFSharpUnitAnnotated, "float<kg> should be IsFSharpUnitAnnotated")
690+
| _ -> failwith "Expected ProvidedTypeSymbol for annotated type"
691+
692+
[<Fact>]
693+
let ``ProvidedMeasureBuilder compound units Product, Ratio, Square, Inverse, One are non-null``() =
694+
let kg = ProvidedMeasureBuilder.SI "kg"
695+
let m = ProvidedMeasureBuilder.SI "m"
696+
let s = ProvidedMeasureBuilder.SI "s"
697+
698+
Assert.NotNull(ProvidedMeasureBuilder.Product(kg, m))
699+
Assert.NotNull(ProvidedMeasureBuilder.Ratio(kg, m))
700+
Assert.NotNull(ProvidedMeasureBuilder.Square(m))
701+
Assert.NotNull(ProvidedMeasureBuilder.Inverse(s))
702+
Assert.NotNull(ProvidedMeasureBuilder.One)
703+
704+
// float<m/s²> annotated type should also be IsFSharpUnitAnnotated
705+
let accel = ProvidedMeasureBuilder.Ratio(m, ProvidedMeasureBuilder.Square(s))
706+
let floatAccel = ProvidedMeasureBuilder.AnnotateType(typeof<float>, [ accel ])
707+
match floatAccel with
708+
| :? ProvidedTypeSymbol as sym ->
709+
Assert.True(sym.IsFSharpUnitAnnotated, "float<m/s²> should be IsFSharpUnitAnnotated")
710+
| _ -> failwith "Expected ProvidedTypeSymbol for float<m/s²>"
711+
712+
[<Fact>]
713+
let ``ProvidedMeasureBuilder SI name (lowercase) creates a FSharpTypeAbbreviation``() =
714+
let kelvin = ProvidedMeasureBuilder.SI "kelvin"
715+
Assert.NotNull(kelvin)
716+
match kelvin with
717+
| :? ProvidedTypeSymbol as sym ->
718+
Assert.True(sym.IsFSharpTypeAbbreviation, "SI 'kelvin' should be a FSharpTypeAbbreviation")
719+
| _ -> failwith "Expected ProvidedTypeSymbol for 'kelvin'"

0 commit comments

Comments
 (0)