diff --git a/CHANGELOG.md b/CHANGELOG.md index 96fa440..77b2401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.37.0] - 2026-02-27 + +### Changed + +- We now expose stable backward-compatible APIs, independent of the FSharp.Compiler.Service API, which you are encouraged to use. If you use the stable API, then upgrading FSharp.Analyzers.SDK will not require you to rebuild your analyzers until you need to consume features from later versions of the SDK. (For example, if a new F# language version introduces new syntax that you wish to parse, then you will have to upgrade your analyzer to a later stable API version which understands that new syntax.) + ## [0.36.0] - 2026-02-02 ### Added diff --git a/Directory.Packages.props b/Directory.Packages.props index 4db3dab..f49a96c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,6 +7,8 @@ + + diff --git a/FSharp.Analyzers.SDK.sln b/FSharp.Analyzers.SDK.sln index ec613c5..cd8ea4d 100644 --- a/FSharp.Analyzers.SDK.sln +++ b/FSharp.Analyzers.SDK.sln @@ -40,6 +40,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Analyzers.SDK.Testin EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FSharp.Analyzers.Build", "src\FSharp.Analyzers.Build\FSharp.Analyzers.Build.csproj", "{34AD5A2D-5FDE-4A03-8AC5-100F54E6D2DF}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "OptionAnalyzer.V1", "samples\OptionAnalyzer.V1\OptionAnalyzer.V1.fsproj", "{71366770-A3E2-4B3B-9FB5-391BDDAD2AFB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -49,9 +51,6 @@ Global Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {C1D38B7A-0193-46AA-B033-ADBBF642AAA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C1D38B7A-0193-46AA-B033-ADBBF642AAA0}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -125,6 +124,21 @@ Global {34AD5A2D-5FDE-4A03-8AC5-100F54E6D2DF}.Release|x64.Build.0 = Release|Any CPU {34AD5A2D-5FDE-4A03-8AC5-100F54E6D2DF}.Release|x86.ActiveCfg = Release|Any CPU {34AD5A2D-5FDE-4A03-8AC5-100F54E6D2DF}.Release|x86.Build.0 = Release|Any CPU + {71366770-A3E2-4B3B-9FB5-391BDDAD2AFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71366770-A3E2-4B3B-9FB5-391BDDAD2AFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71366770-A3E2-4B3B-9FB5-391BDDAD2AFB}.Debug|x64.ActiveCfg = Debug|Any CPU + {71366770-A3E2-4B3B-9FB5-391BDDAD2AFB}.Debug|x64.Build.0 = Debug|Any CPU + {71366770-A3E2-4B3B-9FB5-391BDDAD2AFB}.Debug|x86.ActiveCfg = Debug|Any CPU + {71366770-A3E2-4B3B-9FB5-391BDDAD2AFB}.Debug|x86.Build.0 = Debug|Any CPU + {71366770-A3E2-4B3B-9FB5-391BDDAD2AFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71366770-A3E2-4B3B-9FB5-391BDDAD2AFB}.Release|Any CPU.Build.0 = Release|Any CPU + {71366770-A3E2-4B3B-9FB5-391BDDAD2AFB}.Release|x64.ActiveCfg = Release|Any CPU + {71366770-A3E2-4B3B-9FB5-391BDDAD2AFB}.Release|x64.Build.0 = Release|Any CPU + {71366770-A3E2-4B3B-9FB5-391BDDAD2AFB}.Release|x86.ActiveCfg = Release|Any CPU + {71366770-A3E2-4B3B-9FB5-391BDDAD2AFB}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {C1D38B7A-0193-46AA-B033-ADBBF642AAA0} = {95A9FA19-723D-4D2C-A936-F0B45656B0D6} @@ -135,5 +149,6 @@ Global {9A9AC3F8-E34B-4C30-A52A-A507D6E0CA01} = {0FE81935-26A8-45E1-A62E-5148C73BA6A2} {3C70D1B2-DDCE-439A-BAB2-AC6B2E0919D5} = {95A9FA19-723D-4D2C-A936-F0B45656B0D6} {34AD5A2D-5FDE-4A03-8AC5-100F54E6D2DF} = {95A9FA19-723D-4D2C-A936-F0B45656B0D6} + {71366770-A3E2-4B3B-9FB5-391BDDAD2AFB} = {0FE81935-26A8-45E1-A62E-5148C73BA6A2} EndGlobalSection EndGlobal diff --git a/samples/OptionAnalyzer.Test/OptionAnalyzer.Test.fsproj b/samples/OptionAnalyzer.Test/OptionAnalyzer.Test.fsproj index bb96317..186ddec 100644 --- a/samples/OptionAnalyzer.Test/OptionAnalyzer.Test.fsproj +++ b/samples/OptionAnalyzer.Test/OptionAnalyzer.Test.fsproj @@ -13,6 +13,7 @@ + @@ -21,12 +22,15 @@ + + + diff --git a/samples/OptionAnalyzer.Test/V1Tests.fs b/samples/OptionAnalyzer.Test/V1Tests.fs new file mode 100644 index 0000000..1b1559c --- /dev/null +++ b/samples/OptionAnalyzer.Test/V1Tests.fs @@ -0,0 +1,1040 @@ +module OptionAnalyzer.V1Tests + +#nowarn "57" + +open System.Collections.Generic +open System.Runtime.CompilerServices +open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.Symbols +open FSharp.Compiler.Text +open NUnit.Framework +open FsCheck.NUnit +open FSharp.Analyzers.SDK +open FSharp.Analyzers.SDK.Testing +open FSharp.Analyzers.SDK.AdapterV1 +open OptionAnalyzer.TestHelpers + +// ─── Oracle tests: V1 and legacy analyzers must agree ────────────── + +module OracleTests = + + let mutable projectOptions: FSharpProjectOptions = Unchecked.defaultof<_> + + [] + let Setup () = + task { + let! opts = mkTestProjectOptions () + projectOptions <- opts + } + + [] + let ``V1 and legacy agree on single Option.Value`` () = + async { + let source = + """ +module M + +let f () = + let option : Option = None + option.Value + """ + + let ctx = getContext projectOptions source + let! legacyMsgs = OptionAnalyzer.optionValueAnalyzer ctx + + let v1Ctx = contextToV1 ctx + let! v1Raw = OptionAnalyzer.V1.optionValueAnalyzer v1Ctx + + let v1Msgs = + v1Raw + |> List.map messageFromV1 + + Assert.AreEqual(legacyMsgs.Length, v1Msgs.Length, "message count") + + for legacy, v1 in List.zip legacyMsgs v1Msgs do + Assert.AreEqual(legacy, v1) + } + + [] + let ``V1 and legacy agree on multiple Option.Value usages`` () = + async { + let source = + """ +module M + +let f () = + let a : Option = None + let b : Option = None + let _ = a.Value + let _ = b.Value + a.Value + b.Value.Length + """ + + let ctx = getContext projectOptions source + let! legacyMsgs = OptionAnalyzer.optionValueAnalyzer ctx + + let v1Ctx = contextToV1 ctx + let! v1Raw = OptionAnalyzer.V1.optionValueAnalyzer v1Ctx + + let v1Msgs = + v1Raw + |> List.map messageFromV1 + + let sort msgs = + msgs + |> List.sortBy (fun (m: Message) -> m.Range.StartLine, m.Range.StartColumn) + + Assert.AreEqual(legacyMsgs.Length, v1Msgs.Length, "message count") + + for legacy, v1 in List.zip (sort legacyMsgs) (sort v1Msgs) do + Assert.AreEqual(legacy, v1) + } + + [] + let ``V1 and legacy agree on clean input`` () = + async { + let source = + """ +module M + +let f () = + let x = Some 42 + match x with + | Some v -> v + | None -> 0 + """ + + let ctx = getContext projectOptions source + let! legacyMsgs = OptionAnalyzer.optionValueAnalyzer ctx + + let v1Ctx = contextToV1 ctx + let! v1Raw = OptionAnalyzer.V1.optionValueAnalyzer v1Ctx + + let v1Msgs = + v1Raw + |> List.map messageFromV1 + + Assert.IsEmpty legacyMsgs + Assert.IsEmpty v1Msgs + } + +// ─── Client integration tests ────────────────────────────────────── + +module ClientIntegrationTests = + + let mutable projectOptions: FSharpProjectOptions = Unchecked.defaultof<_> + + [] + let Setup () = + task { + let! opts = mkTestProjectOptions () + projectOptions <- opts + } + + [] + let ``LoadAnalyzers includes V1 analyzers`` () = + let _client, stats = loadAnalyzers () + Assert.That(stats.AnalyzerNames, Does.Contain "OptionAnalyzer") + Assert.That(stats.AnalyzerNames, Does.Contain "InferredReturnAnalyzer") + + [] + let ``RunAnalyzersSafely produces results from both legacy and V1`` () = + async { + let ctx = getContext projectOptions ClientTestSources.optionValue + let client, _stats = loadAnalyzers () + let! results = client.RunAnalyzersSafely(ctx) + + let optionResults = + results + |> List.filter (fun r -> r.AnalyzerName = "OptionAnalyzer") + |> List.choose (fun r -> + match r.Output with + | Ok msgs -> Some msgs + | Error ex -> + Assert.Fail($"Analyzer result was Error: %A{ex}") + None + ) + + // Both legacy and V1 OptionAnalyzer should produce results + Assert.That( + optionResults + |> List.filter (fun msgs -> + msgs + |> List.exists (fun m -> m.Code = "OV001") + ) + |> List.length, + Is.GreaterThanOrEqualTo 2, + "Expected OV001 results from both legacy and V1 OptionAnalyzer" + ) + + for msgs in optionResults do + for msg in msgs do + Assert.AreEqual("OV001", msg.Code) + Assert.AreEqual(Severity.Warning, msg.Severity) + } + + [] + let ``LoadAnalyzers discovers V1 analyzer with inferred return type`` () = + let _client, stats = loadAnalyzers () + + Assert.That( + stats.AnalyzerNames, + Does.Contain "InferredReturnAnalyzer", + "V1 analyzer with inferred Async<'a list> return type should be discovered" + ) + + [] + let ``LoadAnalyzers discovers V1 analyzer with explicit generic parameter`` () = + let _client, stats = loadAnalyzers () + + Assert.That( + stats.AnalyzerNames, + Does.Contain "ExplicitGenericAnalyzer", + "V1 analyzer with explicit generic parameter should be discovered" + ) + + [] + let ``InferredReturnAnalyzer runs without error`` () = + async { + let ctx = getContext projectOptions ClientTestSources.optionValue + let client, _stats = loadAnalyzers () + let! results = client.RunAnalyzersSafely(ctx) + + let inferredResults = + results + |> List.filter (fun r -> r.AnalyzerName = "InferredReturnAnalyzer") + + Assert.That( + inferredResults, + Is.Not.Empty, + "InferredReturnAnalyzer should produce a result" + ) + + for r in inferredResults do + match r.Output with + | Ok _ -> () + | Error ex -> Assert.Fail($"InferredReturnAnalyzer returned Error: %A{ex}") + } + + [] + let ``ExplicitGenericAnalyzer runs without error`` () = + async { + let ctx = getContext projectOptions ClientTestSources.optionValue + let client, _stats = loadAnalyzers () + let! results = client.RunAnalyzersSafely(ctx) + + let explicitResults = + results + |> List.filter (fun r -> r.AnalyzerName = "ExplicitGenericAnalyzer") + + Assert.That( + explicitResults, + Is.Not.Empty, + "ExplicitGenericAnalyzer should produce a result" + ) + + for r in explicitResults do + match r.Output with + | Ok _ -> () + | Error ex -> Assert.Fail($"ExplicitGenericAnalyzer returned Error: %A{ex}") + } + +// ─── Adapter unit tests ──────────────────────────────────────────── + +module AdapterTests = + + // Abbreviations for V1 types to avoid ambiguity with SDK types. + type V1Severity = FSharp.Analyzers.SDK.V1.Severity + type V1SourceRange = FSharp.Analyzers.SDK.V1.SourceRange + type V1Fix = FSharp.Analyzers.SDK.V1.Fix + type V1Message = FSharp.Analyzers.SDK.V1.Message + type V1AnalyzerIgnoreRange = FSharp.Analyzers.SDK.V1.AnalyzerIgnoreRange + + let mutable projectOptions: FSharpProjectOptions = Unchecked.defaultof<_> + + [] + let Setup () = + task { + let! opts = mkTestProjectOptions () + projectOptions <- opts + } + + [] + let ``rangeToV1 then rangeFromV1 preserves all fields`` + (sl: int) + (sc: int) + (ls: int) + (ec: int) + = + // Constrain to valid Position.mkPos inputs (line >= 1, col >= 0). + // Mask sign bit to avoid abs(Int32.MinValue) overflow. + let startLine = + (sl + &&& 0x7FFFFFFF) % 10000 + + 1 + + let startCol = + (sc + &&& 0x7FFFFFFF) % 200 + + let endLine = + startLine + + (ls + &&& 0x7FFFFFFF) % 100 + + let endCol = + (ec + &&& 0x7FFFFFFF) % 200 + + let r = + Range.mkRange + "Test.fs" + (Position.mkPos startLine startCol) + (Position.mkPos endLine endCol) + + let rt = rangeFromV1 (rangeToV1 r) + + rt.FileName = r.FileName + && rt.StartLine = r.StartLine + && rt.StartColumn = r.StartColumn + && rt.EndLine = r.EndLine + && rt.EndColumn = r.EndColumn + + [] + let ``severityFromV1 maps all cases correctly`` () = + Assert.AreEqual(Severity.Info, severityFromV1 V1Severity.Info) + Assert.AreEqual(Severity.Hint, severityFromV1 V1Severity.Hint) + Assert.AreEqual(Severity.Warning, severityFromV1 V1Severity.Warning) + Assert.AreEqual(Severity.Error, severityFromV1 V1Severity.Error) + + [] + let ``messageFromV1 preserves all fields`` () = + let v1Range: V1SourceRange = + { + FileName = "Test.fs" + StartLine = 10 + StartColumn = 4 + EndLine = 10 + EndColumn = 16 + } + + let v1Fix: V1Fix = + { + FromRange = v1Range + FromText = "old" + ToText = "new" + } + + let v1Msg: V1Message = + { + Type = "TestType" + Message = "TestMessage" + Code = "T001" + Severity = V1Severity.Error + Range = v1Range + Fixes = [ v1Fix ] + } + + let sdkMsg = messageFromV1 v1Msg + + Assert.AreEqual("TestType", sdkMsg.Type) + Assert.AreEqual("TestMessage", sdkMsg.Message) + Assert.AreEqual("T001", sdkMsg.Code) + Assert.AreEqual(Severity.Error, sdkMsg.Severity) + Assert.AreEqual("Test.fs", sdkMsg.Range.FileName) + Assert.AreEqual(10, sdkMsg.Range.StartLine) + Assert.AreEqual(4, sdkMsg.Range.StartColumn) + Assert.AreEqual(10, sdkMsg.Range.EndLine) + Assert.AreEqual(16, sdkMsg.Range.EndColumn) + Assert.AreEqual(1, sdkMsg.Fixes.Length) + Assert.AreEqual("old", sdkMsg.Fixes[0].FromText) + Assert.AreEqual("new", sdkMsg.Fixes[0].ToText) + Assert.AreEqual(10, sdkMsg.Fixes[0].FromRange.StartLine) + + [] + let ``analyzerIgnoreRangeToV1 maps all cases`` () = + Assert.AreEqual( + V1AnalyzerIgnoreRange.File, + analyzerIgnoreRangeToV1 AnalyzerIgnoreRange.File + ) + + Assert.AreEqual( + V1AnalyzerIgnoreRange.Range(3, 7), + analyzerIgnoreRangeToV1 (AnalyzerIgnoreRange.Range(3, 7)) + ) + + Assert.AreEqual( + V1AnalyzerIgnoreRange.NextLine 5, + analyzerIgnoreRangeToV1 (AnalyzerIgnoreRange.NextLine 5) + ) + + Assert.AreEqual( + V1AnalyzerIgnoreRange.CurrentLine 10, + analyzerIgnoreRangeToV1 (AnalyzerIgnoreRange.CurrentLine 10) + ) + + [] + let ``contextToV1 preserves filename and source text`` () = + let source = + """ +module M +let x = 1 +""" + + let ctx = getContext projectOptions source + let v1Ctx = contextToV1 ctx + + Assert.AreEqual(ctx.FileName, v1Ctx.FileName) + + let expectedText = ctx.SourceText.GetSubTextString(0, ctx.SourceText.Length) + Assert.AreEqual(expectedText, v1Ctx.SourceText) + + [] + let ``contextToV1 preserves project options`` () = + let source = + """ +module M +let x = 1 +""" + + let ctx = getContext projectOptions source + let v1Ctx = contextToV1 ctx + + Assert.AreEqual(ctx.ProjectOptions.ProjectFileName, v1Ctx.ProjectOptions.ProjectFileName) + + Assert.AreEqual(ctx.ProjectOptions.SourceFiles, v1Ctx.ProjectOptions.SourceFiles) + + Assert.AreEqual(ctx.ProjectOptions.OtherOptions, v1Ctx.ProjectOptions.OtherOptions) + + // ─── Helpers for typed-tree inspection ───────────────────────────── + + /// Extract all top-level MemberOrFunctionOrValue bindings from declarations, + /// descending one level into Entity (module) wrappers. + let private findBindings (decls: FSharp.Analyzers.SDK.V1.TypedDeclaration list) = + decls + |> List.collect (fun d -> + match d with + | FSharp.Analyzers.SDK.V1.TypedDeclaration.Entity(_, subDecls) -> subDecls + | other -> [ other ] + ) + |> List.choose (fun d -> + match d with + | FSharp.Analyzers.SDK.V1.TypedDeclaration.MemberOrFunctionOrValue(v, _, _) -> Some v + | _ -> None + ) + + let private getBinding name decls = + findBindings decls + |> List.find (fun v -> v.DisplayName = name) + + let private convertSource source = + let ctx = getContext projectOptions source + let v1Ctx = contextToV1 ctx + let handle = v1Ctx.TypedTree.Value + FSharp.Analyzers.SDK.V1.TASTCollecting.convertTast handle + + // ─── TypedTreeHandle opacity tests ───────────────────────────────── + + /// Recursively check whether a System.Type references any type from the + /// FSharp.Compiler assembly (FCS), including through generic arguments. + let rec private referencesFCS (ty: System.Type) = + if + isNull ty + || isNull ty.FullName + then + false + elif ty.FullName.StartsWith("FSharp.Compiler", System.StringComparison.Ordinal) then + true + elif ty.IsGenericType then + ty.GetGenericArguments() + |> Array.exists referencesFCS + elif ty.IsArray then + referencesFCS (ty.GetElementType()) + elif ty.IsByRef then + referencesFCS (ty.GetElementType()) + else + false + + [] + let ``no V1 public type exposes FCS types in its public surface`` () = + let v1Assembly = typeof.Assembly + + let v1Types = + v1Assembly.GetTypes() + |> Array.filter (fun t -> + t.IsPublic + && not (isNull t.Namespace) + && t.Namespace.StartsWith( + "FSharp.Analyzers.SDK.V1", + System.StringComparison.Ordinal + ) + ) + + // Sanity: we should find a reasonable number of V1 types. + NUnit.Framework.Assert.That( + v1Types.Length, + Is.GreaterThanOrEqualTo(10), + "Should find V1 types" + ) + + for t in v1Types do + let publicMembers = + t.GetMembers( + System.Reflection.BindingFlags.Public + ||| System.Reflection.BindingFlags.Instance + ||| System.Reflection.BindingFlags.Static + ||| System.Reflection.BindingFlags.DeclaredOnly + ) + + for m in publicMembers do + let memberTypes = + match m with + | :? System.Reflection.PropertyInfo as p -> [ p.PropertyType ] + | :? System.Reflection.FieldInfo as f -> [ f.FieldType ] + | :? System.Reflection.MethodInfo as mi -> + mi.ReturnType + :: (mi.GetParameters() + |> Array.toList + |> List.map (fun p -> p.ParameterType)) + | :? System.Reflection.ConstructorInfo as ci -> + ci.GetParameters() + |> Array.toList + |> List.map (fun p -> p.ParameterType) + | _ -> [] + + for mt in memberTypes do + NUnit.Framework.Assert.That( + referencesFCS mt, + Is.False, + $"Type '{t.Name}', member '{m.Name}' exposes FCS type '{mt.FullName}'" + ) + + // ─── Type conversion cache-collision tests ───────────────────────── + + [] + let ``typeToV1 distinguishes list from list`` () = + let source = + """ +module M + +let a : list = [] +let b : list = [] +""" + + let decls = convertSource source + let aInfo = getBinding "a" decls + let bInfo = getBinding "b" decls + + // Both should have the same base list type + Assert.AreEqual( + aInfo.FullType.BasicQualifiedName, + bInfo.FullType.BasicQualifiedName, + "Both should share the same base list type name" + ) + + // But their generic arguments must differ + Assert.AreEqual(1, aInfo.FullType.GenericArguments.Length, "list has 1 generic arg") + Assert.AreEqual(1, bInfo.FullType.GenericArguments.Length, "list has 1 generic arg") + + Assert.AreNotEqual( + aInfo.FullType.GenericArguments.[0].BasicQualifiedName, + bInfo.FullType.GenericArguments.[0].BasicQualifiedName, + "list and list must have different generic arguments" + ) + + [] + let ``typeToV1 preserves generic argument order for Result vs Result`` + () + = + let source = + """ +module M + +let a : Result = Ok 1 +let b : Result = Ok "" +""" + + let decls = convertSource source + let aInfo = getBinding "a" decls + let bInfo = getBinding "b" decls + + // Same base type + Assert.AreEqual( + aInfo.FullType.BasicQualifiedName, + bInfo.FullType.BasicQualifiedName, + "Both should share the same base Result type name" + ) + + Assert.AreEqual(2, aInfo.FullType.GenericArguments.Length) + Assert.AreEqual(2, bInfo.FullType.GenericArguments.Length) + + // a's first arg (int) should match b's second arg (int), and vice versa + Assert.AreEqual( + aInfo.FullType.GenericArguments.[0].BasicQualifiedName, + bInfo.FullType.GenericArguments.[1].BasicQualifiedName, + "First arg of Result should equal second arg of Result" + ) + + Assert.AreEqual( + aInfo.FullType.GenericArguments.[1].BasicQualifiedName, + bInfo.FullType.GenericArguments.[0].BasicQualifiedName, + "Second arg of Result should equal first arg of Result" + ) + + [] + let ``typeToV1 distinguishes nested generic instantiations`` () = + let source = + """ +module M + +let a : list> = [] +let b : list> = [] +""" + + let decls = convertSource source + let aInfo = getBinding "a" decls + let bInfo = getBinding "b" decls + + // Outer list types have same BasicQualifiedName + Assert.AreEqual(aInfo.FullType.BasicQualifiedName, bInfo.FullType.BasicQualifiedName) + + // The inner list's generic argument must differ + let aInner = aInfo.FullType.GenericArguments.[0] + let bInner = bInfo.FullType.GenericArguments.[0] + + Assert.AreEqual( + aInner.BasicQualifiedName, + bInner.BasicQualifiedName, + "Inner lists share the same base type name" + ) + + Assert.AreNotEqual( + aInner.GenericArguments.[0].BasicQualifiedName, + bInner.GenericArguments.[0].BasicQualifiedName, + "list> and list> must differ at the innermost generic argument" + ) + + [] + let ``typeToV1 correctly converts two values with the same generic instantiation`` () = + let source = + """ +module M + +let a : list = [] +let b : list = [] +""" + + let decls = convertSource source + let aInfo = getBinding "a" decls + let bInfo = getBinding "b" decls + + // Both should be structurally equal + Assert.AreEqual(aInfo.FullType.BasicQualifiedName, bInfo.FullType.BasicQualifiedName) + + Assert.AreEqual( + aInfo.FullType.GenericArguments.[0].BasicQualifiedName, + bInfo.FullType.GenericArguments.[0].BasicQualifiedName, + "Two list values should have the same generic argument" + ) + +// ─── Cycle diagnostic tests ────────────────────────────────────── + +/// Simulate entityToV1's cache and traversal against the live FCS +/// entity/type graph to find what causes unbounded recursion. +module CycleDiagnosticTests = + + let mutable projectOptions: FSharpProjectOptions = Unchecked.defaultof<_> + + [] + let Setup () = + task { + let! opts = mkTestProjectOptions () + projectOptions <- opts + } + + let private refEq<'T when 'T: not struct> = + { new IEqualityComparer<'T> with + member _.Equals(x, y) = obj.ReferenceEquals(x, y) + member _.GetHashCode(x) = RuntimeHelpers.GetHashCode(x) + } + + let inline private safe def ([] f) = + try + f () + with _ -> + def + + /// Walk the FCS entity/type graph following entityToV1's exact + /// cache logic and field-evaluation order. Returns diagnostics + /// instead of crashing. + type WalkStats = + { + CallCount: int + MaxDepth: int + EntitiesByName: int + EntitiesByRef: int + NoNameEntityEncounters: int + /// Entities where FullName throws AND FCS returned a + /// *different* object reference for the same DisplayName. + DifferentRefNoNameEntities: (string * int) list + /// True when CallCount hit the safety limit. + HitLimit: bool + } + + let walkFrom (symbols: FSharpSymbolUse seq) : WalkStats = + // Simulate entityToV1's two-tier cache. + let nameCache = HashSet() + let refCache = HashSet(refEq) + let noNameByDisplay = Dictionary() + + let mutable callCount = 0 + let mutable maxDepth = 0 + let mutable noNameHits = 0 + let limit = 1_000_000 + let depthLimit = 400 + + let rec walkEntity depth (e: FSharpEntity) = + callCount <- + callCount + + 1 + + if depth > maxDepth then + maxDepth <- depth + + if + callCount + >= limit + || depth + >= depthLimit + then + () + else + let fn = safe None (fun () -> Some e.FullName) + + // Mirror the three-tier cache key from entityToV1: + // FullName -> AccessPath.DisplayName -> reference identity + let stableName = + match fn with + | Some _ -> fn + | None -> + safe + None + (fun () -> + let dn = e.DisplayName + + if System.String.IsNullOrEmpty(dn) then + None + else + let ap = e.AccessPath + + if System.String.IsNullOrEmpty(ap) then + None + else + Some( + ap + + "." + + dn + ) + ) + + let isHit = + match stableName with + | Some name -> not (nameCache.Add(name)) + | None -> not (refCache.Add(e)) + + if stableName.IsNone then + noNameHits <- + noNameHits + + 1 + + let dn = safe "???" (fun () -> e.DisplayName) + + let existing = + match noNameByDisplay.TryGetValue(dn) with + | true, xs -> xs + | _ -> [] + + let hasDiffRef = + existing + |> List.exists (fun x -> not (obj.ReferenceEquals(x, e))) + + if hasDiffRef then + printfn + "CYCLE PROOF: no-name entity '%s' at depth %d — FullName throws AND different FCS object reference (ref-cache miss)" + dn + depth + + noNameByDisplay.[dn] <- + e + :: existing + + if not isHit then + let dn = safe "???" (fun () -> e.DisplayName) + + if fn.IsNone then + printfn " [depth %d] processing NO-NAME entity '%s'" depth dn + + // 1. DeclaringEntity + safe + () + (fun () -> + e.DeclaringEntity + |> Option.iter (walkEntity (depth + 1)) + ) + + // 2. UnionCases + safe + () + (fun () -> + for uc in e.UnionCases do + safe + () + (fun () -> + for f in uc.Fields do + safe () (fun () -> walkType (depth + 1) f.FieldType) + + safe + () + (fun () -> + f.DeclaringEntity + |> Option.iter (walkEntity (depth + 1)) + ) + ) + + safe () (fun () -> walkType (depth + 1) uc.ReturnType) + safe () (fun () -> walkEntity (depth + 1) uc.DeclaringEntity) + ) + + // 3. FSharpFields + safe + () + (fun () -> + for f in e.FSharpFields do + safe () (fun () -> walkType (depth + 1) f.FieldType) + + safe + () + (fun () -> + f.DeclaringEntity + |> Option.iter (walkEntity (depth + 1)) + ) + ) + + // 4. MembersFunctionsAndValues + safe + () + (fun () -> + for m in e.MembersFunctionsAndValues do + walkMember (depth + 1) m + ) + + // 5. BaseType + safe + () + (fun () -> + e.BaseType + |> Option.iter (walkType (depth + 1)) + ) + + // 6. DeclaredInterfaces + safe + () + (fun () -> + for iface in e.DeclaredInterfaces do + walkType (depth + 1) iface + ) + + // 7. AbbreviatedType + safe + () + (fun () -> + if e.IsFSharpAbbreviation then + walkType (depth + 1) e.AbbreviatedType + ) + + and walkMember depth (m: FSharpMemberOrFunctionOrValue) = + if + depth + >= depthLimit + then + () + else + + safe + () + (fun () -> + m.DeclaringEntity + |> Option.iter (walkEntity (depth + 1)) + ) + + safe () (fun () -> walkType (depth + 1) m.FullType) + + safe + () + (fun () -> + for group in m.CurriedParameterGroups do + for p in group do + safe () (fun () -> walkType (depth + 1) p.Type) + ) + + safe () (fun () -> walkType (depth + 1) m.ReturnParameter.Type) + + and walkType depth (t: FSharpType) = + if + depth + >= depthLimit + then + () + else + + safe + () + (fun () -> + for ga in t.GenericArguments do + walkType (depth + 1) ga + ) + + safe + () + (fun () -> + if t.IsAbbreviation then + walkType (depth + 1) t.AbbreviatedType + ) + + safe + () + (fun () -> + if t.HasTypeDefinition then + walkEntity (depth + 1) t.TypeDefinition + ) + + for su in symbols do + match su.Symbol with + | :? FSharpEntity as e -> walkEntity 0 e + | :? FSharpMemberOrFunctionOrValue as m -> + safe + () + (fun () -> + m.DeclaringEntity + |> Option.iter (walkEntity 0) + ) + + safe () (fun () -> walkType 0 m.FullType) + | :? FSharpField as f -> + safe () (fun () -> walkType 0 f.FieldType) + + safe + () + (fun () -> + f.DeclaringEntity + |> Option.iter (walkEntity 0) + ) + | :? FSharpUnionCase as uc -> safe () (fun () -> walkEntity 0 uc.DeclaringEntity) + | _ -> () + + let differentRefs = + noNameByDisplay + |> Seq.choose (fun kv -> + let distinctRefs = + kv.Value + |> List.distinctBy (fun e -> RuntimeHelpers.GetHashCode(e)) + |> List.length + + if distinctRefs > 1 then + Some(kv.Key, distinctRefs) + else + None + ) + |> Seq.toList + + { + CallCount = callCount + MaxDepth = maxDepth + EntitiesByName = nameCache.Count + EntitiesByRef = refCache.Count + NoNameEntityEncounters = noNameHits + DifferentRefNoNameEntities = differentRefs + HitLimit = + callCount + >= limit + } + + [] + let ``entityToV1 traversal is bounded for record type`` () = + let source = + """ +module M +type Foo = { X: int; Y: string; Z: float } +let f (x: Foo) = x.X +""" + + let ctx = getContext projectOptions source + let symbols = ctx.CheckFileResults.GetAllUsesOfAllSymbolsInFile() + let stats = walkFrom symbols + + printfn "=== Record type ===" + printfn "Calls: %d MaxDepth: %d" stats.CallCount stats.MaxDepth + printfn "Entities by name: %d by ref: %d" stats.EntitiesByName stats.EntitiesByRef + printfn "No-name encounters: %d" stats.NoNameEntityEncounters + + for name, refs in stats.DifferentRefNoNameEntities do + printfn "!! DIFFERENT REFS for no-name entity '%s': %d distinct objects" name refs + + Assert.That(stats.HitLimit, Is.False, "walk should terminate") + + Assert.That( + stats.DifferentRefNoNameEntities, + Is.Empty, + "no no-name entity should have multiple distinct FCS object references" + ) + + [] + let ``entityToV1 traversal is bounded for code with System.Type`` () = + let source = + """ +module M +open System +open System.Collections.Generic + +type MyRecord = { Name: string; Value: int; Created: DateTime } +let f (r: MyRecord) = r.Name +let g () : Dictionary = Dictionary() +let h (t: Type) = t.Name +""" + + let ctx = getContext projectOptions source + let symbols = ctx.CheckFileResults.GetAllUsesOfAllSymbolsInFile() + let stats = walkFrom symbols + + printfn "=== Complex BCL types ===" + printfn "Calls: %d MaxDepth: %d" stats.CallCount stats.MaxDepth + printfn "Entities by name: %d by ref: %d" stats.EntitiesByName stats.EntitiesByRef + printfn "No-name encounters: %d" stats.NoNameEntityEncounters + + for name, refs in stats.DifferentRefNoNameEntities do + printfn "!! DIFFERENT REFS for no-name entity '%s': %d distinct objects" name refs + + Assert.That(stats.HitLimit, Is.False, "walk should terminate") + + Assert.That( + stats.DifferentRefNoNameEntities, + Is.Empty, + "no no-name entity should have multiple distinct FCS object references" + ) + + [] + let ``contextToV1 completes without overflow for type abbreviations`` () = + let source = + """ +module M + +let a (x: int) (y: bool) (z: string) (w: float) (u: unit) (o: obj) = () +let b : int list = [] +let c : bool option = None +let d = System.Int32.MaxValue +""" + + let ctx = getContext projectOptions source + // This would stack-overflow before the AccessPath.DisplayName fix. + let v1Ctx = contextToV1 ctx + Assert.IsNotNull(v1Ctx, "contextToV1 should complete without overflow") diff --git a/samples/OptionAnalyzer.V1/Library.fs b/samples/OptionAnalyzer.V1/Library.fs new file mode 100644 index 0000000..373dd73 --- /dev/null +++ b/samples/OptionAnalyzer.V1/Library.fs @@ -0,0 +1,52 @@ +module OptionAnalyzer.V1 + +open FSharp.Analyzers.SDK.V1 +open FSharp.Analyzers.SDK.V1.TASTCollecting + +let rec private findOptionValue (expr: TypedExpr) (results: ResizeArray) = + match expr with + | TypedExpr.Call(_, m, _, _, _, range) -> + m.DeclaringEntity + |> Option.iter (fun de -> + let name = System.String.Join(".", de.FullName, m.DisplayName) + + if name = "Microsoft.FSharp.Core.FSharpOption`1.Value" then + results.Add range + ) + | _ -> () + +[] +let optionValueAnalyzer: Analyzer = + fun ctx -> + async { + let results = ResizeArray() + + match ctx.TypedTree with + | None -> () + | Some handle -> + let tree = convertTast handle + visitTypedTree (fun expr -> findOptionValue expr results) tree + + return + results + |> Seq.map (fun r -> + { + Type = "Option.Value analyzer" + Message = "Option.Value shouldn't be used" + Code = "OV001" + Severity = Severity.Warning + Range = r + Fixes = [] + } + ) + |> Seq.toList + } + +/// A V1 analyzer defined as a method with inferred return type (returns [] without +/// explicit type annotation). This exercises the same codepath that the legacy loader +/// handles via its generic-return-type fallback. +[] +let inferredReturnAnalyzer (_ctx: CliContext) = async { return [] } + +[] +let explicitGenericAnalyzer<'b> (_ctx: CliContext) : Async<'b list> = async { return [] } diff --git a/samples/OptionAnalyzer.V1/OptionAnalyzer.V1.fsproj b/samples/OptionAnalyzer.V1/OptionAnalyzer.V1.fsproj new file mode 100644 index 0000000..4e29e74 --- /dev/null +++ b/samples/OptionAnalyzer.V1/OptionAnalyzer.V1.fsproj @@ -0,0 +1,19 @@ + + + + net8.0 + + + + + + + + + + + + + + + diff --git a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fs b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fs index 7535e0e..f9ec681 100644 --- a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fs +++ b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fs @@ -70,7 +70,7 @@ module Client = // This could still be generic, as in an empty list is returned from the analyzer let msgType = listType.GenericTypeArguments.[0] - msgType.Name = "a" + msgType.IsGenericParameter || msgType = typeof else false @@ -95,9 +95,17 @@ module Client = ) elif hasExpectReturnType m.ReturnType then try + let method = + if m.ContainsGenericParameters then + m.MakeGenericMethod( + Array.create (m.GetGenericArguments().Length) typeof + ) + else + m + let analyzer: Analyzer<'TContext> = fun ctx -> - m.Invoke(null, [| ctx |]) + method.Invoke(null, [| ctx |]) |> unbox Some analyzer @@ -198,6 +206,320 @@ module Client = |> Seq.choose (analyzerFromMember<'TAnalyzerAttribute, 'TContext> path) |> Seq.toList +module internal V1Support = + + let isV1CliAnalyzer (mi: MemberInfo) : FSharp.Analyzers.SDK.V1.CliAnalyzerAttribute option = + mi.GetCustomAttributes true + |> Array.tryFind (fun a -> + a.GetType().FullName = "FSharp.Analyzers.SDK.V1.CliAnalyzerAttribute" + ) + |> Option.bind (fun attr -> + try + Some(unbox attr) + with _ -> + None + ) + + let adaptV1Analyzer (v1Analyzer: FSharp.Analyzers.SDK.V1.Analyzer) : Analyzer = + fun ctx -> + async { + let v1Ctx = AdapterV1.contextToV1 ctx + let! v1Messages = v1Analyzer v1Ctx + + return + v1Messages + |> List.map AdapterV1.messageFromV1 + } + + let private v1ExpectedReturnType = + typeof> + + let private hasV1ExpectReturnType (t: Type) = + if t = v1ExpectedReturnType then + true + // Fallback for methods with inferred generic return type (e.g. returning []). + // The FullName is null for types containing unresolved generic parameters. + elif + isNull t.FullName + && t.Name = "FSharpAsync`1" + && t.GenericTypeArguments.Length = 1 + then + let listType = t.GenericTypeArguments.[0] + + listType.Name = "FSharpList`1" + && listType.GenericTypeArguments.Length = 1 + && (let msgType = listType.GenericTypeArguments.[0] in + + msgType.IsGenericParameter + || msgType = typeof) + else + false + + let v1CliAnalyzerFromMember + (path: string) + (mi: MemberInfo) + : Client.RegisteredAnalyzer option + = + let inline unboxV1Analyzer v = + if isNull v then + failwith "V1 Analyzer is null" + else + unbox v + + let getV1Analyzer (mi: MemberInfo) : FSharp.Analyzers.SDK.V1.Analyzer option = + try + match box mi with + | :? FieldInfo as m -> + if m.FieldType = typeof then + Some( + m.GetValue(null) + |> unboxV1Analyzer + ) + else + None + | :? MethodInfo as m -> + if m.ReturnType = typeof then + Some( + m.Invoke(null, null) + |> unboxV1Analyzer + ) + elif hasV1ExpectReturnType m.ReturnType then + let method = + if m.ContainsGenericParameters then + m.MakeGenericMethod( + Array.create + (m.GetGenericArguments().Length) + typeof + ) + else + m + + let analyzer: FSharp.Analyzers.SDK.V1.Analyzer = + fun ctx -> + method.Invoke(null, [| ctx |]) + |> unbox + + Some analyzer + else + None + | :? PropertyInfo as m -> + if m.PropertyType = typeof then + Some( + m.GetValue(null, null) + |> unboxV1Analyzer + ) + else + None + | _ -> None + with _ -> + None + + match isV1CliAnalyzer mi with + | Some attr -> + match getV1Analyzer mi with + | Some v1Analyzer -> + let name = + if String.IsNullOrWhiteSpace attr.Name then + mi.Name + else + attr.Name + + Some + { + AssemblyPath = path + Name = name + Analyzer = adaptV1Analyzer v1Analyzer + ShortDescription = attr.ShortDescription + HelpUri = attr.HelpUri + } + | None -> None + | None -> None + + let v1CliAnalyzersFromType + (path: string) + (t: Type) + : Client.RegisteredAnalyzer list + = + let asMembers x = Seq.map (fun m -> m :> MemberInfo) x + + let bindingFlags = + BindingFlags.Public + ||| BindingFlags.Static + + let members = + [ + t.GetTypeInfo().GetMethods bindingFlags + |> asMembers + t.GetTypeInfo().GetProperties bindingFlags + |> asMembers + t.GetTypeInfo().GetFields bindingFlags + |> asMembers + ] + |> Seq.collect id + + members + |> Seq.choose (v1CliAnalyzerFromMember path) + |> Seq.toList + + // ─── V1 Editor analyzer support ────────────────────────────────── + + let isV1EditorAnalyzer + (mi: MemberInfo) + : FSharp.Analyzers.SDK.V1.EditorAnalyzerAttribute option + = + mi.GetCustomAttributes true + |> Array.tryFind (fun a -> + a.GetType().FullName = "FSharp.Analyzers.SDK.V1.EditorAnalyzerAttribute" + ) + |> Option.bind (fun attr -> + try + Some(unbox attr) + with _ -> + None + ) + + let adaptV1EditorAnalyzer + (v1Analyzer: FSharp.Analyzers.SDK.V1.EditorAnalyzer) + : Analyzer + = + fun ctx -> + async { + let v1Ctx = AdapterV1.editorContextToV1 ctx + let! v1Messages = v1Analyzer v1Ctx + + return + v1Messages + |> List.map AdapterV1.messageFromV1 + } + + let private v1EditorExpectedReturnType = + typeof> + + let private hasV1EditorExpectedReturnType (t: Type) = + if t = v1EditorExpectedReturnType then + true + elif + isNull t.FullName + && t.Name = "FSharpAsync`1" + && t.GenericTypeArguments.Length = 1 + then + let listType = t.GenericTypeArguments.[0] + + listType.Name = "FSharpList`1" + && listType.GenericTypeArguments.Length = 1 + && (let msgType = listType.GenericTypeArguments.[0] in + + msgType.IsGenericParameter + || msgType = typeof) + else + false + + let v1EditorAnalyzerFromMember + (path: string) + (mi: MemberInfo) + : Client.RegisteredAnalyzer option + = + let inline unboxV1Analyzer v = + if isNull v then + failwith "V1 EditorAnalyzer is null" + else + unbox v + + let getV1Analyzer (mi: MemberInfo) : FSharp.Analyzers.SDK.V1.EditorAnalyzer option = + try + match box mi with + | :? FieldInfo as m -> + if m.FieldType = typeof then + Some( + m.GetValue(null) + |> unboxV1Analyzer + ) + else + None + | :? MethodInfo as m -> + if m.ReturnType = typeof then + Some( + m.Invoke(null, null) + |> unboxV1Analyzer + ) + elif hasV1EditorExpectedReturnType m.ReturnType then + let method = + if m.ContainsGenericParameters then + m.MakeGenericMethod( + Array.create + (m.GetGenericArguments().Length) + typeof + ) + else + m + + let analyzer: FSharp.Analyzers.SDK.V1.EditorAnalyzer = + fun ctx -> + method.Invoke(null, [| ctx |]) + |> unbox + + Some analyzer + else + None + | :? PropertyInfo as m -> + if m.PropertyType = typeof then + Some( + m.GetValue(null, null) + |> unboxV1Analyzer + ) + else + None + | _ -> None + with _ -> + None + + match isV1EditorAnalyzer mi with + | Some attr -> + match getV1Analyzer mi with + | Some v1Analyzer -> + let name = + if String.IsNullOrWhiteSpace attr.Name then + mi.Name + else + attr.Name + + Some + { + AssemblyPath = path + Name = name + Analyzer = adaptV1EditorAnalyzer v1Analyzer + ShortDescription = attr.ShortDescription + HelpUri = attr.HelpUri + } + | None -> None + | None -> None + + let v1EditorAnalyzersFromType + (path: string) + (t: Type) + : Client.RegisteredAnalyzer list + = + let asMembers x = Seq.map (fun m -> m :> MemberInfo) x + + let bindingFlags = + BindingFlags.Public + ||| BindingFlags.Static + + let members = + [ + t.GetTypeInfo().GetMethods bindingFlags + |> asMembers + t.GetTypeInfo().GetProperties bindingFlags + |> asMembers + t.GetTypeInfo().GetFields bindingFlags + |> asMembers + ] + |> Seq.collect id + + members + |> Seq.choose (v1EditorAnalyzerFromMember path) + |> Seq.toList + type AssemblyLoadStats = { AnalyzerAssemblies: int @@ -263,62 +585,108 @@ type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TC let skippedAssemblies = ref 0 + let filterByExcludeInclude + (assembly: Assembly) + (registeredAnalyzer: Client.RegisteredAnalyzer<'TContext>) + = + match excludeInclude with + | Some(ExcludeFilter excludeFilter) -> + let shouldExclude = excludeFilter registeredAnalyzer.Name + + if shouldExclude then + logger.LogInformation( + "Excluding {Name} from {FullName}", + registeredAnalyzer.Name, + assembly.FullName + ) + + not shouldExclude + | Some(IncludeFilter includeFilter) -> + let shouldInclude = includeFilter registeredAnalyzer.Name + + if shouldInclude then + logger.LogInformation( + "Including {Name} from {FullName}", + registeredAnalyzer.Name, + assembly.FullName + ) + + shouldInclude + | None -> true + let analyzers = analyzerAssemblies - |> Array.filter (fun (name, analyzerAssembly) -> - let version = findFSharpAnalyzerSDKVersion analyzerAssembly + |> Array.map (fun (path, assembly) -> + let version = findFSharpAnalyzerSDKVersion assembly - if + let isVersionMatch = version.Major = Utils.currentFSharpAnalyzersSDKVersion.Major && version.Minor = Utils.currentFSharpAnalyzersSDKVersion.Minor - then - true - else - System.Threading.Interlocked.Increment skippedAssemblies - |> ignore - - logger.LogError( - "Trying to load {Name} which was built using SDK version {Version}. Expect {SdkVersion} instead. Assembly will be skipped.", - name, - version, - Utils.currentFSharpAnalyzersSDKVersion - ) - false - ) - |> Array.map (fun (path, assembly) -> - let analyzers = - assembly.GetExportedTypes() - |> Seq.collect (Client.analyzersFromType<'TAttribute, 'TContext> path) - |> Seq.filter (fun registeredAnalyzer -> - match excludeInclude with - | Some(ExcludeFilter excludeFilter) -> - let shouldExclude = excludeFilter registeredAnalyzer.Name - - if shouldExclude then - logger.LogInformation( - "Excluding {Name} from {FullName}", - registeredAnalyzer.Name, - assembly.FullName - ) - - not shouldExclude - | Some(IncludeFilter includeFilter) -> - let shouldInclude = includeFilter registeredAnalyzer.Name - - if shouldInclude then - logger.LogInformation( - "Including {Name} from {FullName}", - registeredAnalyzer.Name, - assembly.FullName - ) - - shouldInclude - | None -> true - ) - |> Seq.toList + // V1 analyzers: load regardless of version (no version gate). + let v1Analyzers: Client.RegisteredAnalyzer<'TContext> list = + if typeof<'TContext> = typeof then + try + assembly.GetExportedTypes() + |> Seq.collect (V1Support.v1CliAnalyzersFromType path) + |> Seq.map (fun ra -> + unbox> (box ra) + ) + |> Seq.toList + with _ -> + [] + elif typeof<'TContext> = typeof then + try + assembly.GetExportedTypes() + |> Seq.collect (V1Support.v1EditorAnalyzersFromType path) + |> Seq.map (fun ra -> + unbox> (box ra) + ) + |> Seq.toList + with _ -> + [] + else + [] + + // Legacy analyzers: version check required. + let legacyAnalyzers = + if isVersionMatch then + try + assembly.GetExportedTypes() + |> Seq.collect ( + Client.analyzersFromType<'TAttribute, 'TContext> path + ) + |> Seq.toList + with _ -> + [] + else + [] + + if not isVersionMatch then + if List.isEmpty v1Analyzers then + System.Threading.Interlocked.Increment skippedAssemblies + |> ignore + + logger.LogError( + "Trying to load {Name} which was built using SDK version {Version}. Expect {SdkVersion} instead. Assembly will be skipped.", + path, + version, + Utils.currentFSharpAnalyzersSDKVersion + ) + else + logger.LogWarning( + "Assembly {Name} was built using SDK version {Version} (expected {SdkVersion}). Legacy analyzers from this assembly will be skipped; only V1 analyzers will be loaded.", + path, + version, + Utils.currentFSharpAnalyzersSDKVersion + ) + + let allAnalyzers = + (legacyAnalyzers + @ v1Analyzers) + |> List.filter (filterByExcludeInclude assembly) - path, analyzers + path, allAnalyzers ) for path, analyzers in analyzers do diff --git a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fsproj b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fsproj index c03f91c..f57ec7e 100644 --- a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fsproj +++ b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fsproj @@ -9,6 +9,9 @@ Copyright 2019 Lambda Factory true + + + @@ -16,6 +19,12 @@ + + + + + + diff --git a/src/FSharp.Analyzers.SDK/V1/Adapter.fs b/src/FSharp.Analyzers.SDK/V1/Adapter.fs new file mode 100644 index 0000000..2dc199e --- /dev/null +++ b/src/FSharp.Analyzers.SDK/V1/Adapter.fs @@ -0,0 +1,948 @@ +module internal FSharp.Analyzers.SDK.AdapterV1 + +open System.Collections.Generic +open System.Runtime.CompilerServices +open FSharp.Compiler.Text +open FSharp.Compiler.Symbols +open FSharp.Compiler.Syntax +open FSharp.Compiler.CodeAnalysis +open FSharp.Analyzers.SDK.V1 +// FSharpExprPatterns must be opened last so its active patterns +// shadow the identically-named TypedExpr DU cases from V1. +open FSharp.Compiler.Symbols.FSharpExprPatterns + +// Cache keyed by logical identity rather than reference identity, +// because FCS may return different objects for the same logical entity when +// accessed via different paths (e.g. member.DeclaringEntity vs the entity +// directly). Only entities are cached (using a stub to break cycles). +// Types are NOT cached because BasicQualifiedName does not distinguish +// generic instantiations (e.g. list vs list). +// +// Three-tier cache key strategy: +// 1. FullName — most entities (e.g. "Microsoft.FSharp.Core.FSharpOption`1") +// 2. AccessPath.DisplayName — type-abbreviation entities (bool, int, obj, +// unit, etc.) whose FullName throws but whose AccessPath + DisplayName +// is stable across different FCS object instances +// 3. Reference identity — truly anonymous/compiler-generated entities where +// neither FullName nor AccessPath.DisplayName is available +let private referenceComparer<'T when 'T: not struct> = + { new IEqualityComparer<'T> with + member _.Equals(x, y) = obj.ReferenceEquals(x, y) + member _.GetHashCode(x) = RuntimeHelpers.GetHashCode(x) + } + +type ConversionCache() = + member val Entities = Dictionary() + member val EntitiesByRef = Dictionary(referenceComparer) + +let inline private tryGet defaultValue ([] f) = + try + f () + with _ -> + defaultValue + +let private emptyTypeInfo: TypeInfo = + { + BasicQualifiedName = "" + IsAbbreviation = false + IsFunctionType = false + IsTupleType = false + IsStructTupleType = false + IsGenericParameter = false + HasTypeDefinition = false + GenericArguments = [] + AbbreviatedType = None + TypeDefinition = None + GenericParameter = None + } + +// ─── Range conversion ─────────────────────────────────────────────── + +let rangeToV1 (r: range) : SourceRange = + { + FileName = r.FileName + StartLine = r.StartLine + StartColumn = r.StartColumn + EndLine = r.EndLine + EndColumn = r.EndColumn + } + +let rangeFromV1 (sr: SourceRange) : range = + let start = Position.mkPos sr.StartLine sr.StartColumn + let finish = Position.mkPos sr.EndLine sr.EndColumn + Range.mkRange sr.FileName start finish + +// ─── Generic parameter conversion ─────────────────────────────────── + +let genericParameterToV1 (gp: FSharpGenericParameter) : GenericParameterInfo = + { + Name = gp.Name + IsSolveAtCompileTime = gp.IsSolveAtCompileTime + IsCompilerGenerated = gp.IsCompilerGenerated + IsMeasure = gp.IsMeasure + } + +// ─── Mutually recursive FCS type conversions ──────────────────────── + +let rec entityToV1 (cache: ConversionCache) (e: FSharpEntity) : EntityInfo = + // Use FullName as cache key when available; for type-abbreviation + // entities (bool, int, obj, etc.) whose FullName throws, fall back + // to AccessPath.DisplayName; truly anonymous entities use reference + // identity so distinct nameless entities don't collide. + let fullName = tryGet None (fun () -> Some e.FullName) + + let stableName = + match fullName with + | Some _ -> fullName + | None -> + tryGet + None + (fun () -> + let dn = e.DisplayName + + if System.String.IsNullOrEmpty(dn) then + None + else + let ap = e.AccessPath + + if System.String.IsNullOrEmpty(ap) then + None + else + Some( + ap + + "." + + dn + ) + ) + + let cached = + match stableName with + | Some name -> + match cache.Entities.TryGetValue(name) with + | true, v -> ValueSome v + | _ -> ValueNone + | None -> + match cache.EntitiesByRef.TryGetValue(e) with + | true, v -> ValueSome v + | _ -> ValueNone + + match cached with + | ValueSome v -> v + | ValueNone -> + let fullNameStr = + stableName + |> Option.defaultValue "" + + let cacheSet v = + match stableName with + | Some name -> cache.Entities.[name] <- v + | None -> cache.EntitiesByRef.[e] <- v + + // Insert a stub to break cycles. + let stub: EntityInfo = + { + FullName = fullNameStr + DisplayName = tryGet "" (fun () -> e.DisplayName) + CompiledName = tryGet "" (fun () -> e.CompiledName) + Namespace = tryGet None (fun () -> e.Namespace) + DeclaringEntity = None + IsModule = tryGet false (fun () -> e.IsFSharpModule) + IsNamespace = tryGet false (fun () -> e.IsNamespace) + IsUnion = tryGet false (fun () -> e.IsFSharpUnion) + IsRecord = tryGet false (fun () -> e.IsFSharpRecord) + IsClass = tryGet false (fun () -> e.IsClass) + IsEnum = tryGet false (fun () -> e.IsEnum) + IsInterface = tryGet false (fun () -> e.IsInterface) + IsValueType = tryGet false (fun () -> e.IsValueType) + IsAbstractClass = tryGet false (fun () -> e.IsAbstractClass) + IsFSharpModule = tryGet false (fun () -> e.IsFSharpModule) + IsFSharpUnion = tryGet false (fun () -> e.IsFSharpUnion) + IsFSharpRecord = tryGet false (fun () -> e.IsFSharpRecord) + IsFSharpExceptionDeclaration = + tryGet false (fun () -> e.IsFSharpExceptionDeclaration) + IsMeasure = tryGet false (fun () -> e.IsMeasure) + IsDelegate = tryGet false (fun () -> e.IsDelegate) + IsByRef = tryGet false (fun () -> e.IsByRef) + IsAbbreviation = tryGet false (fun () -> e.IsFSharpAbbreviation) + UnionCases = [] + FSharpFields = [] + MembersFunctionsAndValues = [] + GenericParameters = [] + BaseType = None + DeclaredInterfaces = [] + AbbreviatedType = None + } + + cacheSet stub + + let full: EntityInfo = + { stub with + DeclaringEntity = + tryGet + None + (fun () -> + e.DeclaringEntity + |> Option.map (entityToV1 cache) + ) + UnionCases = + tryGet + [] + (fun () -> + e.UnionCases + |> Seq.map (unionCaseToV1 cache) + |> Seq.toList + ) + FSharpFields = + tryGet + [] + (fun () -> + e.FSharpFields + |> Seq.map (fieldToV1 cache) + |> Seq.toList + ) + MembersFunctionsAndValues = + tryGet + [] + (fun () -> + e.MembersFunctionsAndValues + |> Seq.map (memberToV1 cache) + |> Seq.toList + ) + GenericParameters = + tryGet + [] + (fun () -> + e.GenericParameters + |> Seq.map genericParameterToV1 + |> Seq.toList + ) + BaseType = + tryGet + None + (fun () -> + e.BaseType + |> Option.map (typeToV1 cache) + ) + DeclaredInterfaces = + tryGet + [] + (fun () -> + e.DeclaredInterfaces + |> Seq.map (typeToV1 cache) + |> Seq.toList + ) + AbbreviatedType = + tryGet + None + (fun () -> + if e.IsFSharpAbbreviation then + Some(typeToV1 cache e.AbbreviatedType) + else + None + ) + } + + cacheSet full + full + +and typeToV1 (cache: ConversionCache) (t: FSharpType) : TypeInfo = + { + BasicQualifiedName = tryGet "" (fun () -> t.BasicQualifiedName) + IsAbbreviation = tryGet false (fun () -> t.IsAbbreviation) + IsFunctionType = tryGet false (fun () -> t.IsFunctionType) + IsTupleType = tryGet false (fun () -> t.IsTupleType) + IsStructTupleType = tryGet false (fun () -> t.IsStructTupleType) + IsGenericParameter = tryGet false (fun () -> t.IsGenericParameter) + HasTypeDefinition = tryGet false (fun () -> t.HasTypeDefinition) + GenericArguments = + tryGet + [] + (fun () -> + t.GenericArguments + |> Seq.map (typeToV1 cache) + |> Seq.toList + ) + AbbreviatedType = + tryGet + None + (fun () -> + if t.IsAbbreviation then + Some(typeToV1 cache t.AbbreviatedType) + else + None + ) + TypeDefinition = + tryGet + None + (fun () -> + if t.HasTypeDefinition then + Some(entityToV1 cache t.TypeDefinition) + else + None + ) + GenericParameter = + tryGet + None + (fun () -> + if t.IsGenericParameter then + Some(genericParameterToV1 t.GenericParameter) + else + None + ) + } + +and memberToV1 + (cache: ConversionCache) + (m: FSharpMemberOrFunctionOrValue) + : MemberOrFunctionOrValueInfo + = + { + DisplayName = tryGet "" (fun () -> m.DisplayName) + FullName = tryGet "" (fun () -> m.FullName) + CompiledName = tryGet "" (fun () -> m.CompiledName) + IsProperty = tryGet false (fun () -> m.IsProperty) + IsMethod = tryGet false (fun () -> m.IsMethod) + IsCompilerGenerated = tryGet false (fun () -> m.IsCompilerGenerated) + IsMutable = tryGet false (fun () -> m.IsMutable) + IsExtensionMember = tryGet false (fun () -> m.IsExtensionMember) + IsActivePattern = tryGet false (fun () -> m.IsActivePattern) + IsConstructor = tryGet false (fun () -> m.IsConstructor) + IsPropertyGetterMethod = tryGet false (fun () -> m.IsPropertyGetterMethod) + IsPropertySetterMethod = tryGet false (fun () -> m.IsPropertySetterMethod) + IsModuleValueOrMember = tryGet false (fun () -> m.IsModuleValueOrMember) + IsValue = tryGet false (fun () -> m.IsValue) + IsMember = tryGet false (fun () -> m.IsMember) + IsInstanceMember = tryGet false (fun () -> m.IsInstanceMember) + IsInstanceMemberInCompiledCode = tryGet false (fun () -> m.IsInstanceMemberInCompiledCode) + IsDispatchSlot = tryGet false (fun () -> m.IsDispatchSlot) + IsOverrideOrExplicitInterfaceImplementation = + tryGet false (fun () -> m.IsOverrideOrExplicitInterfaceImplementation) + DeclaringEntity = + tryGet + None + (fun () -> + m.DeclaringEntity + |> Option.map (entityToV1 cache) + ) + FullType = tryGet emptyTypeInfo (fun () -> typeToV1 cache m.FullType) + CurriedParameterGroups = + tryGet + [] + (fun () -> + m.CurriedParameterGroups + |> Seq.map (fun group -> + group + |> Seq.map (parameterToV1 cache) + |> Seq.toList + ) + |> Seq.toList + ) + ReturnParameter = tryGet None (fun () -> Some(parameterToV1 cache m.ReturnParameter)) + GenericParameters = + tryGet + [] + (fun () -> + m.GenericParameters + |> Seq.map genericParameterToV1 + |> Seq.toList + ) + } + +and parameterToV1 (cache: ConversionCache) (p: FSharpParameter) : ParameterInfo = + { + Name = tryGet None (fun () -> p.Name) + Type = tryGet emptyTypeInfo (fun () -> typeToV1 cache p.Type) + IsOptionalArg = tryGet false (fun () -> p.IsOptionalArg) + } + +and abstractParameterToV1 (cache: ConversionCache) (p: FSharpAbstractParameter) : ParameterInfo = + { + Name = tryGet None (fun () -> p.Name) + Type = tryGet emptyTypeInfo (fun () -> typeToV1 cache p.Type) + IsOptionalArg = tryGet false (fun () -> p.IsOptionalArg) + } + +and fieldToV1 (cache: ConversionCache) (f: FSharpField) : FieldInfo = + { + Name = tryGet "" (fun () -> f.Name) + FieldType = tryGet emptyTypeInfo (fun () -> typeToV1 cache f.FieldType) + IsCompilerGenerated = tryGet false (fun () -> f.IsCompilerGenerated) + IsMutable = tryGet false (fun () -> f.IsMutable) + IsStatic = tryGet false (fun () -> f.IsStatic) + IsVolatile = tryGet false (fun () -> f.IsVolatile) + IsLiteral = tryGet false (fun () -> f.IsLiteral) + LiteralValue = tryGet None (fun () -> f.LiteralValue) + DeclaringEntity = + tryGet + None + (fun () -> + f.DeclaringEntity + |> Option.map (entityToV1 cache) + ) + } + +and unionCaseToV1 (cache: ConversionCache) (uc: FSharpUnionCase) : UnionCaseInfo = + { + Name = tryGet "" (fun () -> uc.Name) + CompiledName = tryGet "" (fun () -> uc.CompiledName) + Fields = + tryGet + [] + (fun () -> + uc.Fields + |> Seq.map (fieldToV1 cache) + |> Seq.toList + ) + ReturnType = tryGet emptyTypeInfo (fun () -> typeToV1 cache uc.ReturnType) + DeclaringEntity = tryGet None (fun () -> Some(entityToV1 cache uc.DeclaringEntity)) + HasFields = tryGet false (fun () -> uc.HasFields) + } + +// ─── Member flags conversion ──────────────────────────────────────── + +let memberKindToV1 (mk: SynMemberKind) : MemberKind = + match mk with + | SynMemberKind.ClassConstructor -> MemberKind.ClassConstructor + | SynMemberKind.Constructor -> MemberKind.Constructor + | SynMemberKind.Member -> MemberKind.Member + | SynMemberKind.PropertyGet -> MemberKind.PropertyGet + | SynMemberKind.PropertySet -> MemberKind.PropertySet + | SynMemberKind.PropertyGetSet -> MemberKind.PropertyGetSet + +let memberFlagsToV1 (mf: SynMemberFlags) : MemberFlags = + { + IsInstance = mf.IsInstance + IsDispatchSlot = mf.IsDispatchSlot + IsOverrideOrExplicitImpl = mf.IsOverrideOrExplicitImpl + IsFinal = mf.IsFinal + MemberKind = memberKindToV1 mf.MemberKind + } + +// ─── Symbol use conversion ────────────────────────────────────────── + +let symbolToV1 (cache: ConversionCache) (s: FSharpSymbol) : SymbolInfo = + match s with + | :? FSharpEntity as e -> SymbolInfo.Entity(entityToV1 cache e) + | :? FSharpMemberOrFunctionOrValue as m -> + SymbolInfo.MemberOrFunctionOrValue(memberToV1 cache m) + | :? FSharpField as f -> SymbolInfo.Field(fieldToV1 cache f) + | :? FSharpUnionCase as uc -> SymbolInfo.UnionCase(unionCaseToV1 cache uc) + | :? FSharpGenericParameter as gp -> SymbolInfo.GenericParameter(genericParameterToV1 gp) + | _ -> SymbolInfo.Other(tryGet "" (fun () -> s.DisplayName)) + +let symbolUseToV1 (cache: ConversionCache) (su: FSharpSymbolUse) : SymbolUseInfo = + { + Symbol = symbolToV1 cache su.Symbol + Range = rangeToV1 su.Range + IsFromDefinition = su.IsFromDefinition + IsFromPattern = su.IsFromPattern + IsFromType = su.IsFromType + IsFromAttribute = su.IsFromAttribute + IsFromDispatchSlotImplementation = su.IsFromDispatchSlotImplementation + IsFromComputationExpression = su.IsFromComputationExpression + IsFromOpenStatement = su.IsFromOpenStatement + } + +// ─── ObjectExprOverride conversion ────────────────────────────────── + +let objectExprOverrideToV1 + (cache: ConversionCache) + (exprConvert: ConversionCache -> FSharpExpr -> TypedExpr) + (o: FSharpObjectExprOverride) + : ObjectExprOverrideInfo + = + let sig' = o.Signature + + let signatureInfo: MemberOrFunctionOrValueInfo = + { + DisplayName = tryGet "" (fun () -> sig'.Name) + FullName = tryGet "" (fun () -> sig'.Name) + CompiledName = tryGet "" (fun () -> sig'.Name) + IsProperty = false + IsMethod = true + IsCompilerGenerated = false + IsMutable = false + IsExtensionMember = false + IsActivePattern = false + IsConstructor = false + IsPropertyGetterMethod = false + IsPropertySetterMethod = false + IsModuleValueOrMember = false + IsValue = false + IsMember = true + IsInstanceMember = true + IsInstanceMemberInCompiledCode = true + IsDispatchSlot = false + IsOverrideOrExplicitInterfaceImplementation = true + DeclaringEntity = None + FullType = tryGet emptyTypeInfo (fun () -> typeToV1 cache sig'.AbstractReturnType) + CurriedParameterGroups = + tryGet + [] + (fun () -> + sig'.AbstractArguments + |> Seq.map (fun group -> + group + |> Seq.map (abstractParameterToV1 cache) + |> Seq.toList + ) + |> Seq.toList + ) + ReturnParameter = None + GenericParameters = + tryGet + [] + (fun () -> + sig'.MethodGenericParameters + |> Seq.map genericParameterToV1 + |> Seq.toList + ) + } + + { + Signature = signatureInfo + Body = exprConvert cache o.Body + CurriedParameterGroups = + tryGet + [] + (fun () -> + o.CurriedParameterGroups + |> Seq.map (fun group -> + group + |> Seq.map (memberToV1 cache) + |> Seq.toList + ) + |> Seq.toList + ) + GenericParameters = + tryGet + [] + (fun () -> + o.GenericParameters + |> Seq.map genericParameterToV1 + |> Seq.toList + ) + } + +// ─── Expression conversion ────────────────────────────────────────── +// Mirrors the structure of TASTCollecting.visitExpr but builds TypedExpr values. + +let rec exprToV1 (cache: ConversionCache) (e: FSharpExpr) : TypedExpr = + match e with + | AddressOf lvalueExpr -> TypedExpr.AddressOf(exprToV1 cache lvalueExpr) + | AddressSet(lvalueExpr, rvalueExpr) -> + TypedExpr.AddressSet(exprToV1 cache lvalueExpr, exprToV1 cache rvalueExpr) + | Application(funcExpr, typeArgs, argExprs) -> + TypedExpr.Application( + exprToV1 cache funcExpr, + typeArgs + |> List.map (typeToV1 cache), + argExprs + |> List.map (exprToV1 cache) + ) + | Call(objExprOpt, memberOrFunc, objExprTypeArgs, memberOrFuncTypeArgs, argExprs) -> + TypedExpr.Call( + objExprOpt + |> Option.map (exprToV1 cache), + memberToV1 cache memberOrFunc, + objExprTypeArgs + |> List.map (typeToV1 cache), + memberOrFuncTypeArgs + |> List.map (typeToV1 cache), + argExprs + |> List.map (exprToV1 cache), + rangeToV1 e.Range + ) + | Coerce(targetType, inpExpr) -> + TypedExpr.Coerce(typeToV1 cache targetType, exprToV1 cache inpExpr) + | FastIntegerForLoop(startExpr, + limitExpr, + consumeExpr, + isUp, + _debugPointAtFor, + _debugPointAtInOrTo) -> + TypedExpr.FastIntegerForLoop( + exprToV1 cache startExpr, + exprToV1 cache limitExpr, + exprToV1 cache consumeExpr, + isUp + ) + | ILAsm(asmCode, typeArgs, argExprs) -> + TypedExpr.ILAsm( + asmCode, + typeArgs + |> List.map (typeToV1 cache), + argExprs + |> List.map (exprToV1 cache) + ) + | ILFieldGet(objExprOpt, fieldType, fieldName) -> + TypedExpr.ILFieldGet( + objExprOpt + |> Option.map (exprToV1 cache), + typeToV1 cache fieldType, + fieldName + ) + | ILFieldSet(objExprOpt, fieldType, fieldName, valueExpr) -> + TypedExpr.ILFieldSet( + objExprOpt + |> Option.map (exprToV1 cache), + typeToV1 cache fieldType, + fieldName, + exprToV1 cache valueExpr + ) + | IfThenElse(guardExpr, thenExpr, elseExpr) -> + TypedExpr.IfThenElse( + exprToV1 cache guardExpr, + exprToV1 cache thenExpr, + exprToV1 cache elseExpr + ) + | Lambda(lambdaVar, bodyExpr) -> + TypedExpr.Lambda(memberToV1 cache lambdaVar, exprToV1 cache bodyExpr) + | Let((bindingVar, bindingExpr, _debugPointAtBinding), bodyExpr) -> + TypedExpr.Let( + memberToV1 cache bindingVar, + exprToV1 cache bindingExpr, + exprToV1 cache bodyExpr + ) + | LetRec(recursiveBindings, bodyExpr) -> + let bindings = + recursiveBindings + |> List.map (fun (mfv, expr, _dp) -> (memberToV1 cache mfv, exprToV1 cache expr)) + + TypedExpr.LetRec(bindings, exprToV1 cache bodyExpr) + | NewArray(arrayType, argExprs) -> + TypedExpr.NewArray( + typeToV1 cache arrayType, + argExprs + |> List.map (exprToV1 cache) + ) + | NewDelegate(delegateType, delegateBodyExpr) -> + TypedExpr.NewDelegate(typeToV1 cache delegateType, exprToV1 cache delegateBodyExpr) + | NewObject(objType, typeArgs, argExprs) -> + TypedExpr.NewObject( + memberToV1 cache objType, + typeArgs + |> List.map (typeToV1 cache), + argExprs + |> List.map (exprToV1 cache) + ) + | NewRecord(recordType, argExprs) -> + TypedExpr.NewRecord( + typeToV1 cache recordType, + argExprs + |> List.map (exprToV1 cache), + rangeToV1 e.Range + ) + | NewTuple(tupleType, argExprs) -> + TypedExpr.NewTuple( + typeToV1 cache tupleType, + argExprs + |> List.map (exprToV1 cache) + ) + | NewUnionCase(unionType, unionCase, argExprs) -> + TypedExpr.NewUnionCase( + typeToV1 cache unionType, + unionCaseToV1 cache unionCase, + argExprs + |> List.map (exprToV1 cache) + ) + | Quote quotedExpr -> TypedExpr.Quote(exprToV1 cache quotedExpr) + | FSharpFieldGet(objExprOpt, recordOrClassType, fieldInfo) -> + TypedExpr.FieldGet( + objExprOpt + |> Option.map (exprToV1 cache), + typeToV1 cache recordOrClassType, + fieldToV1 cache fieldInfo + ) + | FSharpFieldSet(objExprOpt, recordOrClassType, fieldInfo, argExpr) -> + TypedExpr.FieldSet( + objExprOpt + |> Option.map (exprToV1 cache), + typeToV1 cache recordOrClassType, + fieldToV1 cache fieldInfo, + exprToV1 cache argExpr + ) + | Sequential(firstExpr, secondExpr) -> + TypedExpr.Sequential(exprToV1 cache firstExpr, exprToV1 cache secondExpr) + | TryFinally(bodyExpr, finalizeExpr, _debugPointAtTry, _debugPointAtFinally) -> + TypedExpr.TryFinally(exprToV1 cache bodyExpr, exprToV1 cache finalizeExpr) + | TryWith(bodyExpr, + filterVar, + filterExpr, + catchVar, + catchExpr, + _debugPointAtTry, + _debugPointAtWith) -> + TypedExpr.TryWith( + exprToV1 cache bodyExpr, + memberToV1 cache filterVar, + exprToV1 cache filterExpr, + memberToV1 cache catchVar, + exprToV1 cache catchExpr + ) + | TupleGet(tupleType, tupleElemIndex, tupleExpr) -> + TypedExpr.TupleGet(typeToV1 cache tupleType, tupleElemIndex, exprToV1 cache tupleExpr) + | DecisionTree(decisionExpr, decisionTargets) -> + let targets = + decisionTargets + |> List.map (fun (vars, expr) -> + (vars + |> List.map (memberToV1 cache), + exprToV1 cache expr) + ) + + TypedExpr.DecisionTree(exprToV1 cache decisionExpr, targets) + | DecisionTreeSuccess(decisionTargetIdx, decisionTargetExprs) -> + TypedExpr.DecisionTreeSuccess( + decisionTargetIdx, + decisionTargetExprs + |> List.map (exprToV1 cache) + ) + | TypeLambda(genericParam, bodyExpr) -> + TypedExpr.TypeLambda( + genericParam + |> List.map genericParameterToV1, + exprToV1 cache bodyExpr + ) + | TypeTest(ty, inpExpr) -> TypedExpr.TypeTest(typeToV1 cache ty, exprToV1 cache inpExpr) + | UnionCaseSet(unionExpr, unionType, unionCase, unionCaseField, valueExpr) -> + TypedExpr.UnionCaseSet( + exprToV1 cache unionExpr, + typeToV1 cache unionType, + unionCaseToV1 cache unionCase, + fieldToV1 cache unionCaseField, + exprToV1 cache valueExpr + ) + | UnionCaseGet(unionExpr, unionType, unionCase, unionCaseField) -> + TypedExpr.UnionCaseGet( + exprToV1 cache unionExpr, + typeToV1 cache unionType, + unionCaseToV1 cache unionCase, + fieldToV1 cache unionCaseField + ) + | UnionCaseTest(unionExpr, unionType, unionCase) -> + TypedExpr.UnionCaseTest( + exprToV1 cache unionExpr, + typeToV1 cache unionType, + unionCaseToV1 cache unionCase + ) + | UnionCaseTag(unionExpr, unionType) -> + TypedExpr.UnionCaseTag(exprToV1 cache unionExpr, typeToV1 cache unionType) + | ObjectExpr(objType, baseCallExpr, overrides, interfaceImplementations) -> + TypedExpr.ObjectExpr( + typeToV1 cache objType, + exprToV1 cache baseCallExpr, + overrides + |> List.map (objectExprOverrideToV1 cache exprToV1), + interfaceImplementations + |> List.map (fun (ty, impls) -> + (typeToV1 cache ty, + impls + |> List.map (objectExprOverrideToV1 cache exprToV1)) + ) + ) + | TraitCall(sourceTypes, traitName, typeArgs, typeInstantiation, argTypes, argExprs) -> + TypedExpr.TraitCall( + sourceTypes + |> List.map (typeToV1 cache), + traitName, + memberFlagsToV1 typeArgs, + typeInstantiation + |> List.map (typeToV1 cache), + argTypes + |> List.map (typeToV1 cache), + argExprs + |> List.map (exprToV1 cache) + ) + | ValueSet(valToSet, valueExpr) -> + TypedExpr.ValueSet(memberToV1 cache valToSet, exprToV1 cache valueExpr) + | WhileLoop(guardExpr, bodyExpr, _debugPointAtWhile) -> + TypedExpr.WhileLoop(exprToV1 cache guardExpr, exprToV1 cache bodyExpr) + | BaseValue baseType -> TypedExpr.BaseValue(typeToV1 cache baseType) + | DefaultValue defaultType -> TypedExpr.DefaultValue(typeToV1 cache defaultType) + | ThisValue thisType -> TypedExpr.ThisValue(typeToV1 cache thisType) + | Const(constValueObj, constType) -> TypedExpr.Const(constValueObj, typeToV1 cache constType) + | Value valueToGet -> TypedExpr.Value(memberToV1 cache valueToGet) + | _ -> TypedExpr.Unknown(rangeToV1 e.Range) + +// ─── Declaration conversion ───────────────────────────────────────── + +// FCS throws on certain compiler-generated members (e.g. CompareTo, GetHashCode, Equals) +// whose expression body can't be safely decompiled. These sets mirror the workaround in +// TASTCollecting.visitDeclaration — see: +// https://github.com/dotnet/fsharp/blob/91ff67b5f698f1929f75e65918e998a2df1c1858/src/Compiler/Symbols/Exprs.fs#L1269 +let private membersToIgnore = + set + [ + "CompareTo" + "GetHashCode" + "Equals" + ] + +let private exprTypesToIgnore = + set + [ + "Microsoft.FSharp.Core.int" + "Microsoft.FSharp.Core.bool" + ] + +let private shouldSkipMember (v: FSharpMemberOrFunctionOrValue) (e: FSharpExpr) = + v.IsCompilerGenerated + && Set.contains v.CompiledName membersToIgnore + && e.Type.IsAbbreviation + && Set.contains e.Type.BasicQualifiedName exprTypesToIgnore + +let rec declarationToV1 + (cache: ConversionCache) + (d: FSharpImplementationFileDeclaration) + : TypedDeclaration option + = + match d with + | FSharpImplementationFileDeclaration.Entity(e, subDecls) -> + TypedDeclaration.Entity( + entityToV1 cache e, + subDecls + |> List.choose (declarationToV1 cache) + ) + |> Some + | FSharpImplementationFileDeclaration.MemberOrFunctionOrValue(v, vs, e) -> + if shouldSkipMember v e then + None + else + // FCS may throw when decompiling certain expression trees — see: + // https://github.com/dotnet/fsharp/blob/91ff67b5f698f1929f75e65918e998a2df1c1858/src/Compiler/Symbols/Exprs.fs#L1329 + let body = + try + exprToV1 cache e + with _ -> + TypedExpr.Unknown(rangeToV1 e.Range) + + TypedDeclaration.MemberOrFunctionOrValue( + memberToV1 cache v, + vs + |> List.map (List.map (memberToV1 cache)), + body + ) + |> Some + | FSharpImplementationFileDeclaration.InitAction e -> + TypedDeclaration.InitAction(exprToV1 cache e) + |> Some + +// ─── Severity / Fix / Message conversion (V1 -> SDK) ──────────────── + +let severityFromV1 (s: Severity) : FSharp.Analyzers.SDK.Severity = + match s with + | Severity.Info -> FSharp.Analyzers.SDK.Severity.Info + | Severity.Hint -> FSharp.Analyzers.SDK.Severity.Hint + | Severity.Warning -> FSharp.Analyzers.SDK.Severity.Warning + | Severity.Error -> FSharp.Analyzers.SDK.Severity.Error + +let fixFromV1 (f: Fix) : FSharp.Analyzers.SDK.Fix = + { + FromRange = rangeFromV1 f.FromRange + FromText = f.FromText + ToText = f.ToText + } + +let messageFromV1 (m: Message) : FSharp.Analyzers.SDK.Message = + { + Type = m.Type + Message = m.Message + Code = m.Code + Severity = severityFromV1 m.Severity + Range = rangeFromV1 m.Range + Fixes = + m.Fixes + |> List.map fixFromV1 + } + +// ─── Ignore range conversion ──────────────────────────────────────── + +let analyzerIgnoreRangeToV1 (r: FSharp.Analyzers.SDK.AnalyzerIgnoreRange) : AnalyzerIgnoreRange = + match r with + | FSharp.Analyzers.SDK.AnalyzerIgnoreRange.File -> AnalyzerIgnoreRange.File + | FSharp.Analyzers.SDK.AnalyzerIgnoreRange.Range(s, e) -> AnalyzerIgnoreRange.Range(s, e) + | FSharp.Analyzers.SDK.AnalyzerIgnoreRange.NextLine l -> AnalyzerIgnoreRange.NextLine l + | FSharp.Analyzers.SDK.AnalyzerIgnoreRange.CurrentLine l -> AnalyzerIgnoreRange.CurrentLine l + +// ─── Context conversion ───────────────────────────────────────────── + +let contextToV1 (ctx: FSharp.Analyzers.SDK.CliContext) : CliContext = + let cache = ConversionCache() + + { + FileName = ctx.FileName + SourceText = ctx.SourceText.GetSubTextString(0, ctx.SourceText.Length) + TypedTree = + ctx.TypedTree + |> Option.map TypedTreeHandle + ProjectOptions = + { + ProjectFileName = ctx.ProjectOptions.ProjectFileName + SourceFiles = ctx.ProjectOptions.SourceFiles + ReferencedProjectsPaths = ctx.ProjectOptions.ReferencedProjectsPath + OtherOptions = ctx.ProjectOptions.OtherOptions + } + AnalyzerIgnoreRanges = + ctx.AnalyzerIgnoreRanges + |> Map.map (fun _ ranges -> + ranges + |> List.map analyzerIgnoreRangeToV1 + ) + SymbolUsesInFile = + tryGet + [] + (fun () -> + ctx.GetAllSymbolUsesOfFile() + |> Seq.map (symbolUseToV1 cache) + |> Seq.toList + ) + SymbolUsesInProject = + tryGet + [] + (fun () -> + ctx.GetAllSymbolUsesOfProject() + |> Seq.map (symbolUseToV1 cache) + |> Seq.toList + ) + } + +let editorContextToV1 (ctx: FSharp.Analyzers.SDK.EditorContext) : CliContext = + let cache = ConversionCache() + + { + FileName = ctx.FileName + SourceText = ctx.SourceText.GetSubTextString(0, ctx.SourceText.Length) + TypedTree = + ctx.TypedTree + |> Option.map TypedTreeHandle + ProjectOptions = + { + ProjectFileName = ctx.ProjectOptions.ProjectFileName + SourceFiles = ctx.ProjectOptions.SourceFiles + ReferencedProjectsPaths = ctx.ProjectOptions.ReferencedProjectsPath + OtherOptions = ctx.ProjectOptions.OtherOptions + } + AnalyzerIgnoreRanges = + ctx.AnalyzerIgnoreRanges + |> Map.map (fun _ ranges -> + ranges + |> List.map analyzerIgnoreRangeToV1 + ) + SymbolUsesInFile = + tryGet + [] + (fun () -> + ctx.GetAllSymbolUsesOfFile() + |> Seq.map (symbolUseToV1 cache) + |> Seq.toList + ) + SymbolUsesInProject = + tryGet + [] + (fun () -> + ctx.GetAllSymbolUsesOfProject() + |> Seq.map (symbolUseToV1 cache) + |> Seq.toList + ) + } diff --git a/src/FSharp.Analyzers.SDK/V1/Attributes.fs b/src/FSharp.Analyzers.SDK/V1/Attributes.fs new file mode 100644 index 0000000..5cf45cf --- /dev/null +++ b/src/FSharp.Analyzers.SDK/V1/Attributes.fs @@ -0,0 +1,59 @@ +namespace FSharp.Analyzers.SDK.V1 + +open System +open System.Runtime.InteropServices + +type Analyzer = CliContext -> Async +type EditorAnalyzer = CliContext -> Async + +[] +type CliAnalyzerAttribute + ( + [] name: string, + [ obj)>] shortDescription: string, + [ obj)>] helpUri: string + ) + = + inherit Attribute() + + member val Name: string = name + + member val ShortDescription: string option = + if String.IsNullOrWhiteSpace shortDescription then + None + else + Some shortDescription + + member val HelpUri: string option = + if String.IsNullOrWhiteSpace helpUri then + None + else + Some helpUri + +[] +type EditorAnalyzerAttribute + ( + [] name: string, + [ obj)>] shortDescription: string, + [ obj)>] helpUri: string + ) + = + inherit Attribute() + + member val Name: string = name + + member val ShortDescription: string option = + if String.IsNullOrWhiteSpace shortDescription then + None + else + Some shortDescription + + member val HelpUri: string option = + if String.IsNullOrWhiteSpace helpUri then + None + else + Some helpUri diff --git a/src/FSharp.Analyzers.SDK/V1/TASTCollecting.fs b/src/FSharp.Analyzers.SDK/V1/TASTCollecting.fs new file mode 100644 index 0000000..1fa4ac5 --- /dev/null +++ b/src/FSharp.Analyzers.SDK/V1/TASTCollecting.fs @@ -0,0 +1,159 @@ +namespace FSharp.Analyzers.SDK.V1 + +module TASTCollecting = + + /// Convert the typed tree into SDK-owned V1 types. + /// This is the main entry point for TAST-based analyzers. + let convertTast (tast: TypedTreeHandle) : TypedDeclaration list = + let cache = FSharp.Analyzers.SDK.AdapterV1.ConversionCache() + + tast.Contents.Declarations + |> List.choose (FSharp.Analyzers.SDK.AdapterV1.declarationToV1 cache) + + /// Walk the typed tree and call handler for each expression node. + let rec visitTypedTree + (handler: TypedExpr -> unit) + (declarations: TypedDeclaration list) + : unit + = + for decl in declarations do + visitDeclaration handler decl + + and private visitDeclaration (handler: TypedExpr -> unit) (decl: TypedDeclaration) : unit = + match decl with + | TypedDeclaration.Entity(_, subDecls) -> + for subDecl in subDecls do + visitDeclaration handler subDecl + | TypedDeclaration.MemberOrFunctionOrValue(_, _, body) -> visitExpr handler body + | TypedDeclaration.InitAction expr -> visitExpr handler expr + + and private visitExpr (handler: TypedExpr -> unit) (expr: TypedExpr) : unit = + handler expr + + match expr with + | TypedExpr.AddressOf e -> visitExpr handler e + | TypedExpr.AddressSet(e1, e2) -> + visitExpr handler e1 + visitExpr handler e2 + | TypedExpr.Application(funcExpr, _, argExprs) -> + visitExpr handler funcExpr + + argExprs + |> List.iter (visitExpr handler) + | TypedExpr.Call(objExpr, _, _, _, argExprs, _) -> + objExpr + |> Option.iter (visitExpr handler) + + argExprs + |> List.iter (visitExpr handler) + | TypedExpr.Coerce(_, e) -> visitExpr handler e + | TypedExpr.FastIntegerForLoop(start, limit, consume, _) -> + visitExpr handler start + visitExpr handler limit + visitExpr handler consume + | TypedExpr.IfThenElse(guard, thenExpr, elseExpr) -> + visitExpr handler guard + visitExpr handler thenExpr + visitExpr handler elseExpr + | TypedExpr.Lambda(_, body) -> visitExpr handler body + | TypedExpr.Let(_, bindingExpr, body) -> + visitExpr handler bindingExpr + visitExpr handler body + | TypedExpr.LetRec(bindings, body) -> + bindings + |> List.iter ( + snd + >> visitExpr handler + ) + + visitExpr handler body + | TypedExpr.NewArray(_, args) -> + args + |> List.iter (visitExpr handler) + | TypedExpr.NewDelegate(_, body) -> visitExpr handler body + | TypedExpr.NewObject(_, _, args) -> + args + |> List.iter (visitExpr handler) + | TypedExpr.NewRecord(_, args, _) -> + args + |> List.iter (visitExpr handler) + | TypedExpr.NewTuple(_, args) -> + args + |> List.iter (visitExpr handler) + | TypedExpr.NewUnionCase(_, _, args) -> + args + |> List.iter (visitExpr handler) + | TypedExpr.Quote e -> visitExpr handler e + | TypedExpr.FieldGet(objExpr, _, _) -> + objExpr + |> Option.iter (visitExpr handler) + | TypedExpr.FieldSet(objExpr, _, _, value) -> + objExpr + |> Option.iter (visitExpr handler) + + visitExpr handler value + | TypedExpr.Sequential(first, second) -> + visitExpr handler first + visitExpr handler second + | TypedExpr.TryFinally(body, finalizer) -> + visitExpr handler body + visitExpr handler finalizer + | TypedExpr.TryWith(body, _, filterExpr, _, catchExpr) -> + visitExpr handler body + visitExpr handler filterExpr + visitExpr handler catchExpr + | TypedExpr.TupleGet(_, _, tuple) -> visitExpr handler tuple + | TypedExpr.DecisionTree(decision, targets) -> + visitExpr handler decision + + targets + |> List.iter ( + snd + >> visitExpr handler + ) + | TypedExpr.DecisionTreeSuccess(_, targetExprs) -> + targetExprs + |> List.iter (visitExpr handler) + | TypedExpr.TypeLambda(_, body) -> visitExpr handler body + | TypedExpr.TypeTest(_, e) -> visitExpr handler e + | TypedExpr.UnionCaseSet(unionExpr, _, _, _, value) -> + visitExpr handler unionExpr + visitExpr handler value + | TypedExpr.UnionCaseGet(unionExpr, _, _, _) -> visitExpr handler unionExpr + | TypedExpr.UnionCaseTest(unionExpr, _, _) -> visitExpr handler unionExpr + | TypedExpr.UnionCaseTag(unionExpr, _) -> visitExpr handler unionExpr + | TypedExpr.ObjectExpr(_, baseCall, overrides, interfaceImpls) -> + visitExpr handler baseCall + + overrides + |> List.iter (fun o -> visitExpr handler o.Body) + + interfaceImpls + |> List.iter (fun (_, impls) -> + impls + |> List.iter (fun o -> visitExpr handler o.Body) + ) + | TypedExpr.TraitCall(_, _, _, _, _, argExprs) -> + argExprs + |> List.iter (visitExpr handler) + | TypedExpr.ValueSet(_, value) -> visitExpr handler value + | TypedExpr.WhileLoop(guard, body) -> + visitExpr handler guard + visitExpr handler body + | TypedExpr.ILAsm(_, _, argExprs) -> + argExprs + |> List.iter (visitExpr handler) + | TypedExpr.ILFieldGet(objExpr, _, _) -> + objExpr + |> Option.iter (visitExpr handler) + | TypedExpr.ILFieldSet(objExpr, _, _, value) -> + objExpr + |> Option.iter (visitExpr handler) + + visitExpr handler value + | TypedExpr.BaseValue _ + | TypedExpr.DefaultValue _ + | TypedExpr.ThisValue _ + | TypedExpr.Const _ + | TypedExpr.Value _ + | TypedExpr.Unknown _ -> () diff --git a/src/FSharp.Analyzers.SDK/V1/TypedTreeHandle.fs b/src/FSharp.Analyzers.SDK/V1/TypedTreeHandle.fs new file mode 100644 index 0000000..47a5704 --- /dev/null +++ b/src/FSharp.Analyzers.SDK/V1/TypedTreeHandle.fs @@ -0,0 +1,9 @@ +namespace FSharp.Analyzers.SDK.V1 + +// Opaque handle to the FCS typed tree. +// This file intentionally has no .fsi so that internal members remain +// visible to later files in the assembly (Adapter.fs, TASTCollecting.fs) +// without the signature referencing any FCS types. +[] +type TypedTreeHandle internal (contents: FSharp.Compiler.Symbols.FSharpImplementationFileContents) = + member internal _.Contents = contents diff --git a/src/FSharp.Analyzers.SDK/V1/Types.fs b/src/FSharp.Analyzers.SDK/V1/Types.fs new file mode 100644 index 0000000..3d3907f --- /dev/null +++ b/src/FSharp.Analyzers.SDK/V1/Types.fs @@ -0,0 +1,336 @@ +namespace FSharp.Analyzers.SDK.V1 + +open System + +/// A source range decoupled from the FCS range type. +type SourceRange = + { + FileName: string + /// 1-based line number. + StartLine: int + /// 0-based column number. + StartColumn: int + /// 1-based line number. + EndLine: int + /// 0-based column number. + EndColumn: int + } + +[] +type Severity = + | Info + | Hint + | Warning + | Error + +type Fix = + { + FromRange: SourceRange + FromText: string + ToText: string + } + +type Message = + { + Type: string + Message: string + Code: string + Severity: Severity + Range: SourceRange + Fixes: Fix list + } + +[] +type MemberKind = + | ClassConstructor + | Constructor + | Member + | PropertyGet + | PropertySet + | PropertyGetSet + +type MemberFlags = + { + IsInstance: bool + IsDispatchSlot: bool + IsOverrideOrExplicitImpl: bool + IsFinal: bool + MemberKind: MemberKind + } + +type GenericParameterInfo = + { + Name: string + IsSolveAtCompileTime: bool + IsCompilerGenerated: bool + IsMeasure: bool + } + +type AnalyzerIgnoreRange = + | File + | Range of commentStart: int * commentEnd: int + | NextLine of commentLine: int + | CurrentLine of commentLine: int + +type ProjectOptionsInfo = + { + ProjectFileName: string + SourceFiles: string list + ReferencedProjectsPaths: string list + OtherOptions: string list + } + +// Mutually recursive group: mirror types for FCS entities, types, members, fields, union cases. +type ParameterInfo = + { + Name: string option + Type: TypeInfo + IsOptionalArg: bool + } + +and EntityInfo = + { + FullName: string + DisplayName: string + CompiledName: string + Namespace: string option + DeclaringEntity: EntityInfo option + // Classification + IsModule: bool + IsNamespace: bool + IsUnion: bool + IsRecord: bool + IsClass: bool + IsEnum: bool + IsInterface: bool + IsValueType: bool + IsAbstractClass: bool + IsFSharpModule: bool + IsFSharpUnion: bool + IsFSharpRecord: bool + IsFSharpExceptionDeclaration: bool + IsMeasure: bool + IsDelegate: bool + IsByRef: bool + IsAbbreviation: bool + // Structural members + UnionCases: UnionCaseInfo list + FSharpFields: FieldInfo list + MembersFunctionsAndValues: MemberOrFunctionOrValueInfo list + GenericParameters: GenericParameterInfo list + BaseType: TypeInfo option + DeclaredInterfaces: TypeInfo list + AbbreviatedType: TypeInfo option + } + +and TypeInfo = + { + BasicQualifiedName: string + IsAbbreviation: bool + IsFunctionType: bool + IsTupleType: bool + IsStructTupleType: bool + IsGenericParameter: bool + HasTypeDefinition: bool + GenericArguments: TypeInfo list + AbbreviatedType: TypeInfo option + TypeDefinition: EntityInfo option + GenericParameter: GenericParameterInfo option + } + +and MemberOrFunctionOrValueInfo = + { + DisplayName: string + FullName: string + CompiledName: string + // Classification + IsProperty: bool + IsMethod: bool + IsCompilerGenerated: bool + IsMutable: bool + IsExtensionMember: bool + IsActivePattern: bool + IsConstructor: bool + IsPropertyGetterMethod: bool + IsPropertySetterMethod: bool + IsModuleValueOrMember: bool + IsValue: bool + IsMember: bool + IsInstanceMember: bool + IsInstanceMemberInCompiledCode: bool + IsDispatchSlot: bool + IsOverrideOrExplicitInterfaceImplementation: bool + // Type info + DeclaringEntity: EntityInfo option + FullType: TypeInfo + CurriedParameterGroups: ParameterInfo list list + ReturnParameter: ParameterInfo option + GenericParameters: GenericParameterInfo list + } + +and FieldInfo = + { + Name: string + FieldType: TypeInfo + IsCompilerGenerated: bool + IsMutable: bool + IsStatic: bool + IsVolatile: bool + IsLiteral: bool + LiteralValue: obj option + DeclaringEntity: EntityInfo option + } + +and UnionCaseInfo = + { + Name: string + CompiledName: string + Fields: FieldInfo list + ReturnType: TypeInfo + DeclaringEntity: EntityInfo option + HasFields: bool + } + +// Mutually recursive group: typed tree types. +type ObjectExprOverrideInfo = + { + Signature: MemberOrFunctionOrValueInfo + Body: TypedExpr + CurriedParameterGroups: MemberOrFunctionOrValueInfo list list + GenericParameters: GenericParameterInfo list + } + +and TypedExpr = + | AddressOf of lvalueExpr: TypedExpr + | AddressSet of lvalueExpr: TypedExpr * rvalueExpr: TypedExpr + | Application of funcExpr: TypedExpr * typeArgs: TypeInfo list * argExprs: TypedExpr list + | Call of + objExpr: TypedExpr option * + memberOrFunc: MemberOrFunctionOrValueInfo * + objTypeArgs: TypeInfo list * + memberTypeArgs: TypeInfo list * + argExprs: TypedExpr list * + range: SourceRange + | Coerce of targetType: TypeInfo * expr: TypedExpr + | FastIntegerForLoop of start: TypedExpr * limit: TypedExpr * consume: TypedExpr * isUp: bool + | IfThenElse of guard: TypedExpr * thenExpr: TypedExpr * elseExpr: TypedExpr + | Lambda of var: MemberOrFunctionOrValueInfo * body: TypedExpr + | Let of binding: MemberOrFunctionOrValueInfo * bindingExpr: TypedExpr * body: TypedExpr + | LetRec of bindings: (MemberOrFunctionOrValueInfo * TypedExpr) list * body: TypedExpr + | NewArray of arrayType: TypeInfo * args: TypedExpr list + | NewDelegate of delegateType: TypeInfo * body: TypedExpr + | NewObject of + ctor: MemberOrFunctionOrValueInfo * + typeArgs: TypeInfo list * + args: TypedExpr list + | NewRecord of recordType: TypeInfo * args: TypedExpr list * range: SourceRange + | NewTuple of tupleType: TypeInfo * args: TypedExpr list + | NewUnionCase of unionType: TypeInfo * case: UnionCaseInfo * args: TypedExpr list + | Quote of expr: TypedExpr + | FieldGet of objExpr: TypedExpr option * recordOrClassType: TypeInfo * field: FieldInfo + | FieldSet of + objExpr: TypedExpr option * + recordOrClassType: TypeInfo * + field: FieldInfo * + value: TypedExpr + | Sequential of first: TypedExpr * second: TypedExpr + | TryFinally of body: TypedExpr * finalizer: TypedExpr + | TryWith of + body: TypedExpr * + filterVar: MemberOrFunctionOrValueInfo * + filterExpr: TypedExpr * + catchVar: MemberOrFunctionOrValueInfo * + catchExpr: TypedExpr + | TupleGet of tupleType: TypeInfo * index: int * tuple: TypedExpr + | DecisionTree of + decision: TypedExpr * + targets: (MemberOrFunctionOrValueInfo list * TypedExpr) list + | DecisionTreeSuccess of targetIdx: int * targetExprs: TypedExpr list + | TypeLambda of genericParams: GenericParameterInfo list * body: TypedExpr + | TypeTest of ty: TypeInfo * expr: TypedExpr + | UnionCaseSet of + unionExpr: TypedExpr * + unionType: TypeInfo * + case: UnionCaseInfo * + field: FieldInfo * + value: TypedExpr + | UnionCaseGet of + unionExpr: TypedExpr * + unionType: TypeInfo * + case: UnionCaseInfo * + field: FieldInfo + | UnionCaseTest of unionExpr: TypedExpr * unionType: TypeInfo * case: UnionCaseInfo + | UnionCaseTag of unionExpr: TypedExpr * unionType: TypeInfo + | ObjectExpr of + objType: TypeInfo * + baseCall: TypedExpr * + overrides: ObjectExprOverrideInfo list * + interfaceImpls: (TypeInfo * ObjectExprOverrideInfo list) list + | TraitCall of + sourceTypes: TypeInfo list * + traitName: string * + memberFlags: MemberFlags * + typeInstantiation: TypeInfo list * + argTypes: TypeInfo list * + argExprs: TypedExpr list + | ValueSet of valToSet: MemberOrFunctionOrValueInfo * value: TypedExpr + | WhileLoop of guard: TypedExpr * body: TypedExpr + | BaseValue of baseType: TypeInfo + | DefaultValue of defaultType: TypeInfo + | ThisValue of thisType: TypeInfo + | Const of value: obj * constType: TypeInfo + | Value of valueToGet: MemberOrFunctionOrValueInfo + | ILAsm of asmCode: string * typeArgs: TypeInfo list * argExprs: TypedExpr list + | ILFieldGet of objExpr: TypedExpr option * fieldType: TypeInfo * fieldName: string + | ILFieldSet of + objExpr: TypedExpr option * + fieldType: TypeInfo * + fieldName: string * + value: TypedExpr + | Unknown of range: SourceRange + +and TypedDeclaration = + | Entity of entity: EntityInfo * subDeclarations: TypedDeclaration list + | MemberOrFunctionOrValue of + value: MemberOrFunctionOrValueInfo * + curriedArgs: MemberOrFunctionOrValueInfo list list * + body: TypedExpr + | InitAction of expr: TypedExpr + +// Symbol use types + +[] +type SymbolInfo = + | Entity of EntityInfo + | MemberOrFunctionOrValue of MemberOrFunctionOrValueInfo + | Field of FieldInfo + | UnionCase of UnionCaseInfo + | GenericParameter of GenericParameterInfo + | Other of displayName: string + +type SymbolUseInfo = + { + Symbol: SymbolInfo + Range: SourceRange + IsFromDefinition: bool + IsFromPattern: bool + IsFromType: bool + IsFromAttribute: bool + IsFromDispatchSlotImplementation: bool + IsFromComputationExpression: bool + IsFromOpenStatement: bool + } + +// TypedTreeHandle is defined in V1/TypedTreeHandle.fs (no .fsi) +// so that internal members are visible without the signature referencing FCS. + +type CliContext = + { + FileName: string + SourceText: string + TypedTree: TypedTreeHandle option + ProjectOptions: ProjectOptionsInfo + AnalyzerIgnoreRanges: Map + SymbolUsesInFile: SymbolUseInfo list + SymbolUsesInProject: SymbolUseInfo list + } diff --git a/src/FSharp.Analyzers.SDK/V1/Types.fsi b/src/FSharp.Analyzers.SDK/V1/Types.fsi new file mode 100644 index 0000000..8ca332a --- /dev/null +++ b/src/FSharp.Analyzers.SDK/V1/Types.fsi @@ -0,0 +1,325 @@ +namespace FSharp.Analyzers.SDK.V1 + +/// A source range decoupled from the FCS range type. +type SourceRange = + { + FileName: string + /// 1-based line number. + StartLine: int + /// 0-based column number. + StartColumn: int + /// 1-based line number. + EndLine: int + /// 0-based column number. + EndColumn: int + } + +[] +type Severity = + | Info + | Hint + | Warning + | Error + +type Fix = + { + FromRange: SourceRange + FromText: string + ToText: string + } + +type Message = + { + Type: string + Message: string + Code: string + Severity: Severity + Range: SourceRange + Fixes: Fix list + } + +[] +type MemberKind = + | ClassConstructor + | Constructor + | Member + | PropertyGet + | PropertySet + | PropertyGetSet + +type MemberFlags = + { + IsInstance: bool + IsDispatchSlot: bool + IsOverrideOrExplicitImpl: bool + IsFinal: bool + MemberKind: MemberKind + } + +type GenericParameterInfo = + { + Name: string + IsSolveAtCompileTime: bool + IsCompilerGenerated: bool + IsMeasure: bool + } + +type AnalyzerIgnoreRange = + | File + | Range of commentStart: int * commentEnd: int + | NextLine of commentLine: int + | CurrentLine of commentLine: int + +type ProjectOptionsInfo = + { + ProjectFileName: string + SourceFiles: string list + ReferencedProjectsPaths: string list + OtherOptions: string list + } + +type ParameterInfo = + { + Name: string option + Type: TypeInfo + IsOptionalArg: bool + } + +and EntityInfo = + { + FullName: string + DisplayName: string + CompiledName: string + Namespace: string option + DeclaringEntity: EntityInfo option + IsModule: bool + IsNamespace: bool + IsUnion: bool + IsRecord: bool + IsClass: bool + IsEnum: bool + IsInterface: bool + IsValueType: bool + IsAbstractClass: bool + IsFSharpModule: bool + IsFSharpUnion: bool + IsFSharpRecord: bool + IsFSharpExceptionDeclaration: bool + IsMeasure: bool + IsDelegate: bool + IsByRef: bool + IsAbbreviation: bool + UnionCases: UnionCaseInfo list + FSharpFields: FieldInfo list + MembersFunctionsAndValues: MemberOrFunctionOrValueInfo list + GenericParameters: GenericParameterInfo list + BaseType: TypeInfo option + DeclaredInterfaces: TypeInfo list + AbbreviatedType: TypeInfo option + } + +and TypeInfo = + { + BasicQualifiedName: string + IsAbbreviation: bool + IsFunctionType: bool + IsTupleType: bool + IsStructTupleType: bool + IsGenericParameter: bool + HasTypeDefinition: bool + GenericArguments: TypeInfo list + AbbreviatedType: TypeInfo option + TypeDefinition: EntityInfo option + GenericParameter: GenericParameterInfo option + } + +and MemberOrFunctionOrValueInfo = + { + DisplayName: string + FullName: string + CompiledName: string + IsProperty: bool + IsMethod: bool + IsCompilerGenerated: bool + IsMutable: bool + IsExtensionMember: bool + IsActivePattern: bool + IsConstructor: bool + IsPropertyGetterMethod: bool + IsPropertySetterMethod: bool + IsModuleValueOrMember: bool + IsValue: bool + IsMember: bool + IsInstanceMember: bool + IsInstanceMemberInCompiledCode: bool + IsDispatchSlot: bool + IsOverrideOrExplicitInterfaceImplementation: bool + DeclaringEntity: EntityInfo option + FullType: TypeInfo + CurriedParameterGroups: ParameterInfo list list + ReturnParameter: ParameterInfo option + GenericParameters: GenericParameterInfo list + } + +and FieldInfo = + { + Name: string + FieldType: TypeInfo + IsCompilerGenerated: bool + IsMutable: bool + IsStatic: bool + IsVolatile: bool + IsLiteral: bool + LiteralValue: obj option + DeclaringEntity: EntityInfo option + } + +and UnionCaseInfo = + { + Name: string + CompiledName: string + Fields: FieldInfo list + ReturnType: TypeInfo + DeclaringEntity: EntityInfo option + HasFields: bool + } + +type ObjectExprOverrideInfo = + { + Signature: MemberOrFunctionOrValueInfo + Body: TypedExpr + CurriedParameterGroups: MemberOrFunctionOrValueInfo list list + GenericParameters: GenericParameterInfo list + } + +and TypedExpr = + | AddressOf of lvalueExpr: TypedExpr + | AddressSet of lvalueExpr: TypedExpr * rvalueExpr: TypedExpr + | Application of funcExpr: TypedExpr * typeArgs: TypeInfo list * argExprs: TypedExpr list + | Call of + objExpr: TypedExpr option * + memberOrFunc: MemberOrFunctionOrValueInfo * + objTypeArgs: TypeInfo list * + memberTypeArgs: TypeInfo list * + argExprs: TypedExpr list * + range: SourceRange + | Coerce of targetType: TypeInfo * expr: TypedExpr + | FastIntegerForLoop of start: TypedExpr * limit: TypedExpr * consume: TypedExpr * isUp: bool + | IfThenElse of guard: TypedExpr * thenExpr: TypedExpr * elseExpr: TypedExpr + | Lambda of var: MemberOrFunctionOrValueInfo * body: TypedExpr + | Let of binding: MemberOrFunctionOrValueInfo * bindingExpr: TypedExpr * body: TypedExpr + | LetRec of bindings: (MemberOrFunctionOrValueInfo * TypedExpr) list * body: TypedExpr + | NewArray of arrayType: TypeInfo * args: TypedExpr list + | NewDelegate of delegateType: TypeInfo * body: TypedExpr + | NewObject of + ctor: MemberOrFunctionOrValueInfo * + typeArgs: TypeInfo list * + args: TypedExpr list + | NewRecord of recordType: TypeInfo * args: TypedExpr list * range: SourceRange + | NewTuple of tupleType: TypeInfo * args: TypedExpr list + | NewUnionCase of unionType: TypeInfo * case: UnionCaseInfo * args: TypedExpr list + | Quote of expr: TypedExpr + | FieldGet of objExpr: TypedExpr option * recordOrClassType: TypeInfo * field: FieldInfo + | FieldSet of + objExpr: TypedExpr option * + recordOrClassType: TypeInfo * + field: FieldInfo * + value: TypedExpr + | Sequential of first: TypedExpr * second: TypedExpr + | TryFinally of body: TypedExpr * finalizer: TypedExpr + | TryWith of + body: TypedExpr * + filterVar: MemberOrFunctionOrValueInfo * + filterExpr: TypedExpr * + catchVar: MemberOrFunctionOrValueInfo * + catchExpr: TypedExpr + | TupleGet of tupleType: TypeInfo * index: int * tuple: TypedExpr + | DecisionTree of + decision: TypedExpr * + targets: (MemberOrFunctionOrValueInfo list * TypedExpr) list + | DecisionTreeSuccess of targetIdx: int * targetExprs: TypedExpr list + | TypeLambda of genericParams: GenericParameterInfo list * body: TypedExpr + | TypeTest of ty: TypeInfo * expr: TypedExpr + | UnionCaseSet of + unionExpr: TypedExpr * + unionType: TypeInfo * + case: UnionCaseInfo * + field: FieldInfo * + value: TypedExpr + | UnionCaseGet of + unionExpr: TypedExpr * + unionType: TypeInfo * + case: UnionCaseInfo * + field: FieldInfo + | UnionCaseTest of unionExpr: TypedExpr * unionType: TypeInfo * case: UnionCaseInfo + | UnionCaseTag of unionExpr: TypedExpr * unionType: TypeInfo + | ObjectExpr of + objType: TypeInfo * + baseCall: TypedExpr * + overrides: ObjectExprOverrideInfo list * + interfaceImpls: (TypeInfo * ObjectExprOverrideInfo list) list + | TraitCall of + sourceTypes: TypeInfo list * + traitName: string * + memberFlags: MemberFlags * + typeInstantiation: TypeInfo list * + argTypes: TypeInfo list * + argExprs: TypedExpr list + | ValueSet of valToSet: MemberOrFunctionOrValueInfo * value: TypedExpr + | WhileLoop of guard: TypedExpr * body: TypedExpr + | BaseValue of baseType: TypeInfo + | DefaultValue of defaultType: TypeInfo + | ThisValue of thisType: TypeInfo + | Const of value: obj * constType: TypeInfo + | Value of valueToGet: MemberOrFunctionOrValueInfo + | ILAsm of asmCode: string * typeArgs: TypeInfo list * argExprs: TypedExpr list + | ILFieldGet of objExpr: TypedExpr option * fieldType: TypeInfo * fieldName: string + | ILFieldSet of + objExpr: TypedExpr option * + fieldType: TypeInfo * + fieldName: string * + value: TypedExpr + | Unknown of range: SourceRange + +and TypedDeclaration = + | Entity of entity: EntityInfo * subDeclarations: TypedDeclaration list + | MemberOrFunctionOrValue of + value: MemberOrFunctionOrValueInfo * + curriedArgs: MemberOrFunctionOrValueInfo list list * + body: TypedExpr + | InitAction of expr: TypedExpr + +[] +type SymbolInfo = + | Entity of EntityInfo + | MemberOrFunctionOrValue of MemberOrFunctionOrValueInfo + | Field of FieldInfo + | UnionCase of UnionCaseInfo + | GenericParameter of GenericParameterInfo + | Other of displayName: string + +type SymbolUseInfo = + { + Symbol: SymbolInfo + Range: SourceRange + IsFromDefinition: bool + IsFromPattern: bool + IsFromType: bool + IsFromAttribute: bool + IsFromDispatchSlotImplementation: bool + IsFromComputationExpression: bool + IsFromOpenStatement: bool + } + +// TypedTreeHandle is defined in V1/TypedTreeHandle.fs (no .fsi). + +type CliContext = + { + FileName: string + SourceText: string + TypedTree: TypedTreeHandle option + ProjectOptions: ProjectOptionsInfo + AnalyzerIgnoreRanges: Map + SymbolUsesInFile: SymbolUseInfo list + SymbolUsesInProject: SymbolUseInfo list + }