From 3dbd175d5ad9777de132c3ddfff6b48906019e77 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 1 Jul 2025 17:11:22 +0400 Subject: [PATCH 1/4] feat: support SLNX solution format --- CHANGELOG.md | 4 +- Directory.Packages.props | 1 - FSharpLint.sln | 1 + FSharpLint.slnx | 108 ++++++++++++++++++ src/FSharpLint.Core/Application/Lint.fs | 64 ++++++----- src/FSharpLint.Core/FSharpLint.Core.fsproj | 4 +- ...harpLint.FunctionalTest.TestedProject.slnx | 4 + tests/FSharpLint.FunctionalTest/TestApi.fs | 36 ++---- 8 files changed, 165 insertions(+), 57 deletions(-) create mode 100644 FSharpLint.slnx create mode 100644 tests/FSharpLint.FunctionalTest.TestedProject/FSharpLint.FunctionalTest.TestedProject.slnx diff --git a/CHANGELOG.md b/CHANGELOG.md index c80f64b58..98d972e8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Use string interpolation instead of `+` concatenation #724 [@xperiandri] - Run tests in parallel #728 [@xperiandri] - Remove `NoPartialFunctions` compiler workaround (#698) [@webwarrior-ws] +- Add SLNX support and migrate to SLNX solution #723 [@xperiandri]\ + Remove `Ionide.ProjInfo.Sln` NuGet package dependency ## [0.24.2] - 2024-02-29 @@ -26,7 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New rule FavourNonMutablePropertyInitialization #683 #662 #535 [@webwarrior-ws] - Workaround for NoPartialFuncs bug #682 [@webwarrior-ws] - Fix AvoidSinglePipeOperator false positive #684 [@Mersho] -- docs(CSS): capitalize

rather than uppercase [@knocte] +- docs(CSS): capitalize `

` rather than uppercase [@knocte] - Simplify SelfCheck #679 [@knocte] diff --git a/Directory.Packages.props b/Directory.Packages.props index 78a0e4d24..76426fc2c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,7 +14,6 @@ - diff --git a/FSharpLint.sln b/FSharpLint.sln index cb85d26a2..395c1afa4 100644 --- a/FSharpLint.sln +++ b/FSharpLint.sln @@ -22,6 +22,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{270E691D-ECA1-4BC5-B851-C5431A64E9FA}" ProjectSection(SolutionItems) = preProject build.fsx = build.fsx + CHANGELOG.md = CHANGELOG.md Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props EndProjectSection diff --git a/FSharpLint.slnx b/FSharpLint.slnx new file mode 100644 index 000000000..2d3d31087 --- /dev/null +++ b/FSharpLint.slnx @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/FSharpLint.Core/Application/Lint.fs b/src/FSharpLint.Core/Application/Lint.fs index 2ad5e7084..f1fb06839 100644 --- a/src/FSharpLint.Core/Application/Lint.fs +++ b/src/FSharpLint.Core/Application/Lint.fs @@ -9,6 +9,7 @@ open System.Threading open FSharp.Compiler.Text open FSharp.Compiler.Diagnostics open FSharp.Compiler.CodeAnalysis +open Microsoft.Build.Construction open Ionide.ProjInfo.ProjectSystem open Ionide.ProjInfo.FCS open FSharpLint.Core @@ -464,36 +465,41 @@ module Lint = | Ok config -> let optionalParams = { optionalParams with Configuration = ConfigurationParam.Configuration config } - let projectsInSolution = - File.ReadAllText(solutionFilePath) - |> String.toLines - |> Array.filter (fun (s, _, _) -> s.StartsWith("Project") && s.Contains(".fsproj")) - |> Array.map (fun (s, _, _) -> - let endIndex = s.IndexOf(".fsproj") + 7 - let startIndex = s.IndexOf(",") + 1 - let projectPath = s.Substring(startIndex, endIndex - startIndex).Trim([|'"'; ' '|]) - let projectPath = - if Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Windows) then + try + // Use Microsoft.Build.Construction.SolutionFile for modern solution parsing + let solutionFile = SolutionFile.Parse(solutionFilePath) + + let projectsInSolution = + solutionFile.ProjectsInOrder + |> Seq.filter (fun project -> + project.ProjectType = SolutionProjectType.KnownToBeMSBuildFormat && + project.RelativePath.EndsWith(".fsproj")) + |> Seq.map (fun project -> + let projectPath = Path.Combine(solutionFolder, project.RelativePath) + if RuntimeInformation.IsOSPlatform(OSPlatform.Windows) then projectPath - else // For non-Windows, we need to convert the project path in the solution to Unix format. - projectPath.Replace("\\", "/") - Path.Combine(solutionFolder, projectPath)) - - let (successes, failures) = - projectsInSolution - |> Array.map (fun projectFilePath -> lintProject optionalParams projectFilePath toolsPath) - |> Array.fold (fun (successes, failures) result -> - match result with - | LintResult.Success warnings -> - (List.append warnings successes, failures) - | LintResult.Failure err -> - (successes, err :: failures)) ([], []) - - match failures with - | [] -> - LintResult.Success successes - | firstErr :: _ -> - LintResult.Failure firstErr + else // For non-Windows, ensure Unix format + projectPath.Replace("\\", "/")) + |> Seq.toArray + + let (successes, failures) = + projectsInSolution + |> Array.map (fun projectFilePath -> lintProject optionalParams projectFilePath toolsPath) + |> Array.fold (fun (successes, failures) result -> + match result with + | LintResult.Success warnings -> + (List.append warnings successes, failures) + | LintResult.Failure err -> + (successes, err :: failures)) ([], []) + + match failures with + | [] -> + LintResult.Success successes + | firstErr :: _ -> + LintResult.Failure firstErr + with + | ex -> + LintResult.Failure (MSBuildFailedToLoadProjectFile (solutionFilePath, BuildFailure.InvalidProjectFileMessage ex.Message)) | Error err -> LintResult.Failure (RunTimeConfigError err) else diff --git a/src/FSharpLint.Core/FSharpLint.Core.fsproj b/src/FSharpLint.Core/FSharpLint.Core.fsproj index 5fac0ce03..f6dac62ba 100644 --- a/src/FSharpLint.Core/FSharpLint.Core.fsproj +++ b/src/FSharpLint.Core/FSharpLint.Core.fsproj @@ -127,6 +127,9 @@ + + + Always @@ -142,7 +145,6 @@ - diff --git a/tests/FSharpLint.FunctionalTest.TestedProject/FSharpLint.FunctionalTest.TestedProject.slnx b/tests/FSharpLint.FunctionalTest.TestedProject/FSharpLint.FunctionalTest.TestedProject.slnx new file mode 100644 index 000000000..334a11125 --- /dev/null +++ b/tests/FSharpLint.FunctionalTest.TestedProject/FSharpLint.FunctionalTest.TestedProject.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/FSharpLint.FunctionalTest/TestApi.fs b/tests/FSharpLint.FunctionalTest/TestApi.fs index ffa91a3aa..9add375e0 100644 --- a/tests/FSharpLint.FunctionalTest/TestApi.fs +++ b/tests/FSharpLint.FunctionalTest/TestApi.fs @@ -102,40 +102,26 @@ module TestApi = | LintResult.Failure err -> Assert.True(false, string err) - [] - member _.``Lint solution via absolute path``() = + [] + [] + member _.``Lint solution via absolute path``(solutionFileName: string, expectedWarnings: int) = let projectPath = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" - let solutionFile = projectPath "FSharpLint.FunctionalTest.TestedProject.sln" + let solutionFile = projectPath solutionFileName let result = lintSolution OptionalLintParameters.Default solutionFile toolsPath match result with | LintResult.Success warnings -> - Assert.AreEqual(18, warnings.Length) - | LintResult.Failure err -> - Assert.True(false, string err) - -#if NETCOREAPP // GetRelativePath is netcore-only - [] - member _.``Lint project via relative path``() = - let projectPath = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore" - let projectFile = projectPath "FSharpLint.FunctionalTest.TestedProject.NetCore.fsproj" - - let relativePathToProjectFile = Path.GetRelativePath (Directory.GetCurrentDirectory(), projectFile) - - let result = lintProject OptionalLintParameters.Default relativePathToProjectFile toolsPath - - match result with - | LintResult.Success warnings -> - Assert.AreEqual(9, warnings.Length) + Assert.AreEqual(expectedWarnings, warnings.Length) | LintResult.Failure err -> Assert.True(false, string err) - () - [] - member _.``Lint solution via relative path``() = +#if NETCOREAPP + [] + [] + member _.``Lint solution via relative path``(solutionFileName: string, expectedWarnings: int) = let projectPath = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" - let solutionFile = projectPath "FSharpLint.FunctionalTest.TestedProject.sln" + let solutionFile = projectPath solutionFileName let relativePathToSolutionFile = Path.GetRelativePath (Directory.GetCurrentDirectory(), solutionFile) @@ -143,7 +129,7 @@ module TestApi = match result with | LintResult.Success warnings -> - Assert.AreEqual(18, warnings.Length) + Assert.AreEqual(expectedWarnings, warnings.Length) | LintResult.Failure err -> Assert.True(false, string err) #endif From afdfae0289c2d74bb3e3abc45b48cdf80100854e Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 1 Jul 2025 17:48:30 +0400 Subject: [PATCH 2/4] chore: remove `sln` solution in favor of `slnx` --- FSharpLint.sln | 217 ------------------------------------------------- build.fsx | 9 +- 2 files changed, 5 insertions(+), 221 deletions(-) delete mode 100644 FSharpLint.sln diff --git a/FSharpLint.sln b/FSharpLint.sln deleted file mode 100644 index 395c1afa4..000000000 --- a/FSharpLint.sln +++ /dev/null @@ -1,217 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.14.36221.1 -MinimumVisualStudioVersion = 15.0.26124.0 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{40C2798B-7078-4D4F-BD37-195240CB827B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{1CD44876-BCDC-4C93-9DC2-C45244BD62AE}" -EndProject -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharpLint.Console", "src\FSharpLint.Console\FSharpLint.Console.fsproj", "{AC24094C-4AB5-4A6B-8430-3D21EF788AEC}" -EndProject -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharpLint.Core", "src\FSharpLint.Core\FSharpLint.Core.fsproj", "{7691E4E4-EC5B-4751-9578-109258A3B1DA}" -EndProject -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharpLint.Core.Tests", "tests\FSharpLint.Core.Tests\FSharpLint.Core.Tests.fsproj", "{E51042D9-CE14-4373-AE1E-FEB78FE23F40}" -EndProject -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharpLint.FunctionalTest", "tests\FSharpLint.FunctionalTest\FSharpLint.FunctionalTest.fsproj", "{0DE70980-49E9-4613-84D4-FCF6ACABA5EE}" -EndProject -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharpLint.Console.Tests", "tests\FSharpLint.Console.Tests\FSharpLint.Console.Tests.fsproj", "{73AD322D-2E3F-45C4-8DB2-179571DECCD0}" -EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharpLint.Benchmarks", "tests\FSharpLint.Benchmarks\FSharpLint.Benchmarks.fsproj", "{B4A92AC6-F74A-4709-B2F7-6C5BABBFDEB0}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{270E691D-ECA1-4BC5-B851-C5431A64E9FA}" - ProjectSection(SolutionItems) = preProject - build.fsx = build.fsx - CHANGELOG.md = CHANGELOG.md - Directory.Build.props = Directory.Build.props - Directory.Packages.props = Directory.Packages.props - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{E1E03FFE-30DF-4522-83DA-9089147B431E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "rules", "rules", "{AEBB56D7-30B4-40D7-B065-54B8BE960298}" - ProjectSection(SolutionItems) = preProject - docs\content\how-tos\rules\FL0001.md = docs\content\how-tos\rules\FL0001.md - docs\content\how-tos\rules\FL0002.md = docs\content\how-tos\rules\FL0002.md - docs\content\how-tos\rules\FL0003.md = docs\content\how-tos\rules\FL0003.md - docs\content\how-tos\rules\FL0004.md = docs\content\how-tos\rules\FL0004.md - docs\content\how-tos\rules\FL0005.md = docs\content\how-tos\rules\FL0005.md - docs\content\how-tos\rules\FL0006.md = docs\content\how-tos\rules\FL0006.md - docs\content\how-tos\rules\FL0007.md = docs\content\how-tos\rules\FL0007.md - docs\content\how-tos\rules\FL0008.md = docs\content\how-tos\rules\FL0008.md - docs\content\how-tos\rules\FL0009.md = docs\content\how-tos\rules\FL0009.md - docs\content\how-tos\rules\FL0010.md = docs\content\how-tos\rules\FL0010.md - docs\content\how-tos\rules\FL0011.md = docs\content\how-tos\rules\FL0011.md - docs\content\how-tos\rules\FL0012.md = docs\content\how-tos\rules\FL0012.md - docs\content\how-tos\rules\FL0013.md = docs\content\how-tos\rules\FL0013.md - docs\content\how-tos\rules\FL0014.md = docs\content\how-tos\rules\FL0014.md - docs\content\how-tos\rules\FL0015.md = docs\content\how-tos\rules\FL0015.md - docs\content\how-tos\rules\FL0016.md = docs\content\how-tos\rules\FL0016.md - docs\content\how-tos\rules\FL0017.md = docs\content\how-tos\rules\FL0017.md - docs\content\how-tos\rules\FL0018.md = docs\content\how-tos\rules\FL0018.md - docs\content\how-tos\rules\FL0019.md = docs\content\how-tos\rules\FL0019.md - docs\content\how-tos\rules\FL0020.md = docs\content\how-tos\rules\FL0020.md - docs\content\how-tos\rules\FL0021.md = docs\content\how-tos\rules\FL0021.md - docs\content\how-tos\rules\FL0022.md = docs\content\how-tos\rules\FL0022.md - docs\content\how-tos\rules\FL0023.md = docs\content\how-tos\rules\FL0023.md - docs\content\how-tos\rules\FL0024.md = docs\content\how-tos\rules\FL0024.md - docs\content\how-tos\rules\FL0025.md = docs\content\how-tos\rules\FL0025.md - docs\content\how-tos\rules\FL0026.md = docs\content\how-tos\rules\FL0026.md - docs\content\how-tos\rules\FL0027.md = docs\content\how-tos\rules\FL0027.md - docs\content\how-tos\rules\FL0028.md = docs\content\how-tos\rules\FL0028.md - docs\content\how-tos\rules\FL0029.md = docs\content\how-tos\rules\FL0029.md - docs\content\how-tos\rules\FL0030.md = docs\content\how-tos\rules\FL0030.md - docs\content\how-tos\rules\FL0031.md = docs\content\how-tos\rules\FL0031.md - docs\content\how-tos\rules\FL0032.md = docs\content\how-tos\rules\FL0032.md - docs\content\how-tos\rules\FL0033.md = docs\content\how-tos\rules\FL0033.md - docs\content\how-tos\rules\FL0034.md = docs\content\how-tos\rules\FL0034.md - docs\content\how-tos\rules\FL0035.md = docs\content\how-tos\rules\FL0035.md - docs\content\how-tos\rules\FL0036.md = docs\content\how-tos\rules\FL0036.md - docs\content\how-tos\rules\FL0037.md = docs\content\how-tos\rules\FL0037.md - docs\content\how-tos\rules\FL0038.md = docs\content\how-tos\rules\FL0038.md - docs\content\how-tos\rules\FL0039.md = docs\content\how-tos\rules\FL0039.md - docs\content\how-tos\rules\FL0040.md = docs\content\how-tos\rules\FL0040.md - docs\content\how-tos\rules\FL0041.md = docs\content\how-tos\rules\FL0041.md - docs\content\how-tos\rules\FL0042.md = docs\content\how-tos\rules\FL0042.md - docs\content\how-tos\rules\FL0043.md = docs\content\how-tos\rules\FL0043.md - docs\content\how-tos\rules\FL0044.md = docs\content\how-tos\rules\FL0044.md - docs\content\how-tos\rules\FL0045.md = docs\content\how-tos\rules\FL0045.md - docs\content\how-tos\rules\FL0046.md = docs\content\how-tos\rules\FL0046.md - docs\content\how-tos\rules\FL0047.md = docs\content\how-tos\rules\FL0047.md - docs\content\how-tos\rules\FL0048.md = docs\content\how-tos\rules\FL0048.md - docs\content\how-tos\rules\FL0049.md = docs\content\how-tos\rules\FL0049.md - docs\content\how-tos\rules\FL0050.md = docs\content\how-tos\rules\FL0050.md - docs\content\how-tos\rules\FL0051.md = docs\content\how-tos\rules\FL0051.md - docs\content\how-tos\rules\FL0052.md = docs\content\how-tos\rules\FL0052.md - docs\content\how-tos\rules\FL0053.md = docs\content\how-tos\rules\FL0053.md - docs\content\how-tos\rules\FL0054.md = docs\content\how-tos\rules\FL0054.md - docs\content\how-tos\rules\FL0055.md = docs\content\how-tos\rules\FL0055.md - docs\content\how-tos\rules\FL0056.md = docs\content\how-tos\rules\FL0056.md - docs\content\how-tos\rules\FL0057.md = docs\content\how-tos\rules\FL0057.md - docs\content\how-tos\rules\FL0058.md = docs\content\how-tos\rules\FL0058.md - docs\content\how-tos\rules\FL0059.md = docs\content\how-tos\rules\FL0059.md - docs\content\how-tos\rules\FL0060.md = docs\content\how-tos\rules\FL0060.md - docs\content\how-tos\rules\FL0061.md = docs\content\how-tos\rules\FL0061.md - docs\content\how-tos\rules\FL0062.md = docs\content\how-tos\rules\FL0062.md - docs\content\how-tos\rules\FL0063.md = docs\content\how-tos\rules\FL0063.md - docs\content\how-tos\rules\FL0064.md = docs\content\how-tos\rules\FL0064.md - docs\content\how-tos\rules\FL0065.md = docs\content\how-tos\rules\FL0065.md - docs\content\how-tos\rules\FL0066.md = docs\content\how-tos\rules\FL0066.md - docs\content\how-tos\rules\FL0067.md = docs\content\how-tos\rules\FL0067.md - docs\content\how-tos\rules\FL0068.md = docs\content\how-tos\rules\FL0068.md - docs\content\how-tos\rules\FL0069.md = docs\content\how-tos\rules\FL0069.md - docs\content\how-tos\rules\FL0070.md = docs\content\how-tos\rules\FL0070.md - docs\content\how-tos\rules\FL0071.md = docs\content\how-tos\rules\FL0071.md - docs\content\how-tos\rules\FL0072.md = docs\content\how-tos\rules\FL0072.md - docs\content\how-tos\rules\FL0073.md = docs\content\how-tos\rules\FL0073.md - docs\content\how-tos\rules\FL0074.md = docs\content\how-tos\rules\FL0074.md - docs\content\how-tos\rules\FL0075.md = docs\content\how-tos\rules\FL0075.md - docs\content\how-tos\rules\FL0076.md = docs\content\how-tos\rules\FL0076.md - docs\content\how-tos\rules\FL0077.md = docs\content\how-tos\rules\FL0077.md - docs\content\how-tos\rules\FL0078.md = docs\content\how-tos\rules\FL0078.md - docs\content\how-tos\rules\FL0079.md = docs\content\how-tos\rules\FL0079.md - docs\content\how-tos\rules\FL0080.md = docs\content\how-tos\rules\FL0080.md - docs\content\how-tos\rules\FL0081.md = docs\content\how-tos\rules\FL0081.md - docs\content\how-tos\rules\FL0082.md = docs\content\how-tos\rules\FL0082.md - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {AC24094C-4AB5-4A6B-8430-3D21EF788AEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AC24094C-4AB5-4A6B-8430-3D21EF788AEC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AC24094C-4AB5-4A6B-8430-3D21EF788AEC}.Debug|x64.ActiveCfg = Debug|Any CPU - {AC24094C-4AB5-4A6B-8430-3D21EF788AEC}.Debug|x64.Build.0 = Debug|Any CPU - {AC24094C-4AB5-4A6B-8430-3D21EF788AEC}.Debug|x86.ActiveCfg = Debug|Any CPU - {AC24094C-4AB5-4A6B-8430-3D21EF788AEC}.Debug|x86.Build.0 = Debug|Any CPU - {AC24094C-4AB5-4A6B-8430-3D21EF788AEC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AC24094C-4AB5-4A6B-8430-3D21EF788AEC}.Release|Any CPU.Build.0 = Release|Any CPU - {AC24094C-4AB5-4A6B-8430-3D21EF788AEC}.Release|x64.ActiveCfg = Release|Any CPU - {AC24094C-4AB5-4A6B-8430-3D21EF788AEC}.Release|x64.Build.0 = Release|Any CPU - {AC24094C-4AB5-4A6B-8430-3D21EF788AEC}.Release|x86.ActiveCfg = Release|Any CPU - {AC24094C-4AB5-4A6B-8430-3D21EF788AEC}.Release|x86.Build.0 = Release|Any CPU - {7691E4E4-EC5B-4751-9578-109258A3B1DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7691E4E4-EC5B-4751-9578-109258A3B1DA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7691E4E4-EC5B-4751-9578-109258A3B1DA}.Debug|x64.ActiveCfg = Debug|Any CPU - {7691E4E4-EC5B-4751-9578-109258A3B1DA}.Debug|x64.Build.0 = Debug|Any CPU - {7691E4E4-EC5B-4751-9578-109258A3B1DA}.Debug|x86.ActiveCfg = Debug|Any CPU - {7691E4E4-EC5B-4751-9578-109258A3B1DA}.Debug|x86.Build.0 = Debug|Any CPU - {7691E4E4-EC5B-4751-9578-109258A3B1DA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7691E4E4-EC5B-4751-9578-109258A3B1DA}.Release|Any CPU.Build.0 = Release|Any CPU - {7691E4E4-EC5B-4751-9578-109258A3B1DA}.Release|x64.ActiveCfg = Release|Any CPU - {7691E4E4-EC5B-4751-9578-109258A3B1DA}.Release|x64.Build.0 = Release|Any CPU - {7691E4E4-EC5B-4751-9578-109258A3B1DA}.Release|x86.ActiveCfg = Release|Any CPU - {7691E4E4-EC5B-4751-9578-109258A3B1DA}.Release|x86.Build.0 = Release|Any CPU - {E51042D9-CE14-4373-AE1E-FEB78FE23F40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E51042D9-CE14-4373-AE1E-FEB78FE23F40}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E51042D9-CE14-4373-AE1E-FEB78FE23F40}.Debug|x64.ActiveCfg = Debug|Any CPU - {E51042D9-CE14-4373-AE1E-FEB78FE23F40}.Debug|x64.Build.0 = Debug|Any CPU - {E51042D9-CE14-4373-AE1E-FEB78FE23F40}.Debug|x86.ActiveCfg = Debug|Any CPU - {E51042D9-CE14-4373-AE1E-FEB78FE23F40}.Debug|x86.Build.0 = Debug|Any CPU - {E51042D9-CE14-4373-AE1E-FEB78FE23F40}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E51042D9-CE14-4373-AE1E-FEB78FE23F40}.Release|Any CPU.Build.0 = Release|Any CPU - {E51042D9-CE14-4373-AE1E-FEB78FE23F40}.Release|x64.ActiveCfg = Release|Any CPU - {E51042D9-CE14-4373-AE1E-FEB78FE23F40}.Release|x64.Build.0 = Release|Any CPU - {E51042D9-CE14-4373-AE1E-FEB78FE23F40}.Release|x86.ActiveCfg = Release|Any CPU - {E51042D9-CE14-4373-AE1E-FEB78FE23F40}.Release|x86.Build.0 = Release|Any CPU - {0DE70980-49E9-4613-84D4-FCF6ACABA5EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0DE70980-49E9-4613-84D4-FCF6ACABA5EE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0DE70980-49E9-4613-84D4-FCF6ACABA5EE}.Debug|x64.ActiveCfg = Debug|Any CPU - {0DE70980-49E9-4613-84D4-FCF6ACABA5EE}.Debug|x64.Build.0 = Debug|Any CPU - {0DE70980-49E9-4613-84D4-FCF6ACABA5EE}.Debug|x86.ActiveCfg = Debug|Any CPU - {0DE70980-49E9-4613-84D4-FCF6ACABA5EE}.Debug|x86.Build.0 = Debug|Any CPU - {0DE70980-49E9-4613-84D4-FCF6ACABA5EE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0DE70980-49E9-4613-84D4-FCF6ACABA5EE}.Release|Any CPU.Build.0 = Release|Any CPU - {0DE70980-49E9-4613-84D4-FCF6ACABA5EE}.Release|x64.ActiveCfg = Release|Any CPU - {0DE70980-49E9-4613-84D4-FCF6ACABA5EE}.Release|x64.Build.0 = Release|Any CPU - {0DE70980-49E9-4613-84D4-FCF6ACABA5EE}.Release|x86.ActiveCfg = Release|Any CPU - {0DE70980-49E9-4613-84D4-FCF6ACABA5EE}.Release|x86.Build.0 = Release|Any CPU - {73AD322D-2E3F-45C4-8DB2-179571DECCD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {73AD322D-2E3F-45C4-8DB2-179571DECCD0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {73AD322D-2E3F-45C4-8DB2-179571DECCD0}.Debug|x64.ActiveCfg = Debug|Any CPU - {73AD322D-2E3F-45C4-8DB2-179571DECCD0}.Debug|x64.Build.0 = Debug|Any CPU - {73AD322D-2E3F-45C4-8DB2-179571DECCD0}.Debug|x86.ActiveCfg = Debug|Any CPU - {73AD322D-2E3F-45C4-8DB2-179571DECCD0}.Debug|x86.Build.0 = Debug|Any CPU - {73AD322D-2E3F-45C4-8DB2-179571DECCD0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {73AD322D-2E3F-45C4-8DB2-179571DECCD0}.Release|Any CPU.Build.0 = Release|Any CPU - {73AD322D-2E3F-45C4-8DB2-179571DECCD0}.Release|x64.ActiveCfg = Release|Any CPU - {73AD322D-2E3F-45C4-8DB2-179571DECCD0}.Release|x64.Build.0 = Release|Any CPU - {73AD322D-2E3F-45C4-8DB2-179571DECCD0}.Release|x86.ActiveCfg = Release|Any CPU - {73AD322D-2E3F-45C4-8DB2-179571DECCD0}.Release|x86.Build.0 = Release|Any CPU - {B4A92AC6-F74A-4709-B2F7-6C5BABBFDEB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B4A92AC6-F74A-4709-B2F7-6C5BABBFDEB0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B4A92AC6-F74A-4709-B2F7-6C5BABBFDEB0}.Debug|x64.ActiveCfg = Debug|Any CPU - {B4A92AC6-F74A-4709-B2F7-6C5BABBFDEB0}.Debug|x64.Build.0 = Debug|Any CPU - {B4A92AC6-F74A-4709-B2F7-6C5BABBFDEB0}.Debug|x86.ActiveCfg = Debug|Any CPU - {B4A92AC6-F74A-4709-B2F7-6C5BABBFDEB0}.Debug|x86.Build.0 = Debug|Any CPU - {B4A92AC6-F74A-4709-B2F7-6C5BABBFDEB0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B4A92AC6-F74A-4709-B2F7-6C5BABBFDEB0}.Release|Any CPU.Build.0 = Release|Any CPU - {B4A92AC6-F74A-4709-B2F7-6C5BABBFDEB0}.Release|x64.ActiveCfg = Release|Any CPU - {B4A92AC6-F74A-4709-B2F7-6C5BABBFDEB0}.Release|x64.Build.0 = Release|Any CPU - {B4A92AC6-F74A-4709-B2F7-6C5BABBFDEB0}.Release|x86.ActiveCfg = Release|Any CPU - {B4A92AC6-F74A-4709-B2F7-6C5BABBFDEB0}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {AC24094C-4AB5-4A6B-8430-3D21EF788AEC} = {40C2798B-7078-4D4F-BD37-195240CB827B} - {7691E4E4-EC5B-4751-9578-109258A3B1DA} = {40C2798B-7078-4D4F-BD37-195240CB827B} - {E51042D9-CE14-4373-AE1E-FEB78FE23F40} = {1CD44876-BCDC-4C93-9DC2-C45244BD62AE} - {0DE70980-49E9-4613-84D4-FCF6ACABA5EE} = {1CD44876-BCDC-4C93-9DC2-C45244BD62AE} - {73AD322D-2E3F-45C4-8DB2-179571DECCD0} = {1CD44876-BCDC-4C93-9DC2-C45244BD62AE} - {B4A92AC6-F74A-4709-B2F7-6C5BABBFDEB0} = {1CD44876-BCDC-4C93-9DC2-C45244BD62AE} - {E1E03FFE-30DF-4522-83DA-9089147B431E} = {270E691D-ECA1-4BC5-B851-C5431A64E9FA} - {AEBB56D7-30B4-40D7-B065-54B8BE960298} = {E1E03FFE-30DF-4522-83DA-9089147B431E} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {B54B4B7D-F019-48A3-BB5B-635B68FE41C3} - EndGlobalSection -EndGlobal diff --git a/build.fsx b/build.fsx index 1942bc876..cb64e6246 100644 --- a/build.fsx +++ b/build.fsx @@ -47,6 +47,7 @@ Target.initEnvironment() // -------------------------------------------------------------------------------------- let project = "FSharpLint" +let solutionFileName = "FSharpLint.slnx" let authors = "Matthew Mcveigh" @@ -139,7 +140,7 @@ Target.create "Clean" (fun _ -> ) Target.create "Build" (fun _ -> - DotNet.build id "FSharpLint.sln" + DotNet.build id solutionFileName ) let filterPerformanceTests (p:DotNet.TestOptions) = { p with Filter = Some "\"TestCategory!=Performance\""; Configuration = DotNet.Release } @@ -168,7 +169,7 @@ Target.create "BuildRelease" (fun _ -> OutputPath = Some buildDir MSBuildParams = { p.MSBuildParams with Properties = properties } } - ) "FSharpLint.sln" + ) solutionFileName ) @@ -188,7 +189,7 @@ Target.create "Pack" (fun _ -> OutputPath = Some nugetDir MSBuildParams = { p.MSBuildParams with Properties = properties } } - ) "FSharpLint.sln" + ) solutionFileName ) Target.create "Push" (fun _ -> @@ -250,7 +251,7 @@ Target.create "SelfCheck" (fun _ -> let srcDir = Path.Combine(rootDir.FullName, "src") |> DirectoryInfo let consoleProj = Path.Combine(srcDir.FullName, "FSharpLint.Console", "FSharpLint.Console.fsproj") |> FileInfo - let sol = Path.Combine(rootDir.FullName, "FSharpLint.sln") |> FileInfo + let sol = Path.Combine(rootDir.FullName, solutionFileName) |> FileInfo exec "dotnet" $"run lint %s{sol.FullName}" consoleProj.Directory.FullName ) From 420329b778e7bd4abf4d811aaf7597484c93f7ac Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Wed, 2 Jul 2025 03:19:16 +0400 Subject: [PATCH 3/4] feat: infer `.slnx` as solution file type --- .../FSharpLint.Console.fsproj | 6 ++++++ src/FSharpLint.Console/Program.fs | 8 ++++---- tests/FSharpLint.Console.Tests/TestApp.fs | 20 +++++++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/FSharpLint.Console/FSharpLint.Console.fsproj b/src/FSharpLint.Console/FSharpLint.Console.fsproj index 028d418ad..5be5cdf16 100644 --- a/src/FSharpLint.Console/FSharpLint.Console.fsproj +++ b/src/FSharpLint.Console/FSharpLint.Console.fsproj @@ -15,6 +15,12 @@ Major + + + <_Parameter1>FSharpLint.Console.Tests + + + diff --git a/src/FSharpLint.Console/Program.fs b/src/FSharpLint.Console/Program.fs index 85174ca67..0a43819b9 100644 --- a/src/FSharpLint.Console/Program.fs +++ b/src/FSharpLint.Console/Program.fs @@ -3,9 +3,9 @@ open Argu open System open System.IO +open System.Reflection open FSharpLint.Framework open FSharpLint.Application -open System.Reflection /// Output format the linter will use. type private OutputFormat = @@ -13,7 +13,7 @@ type private OutputFormat = | MSBuild = 2 /// File type the linter is running against. -type private FileType = +type internal FileType = | Project = 1 | Solution = 2 | File = 3 @@ -60,12 +60,12 @@ let private parserProgress (output:Output.IOutput) = function |> output.WriteError /// Infers the file type of the target based on its file extension. -let private inferFileType (target:string) = +let internal inferFileType (target:string) = if target.EndsWith ".fs" || target.EndsWith ".fsx" then FileType.File else if target.EndsWith ".fsproj" then FileType.Project - else if target.EndsWith ".sln" then + else if target.EndsWith ".slnx" || target.EndsWith ".sln" then FileType.Solution else FileType.Source diff --git a/tests/FSharpLint.Console.Tests/TestApp.fs b/tests/FSharpLint.Console.Tests/TestApp.fs index 8b320fe2d..b229d241e 100644 --- a/tests/FSharpLint.Console.Tests/TestApp.fs +++ b/tests/FSharpLint.Console.Tests/TestApp.fs @@ -117,3 +117,23 @@ type TestConsoleApplication() = Assert.AreEqual(-1, returnCode) Assert.AreEqual(set ["Use prefix syntax for generic type."], errors) + +open FSharpLint.Console.Program + +[] +type TestFileTypeInference() = + + [] + [] + [] + [] + [] + [] + [] + [] + [] + [] + member _.``File type inference test cases``(filename: string, expectedType: int) = + let result = FSharpLint.Console.Program.inferFileType filename + let expectedType = enum(expectedType) + Assert.AreEqual(expectedType, result) From a830164462a071e64fa9140744219d8c259f56b4 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 6 Jul 2025 19:01:32 +0400 Subject: [PATCH 4/4] feat: infer `.slnf` as solution filter file type (technically also solution) --- CHANGELOG.md | 2 +- src/FSharpLint.Console/Program.fs | 2 +- tests/FSharpLint.Console.Tests/TestApp.fs | 4 +++- .../FSharpLint.FunctionalTest.TestedProject.slnf | 9 +++++++++ tests/FSharpLint.FunctionalTest/TestApi.fs | 2 ++ 5 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 tests/FSharpLint.FunctionalTest.TestedProject/FSharpLint.FunctionalTest.TestedProject.slnf diff --git a/CHANGELOG.md b/CHANGELOG.md index 98d972e8d..92d83cac6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Use string interpolation instead of `+` concatenation #724 [@xperiandri] - Run tests in parallel #728 [@xperiandri] - Remove `NoPartialFunctions` compiler workaround (#698) [@webwarrior-ws] -- Add SLNX support and migrate to SLNX solution #723 [@xperiandri]\ +- Add `SLNX` and `SLNF` format support and migrate to SLNX solution #723 [@xperiandri]\ Remove `Ionide.ProjInfo.Sln` NuGet package dependency ## [0.24.2] - 2024-02-29 diff --git a/src/FSharpLint.Console/Program.fs b/src/FSharpLint.Console/Program.fs index 0a43819b9..fac9a7418 100644 --- a/src/FSharpLint.Console/Program.fs +++ b/src/FSharpLint.Console/Program.fs @@ -65,7 +65,7 @@ let internal inferFileType (target:string) = FileType.File else if target.EndsWith ".fsproj" then FileType.Project - else if target.EndsWith ".slnx" || target.EndsWith ".sln" then + else if target.EndsWith ".slnx" || target.EndsWith ".slnf" || target.EndsWith ".sln" then FileType.Solution else FileType.Source diff --git a/tests/FSharpLint.Console.Tests/TestApp.fs b/tests/FSharpLint.Console.Tests/TestApp.fs index b229d241e..322a39cab 100644 --- a/tests/FSharpLint.Console.Tests/TestApp.fs +++ b/tests/FSharpLint.Console.Tests/TestApp.fs @@ -122,16 +122,18 @@ open FSharpLint.Console.Program [] type TestFileTypeInference() = - + [] [] [] [] [] + [] [] [] [] [] + [] [] member _.``File type inference test cases``(filename: string, expectedType: int) = let result = FSharpLint.Console.Program.inferFileType filename diff --git a/tests/FSharpLint.FunctionalTest.TestedProject/FSharpLint.FunctionalTest.TestedProject.slnf b/tests/FSharpLint.FunctionalTest.TestedProject/FSharpLint.FunctionalTest.TestedProject.slnf new file mode 100644 index 000000000..898667fe4 --- /dev/null +++ b/tests/FSharpLint.FunctionalTest.TestedProject/FSharpLint.FunctionalTest.TestedProject.slnf @@ -0,0 +1,9 @@ +{ + "solution": { + "path": "FSharpLint.FunctionalTest.TestedProject.slnx", + "projects": [ + "FSharpLint.FunctionalTest.TestedProject.MultiTarget\\FSharpLint.FunctionalTest.TestedProject.MultiTarget.fsproj", + "FSharpLint.FunctionalTest.TestedProject.NetCore\\FSharpLint.FunctionalTest.TestedProject.NetCore.fsproj" + ] + } +} \ No newline at end of file diff --git a/tests/FSharpLint.FunctionalTest/TestApi.fs b/tests/FSharpLint.FunctionalTest/TestApi.fs index 9add375e0..f42a6ffb2 100644 --- a/tests/FSharpLint.FunctionalTest/TestApi.fs +++ b/tests/FSharpLint.FunctionalTest/TestApi.fs @@ -104,6 +104,7 @@ module TestApi = [] [] + [] member _.``Lint solution via absolute path``(solutionFileName: string, expectedWarnings: int) = let projectPath = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" let solutionFile = projectPath solutionFileName @@ -119,6 +120,7 @@ module TestApi = #if NETCOREAPP [] [] + [] member _.``Lint solution via relative path``(solutionFileName: string, expectedWarnings: int) = let projectPath = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" let solutionFile = projectPath solutionFileName