diff --git a/ApiSurface/ApiMember.fs b/ApiSurface/ApiMember.fs index d4f5c71..4f885b1 100644 --- a/ApiSurface/ApiMember.fs +++ b/ApiSurface/ApiMember.fs @@ -68,7 +68,7 @@ module ApiMember = let typeString = fieldType |> Type.toFullName if fieldInfo.IsLiteral then - let value = m.DeclaringType.GetField(m.Name).GetValue null |> string + let value = fieldInfo.GetValue null |> string // Don't print `= ` for empty strings, as many editors/Git hooks trim trailing whitespace if value = "" then diff --git a/ApiSurface/ApiSurface.fs b/ApiSurface/ApiSurface.fs index 89f93a2..24ebd0f 100644 --- a/ApiSurface/ApiSurface.fs +++ b/ApiSurface/ApiSurface.fs @@ -30,9 +30,7 @@ module ApiSurface = /// In the rare case that you have several different baselines depending on what framework you are running under, /// you can use a more specific name for your baseline files. - let private frameworkBaselineFile = - let desc = RuntimeInformation.FrameworkDescription - + let internal frameworkBaselineFileFor (desc : string) = if desc.StartsWith (".NET Core", StringComparison.Ordinal) then "SurfaceBaseline-NetCore.txt" elif desc.StartsWith (".NET Framework", StringComparison.Ordinal) then @@ -42,10 +40,13 @@ module ApiSurface = let frameworkNumber = Regex(@"^\.NET ([0-9]+)\.").Match desc if frameworkNumber.Success then - sprintf "SurfaceBaseline-Net%s.txt" frameworkNumber.Groups.[0].Value + sprintf "SurfaceBaseline-Net%s.txt" frameworkNumber.Groups.[1].Value else failwithf "Unknown runtime framework: %s" desc + let private frameworkBaselineFile = + RuntimeInformation.FrameworkDescription |> frameworkBaselineFileFor + let surfaces (assembly : Assembly) = assembly.GetManifestResourceNames () |> Seq.choose Option.ofObj diff --git a/ApiSurface/ApiSurface.fsi b/ApiSurface/ApiSurface.fsi index 9fc70dc..a239862 100644 --- a/ApiSurface/ApiSurface.fsi +++ b/ApiSurface/ApiSurface.fsi @@ -16,6 +16,8 @@ type internal Version = [] module ApiSurface = + val internal frameworkBaselineFileFor : string -> string + /// Read the SurfaceBaseline.txt embedded resource from the given assembly. [] val ofAssemblyBaseline : Assembly -> ApiSurface diff --git a/ApiSurface/DocCoverage.fs b/ApiSurface/DocCoverage.fs index 0211fb4..95c53d3 100644 --- a/ApiSurface/DocCoverage.fs +++ b/ApiSurface/DocCoverage.fs @@ -27,13 +27,21 @@ module DocCoverage = let isPublic (memberInfo : MemberInfo) : bool = (isNull memberInfo.DeclaringType || memberInfo.DeclaringType.IsVisible) && match memberInfo.MemberType with - | MemberTypes.Method - | MemberTypes.Constructor -> let i = memberInfo :?> MethodInfo in i.IsPublic - | MemberTypes.Event -> let i = memberInfo :?> EventInfo in i.AddMethod.IsPublic + | MemberTypes.Method -> let i = memberInfo :?> MethodInfo in i.IsPublic + | MemberTypes.Constructor -> let i = memberInfo :?> ConstructorInfo in i.IsPublic + | MemberTypes.Event -> + let i = memberInfo :?> EventInfo + + (not (isNull i.AddMethod) && i.AddMethod.IsPublic) + || (not (isNull i.RemoveMethod) && i.RemoveMethod.IsPublic) | MemberTypes.Field -> let i = memberInfo :?> FieldInfo in i.IsPublic | MemberTypes.TypeInfo - | MemberTypes.NestedType -> let i = memberInfo :?> TypeInfo in i.IsPublic - | MemberTypes.Property -> let i = memberInfo :?> PropertyInfo in i.GetMethod.IsPublic + | MemberTypes.NestedType -> let i = memberInfo :?> Type in i.IsVisible + | MemberTypes.Property -> + let i = memberInfo :?> PropertyInfo + + (not (isNull i.GetMethod) && i.GetMethod.IsPublic) + || (not (isNull i.SetMethod) && i.SetMethod.IsPublic) | memberType -> failwithf "Unrecognised MemberType: %O" memberType let paramInfoToString (pi : ParameterInfo) : string = @@ -263,7 +271,10 @@ module DocCoverage = let publicMembers = publicMembers |> Seq.map (fun (n, c, _) -> n, c) |> Set.ofSeq let nonPublicMembers = - nonPublicMembers |> Seq.map (fun (n, c, _) -> n, c) |> Set.ofSeq + nonPublicMembers + |> Seq.map (fun (n, c, _) -> n, c) + |> Set.ofSeq + |> fun members -> Set.difference members publicMembers DocCoverage (Path.GetFileName assembly.Location, Members.MixedAccess (publicMembers, nonPublicMembers)) diff --git a/ApiSurface/DocCoverage.fsi b/ApiSurface/DocCoverage.fsi index 8aa51c6..cc618d3 100644 --- a/ApiSurface/DocCoverage.fsi +++ b/ApiSurface/DocCoverage.fsi @@ -9,6 +9,8 @@ type DocCoverage [] module DocCoverage = + val internal isPublic : MemberInfo -> bool + /// Map the exposed types and members of an assembly into a list of /// member names expected to be present in the corresponding .XML /// documentation file. diff --git a/ApiSurface/Test/ApiSurface.Test.fsproj b/ApiSurface/Test/ApiSurface.Test.fsproj index 783f233..44068f5 100644 --- a/ApiSurface/Test/ApiSurface.Test.fsproj +++ b/ApiSurface/Test/ApiSurface.Test.fsproj @@ -9,6 +9,7 @@ + diff --git a/ApiSurface/Test/TestApiSurface.fs b/ApiSurface/Test/TestApiSurface.fs index 6d59afb..8f73eda 100644 --- a/ApiSurface/Test/TestApiSurface.fs +++ b/ApiSurface/Test/TestApiSurface.fs @@ -89,3 +89,9 @@ module TestApiSurface = let expected = Sample.publicSurface |> Set.add "Bar" |> Set.add "Foo" actual |> shouldEqual expected + + [] + [] + [] + let ``Test framework baseline filename`` desc expected = + desc |> ApiSurface.frameworkBaselineFileFor |> shouldEqual expected diff --git a/ApiSurface/Test/TestReflectionCorrectness.fs b/ApiSurface/Test/TestReflectionCorrectness.fs new file mode 100644 index 0000000..76dfa89 --- /dev/null +++ b/ApiSurface/Test/TestReflectionCorrectness.fs @@ -0,0 +1,109 @@ +namespace ApiSurface.Test + +open System +open System.Reflection +open System.Reflection.Emit +open NUnit.Framework +open FsUnitTyped +open ApiSurface + +[] +module TestReflectionCorrectness = + + let private reflectionFixture = + lazy + (let assemblyName = AssemblyName "ApiSurface.Test.ReflectionFixture" + + let assembly = + AssemblyBuilder.DefineDynamicAssembly (assemblyName, AssemblyBuilderAccess.Run) + + let moduleBuilder = assembly.DefineDynamicModule assemblyName.Name + + let typeBuilder = + moduleBuilder.DefineType ("ReflectionFixture", TypeAttributes.Public) + + typeBuilder.DefineDefaultConstructor MethodAttributes.Public |> ignore + + let emitReturn (methodBuilder : MethodBuilder) = + methodBuilder.GetILGenerator().Emit OpCodes.Ret + + let propertySetter = + typeBuilder.DefineMethod ( + "set_WriteOnly", + MethodAttributes.Public + ||| MethodAttributes.SpecialName + ||| MethodAttributes.HideBySig, + typeof, + [| typeof |] + ) + + emitReturn propertySetter + + let property = + typeBuilder.DefineProperty ("WriteOnly", PropertyAttributes.None, typeof, Type.EmptyTypes) + + property.SetSetMethod propertySetter + + let eventRemover = + typeBuilder.DefineMethod ( + "remove_RemoveOnly", + MethodAttributes.Public + ||| MethodAttributes.SpecialName + ||| MethodAttributes.HideBySig, + typeof, + [| typeof |] + ) + + emitReturn eventRemover + + let event = + typeBuilder.DefineEvent ("RemoveOnly", EventAttributes.None, typeof) + + event.SetRemoveOnMethod eventRemover + + let literal = + typeBuilder.DefineField ( + "HiddenLiteral", + typeof, + FieldAttributes.Private ||| FieldAttributes.Static ||| FieldAttributes.Literal + ) + + literal.SetConstant 42 + + let nestedType = + typeBuilder.DefineNestedType ("Nested", TypeAttributes.NestedPublic) + + nestedType.CreateTypeInfo () |> ignore + + typeBuilder.CreateTypeInfo().AsType ()) + + [] + let ``isPublic handles constructors`` () = + reflectionFixture.Value.GetConstructors().[0] + |> DocCoverage.isPublic + |> shouldEqual true + + [] + let ``isPublic handles setter-only properties`` () = + reflectionFixture.Value.GetProperty "WriteOnly" + |> DocCoverage.isPublic + |> shouldEqual true + + [] + let ``isPublic handles remove-only events`` () = + reflectionFixture.Value.GetEvent "RemoveOnly" + |> DocCoverage.isPublic + |> shouldEqual true + + [] + let ``isPublic handles nested public types`` () = + reflectionFixture.Value.GetNestedType "Nested" + |> DocCoverage.isPublic + |> shouldEqual true + + [] + let ``print handles private literals`` () = + reflectionFixture.Value.GetField ("HiddenLiteral", BindingFlags.NonPublic ||| BindingFlags.Static) + |> ApiMember.ofMemberInfo + |> ApiMember.print + |> shouldEqual "ReflectionFixture.HiddenLiteral [static field]: int = 42"