From fec96025d42b925a2ec967cdd72339b40c5f56a4 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sun, 21 Apr 2024 17:21:54 -0500 Subject: [PATCH] Support traversal projects (#207) --- src/Ionide.ProjInfo/Library.fs | 289 ++++++++++++++-------- test/Ionide.ProjInfo.Tests/FileUtils.fs | 2 +- test/Ionide.ProjInfo.Tests/TestAssets.fs | 11 + test/Ionide.ProjInfo.Tests/Tests.fs | 31 ++- test/examples/traversal-project/dirs.proj | 5 + 5 files changed, 238 insertions(+), 100 deletions(-) create mode 100644 test/examples/traversal-project/dirs.proj diff --git a/src/Ionide.ProjInfo/Library.fs b/src/Ionide.ProjInfo/Library.fs index ad683f7b..89c5eca7 100644 --- a/src/Ionide.ProjInfo/Library.fs +++ b/src/Ionide.ProjInfo/Library.fs @@ -329,7 +329,10 @@ type BinaryLogGeneration = /// module ProjectLoader = - type LoadedProject = internal LoadedProject of ProjectInstance + type LoadedProject = + internal + | StandardProject of ProjectInstance + | TraversalProject of ProjectInstance let internal projectLoaderLogger = lazy (LogProvider.getLoggerByName "ProjectLoader") @@ -630,12 +633,22 @@ module ProjectLoader = let pi = project.CreateProjectInstance() - let build = pi.Build(designTimeBuildTargets isLegacyFrameworkProjFile, loggers) + let doDesignTimeBuild () = + let build = pi.Build(designTimeBuildTargets isLegacyFrameworkProjFile, loggers) + + if build then + Ok(StandardProject pi) + else + Error(sw.ToString()) + + let yieldTraversalProject () = Ok(TraversalProject pi) + + // do traversal project detection here + match pi.GetProperty "IsTraversal" with + | null -> doDesignTimeBuild () + | p when Boolean.Parse(p.EvaluatedValue) = false -> doDesignTimeBuild () + | _ -> yieldTraversalProject () - if build then - Ok(LoadedProject pi) - else - Error(sw.ToString()) with exc -> projectLoaderLogger.Value.error ( Log.setMessage "Generic error while loading project {path}" @@ -645,38 +658,67 @@ module ProjectLoader = Error(exc.Message) - let getFscArgs (LoadedProject project) = - project.Items + let getFscArgs (p: ProjectInstance) = + p.Items |> Seq.filter (fun p -> p.ItemType = "FscCommandLineArgs") |> Seq.map (fun p -> p.EvaluatedInclude) - let getCscArgs (LoadedProject project) = - project.Items + let getCscArgs (p: ProjectInstance) = + p.Items |> Seq.filter (fun p -> p.ItemType = "CscCommandLineArgs") |> Seq.map (fun p -> p.EvaluatedInclude) - let getP2PRefs (LoadedProject project) = - project.Items - |> Seq.filter (fun p -> p.ItemType = "_MSBuildProjectReferenceExistent") - |> Seq.map (fun p -> - let relativePath = p.EvaluatedInclude - let path = p.GetMetadataValue "FullPath" + let getP2PRefs (project) = + match project with + | TraversalProject p -> + let references = + p.Items + |> Seq.filter (fun p -> p.ItemType = "ProjectReference") + |> Seq.toList + + let mappedItems = ResizeArray() + + for item in references do + let relativePath = item.EvaluatedInclude + let fullPath = Path.GetFullPath(relativePath, item.Project.Directory) + + { + RelativePath = relativePath + ProjectFileName = fullPath + TargetFramework = "" + } + |> mappedItems.Add - let tfms = - if p.HasMetadata "TargetFramework" then - p.GetMetadataValue "TargetFramework" + Seq.toList mappedItems + + | StandardProject p -> + p.Items + |> Seq.choose (fun p -> + if + p.ItemType + <> "_MSBuildProjectReferenceExistent" + then + None else - p.GetMetadataValue "TargetFrameworks" + let relativePath = p.EvaluatedInclude + let path = p.GetMetadataValue "FullPath" - { - RelativePath = relativePath - ProjectFileName = path - TargetFramework = tfms - } - ) + let tfms = + if p.HasMetadata "TargetFramework" then + p.GetMetadataValue "TargetFramework" + else + p.GetMetadataValue "TargetFrameworks" - let getCompileItems (LoadedProject project) = - project.Items + Some { + RelativePath = relativePath + ProjectFileName = path + TargetFramework = tfms + } + ) + |> Seq.toList + + let getCompileItems (p: ProjectInstance) = + p.Items |> Seq.filter (fun p -> p.ItemType = "Compile") |> Seq.map (fun p -> let name = p.EvaluatedInclude @@ -696,8 +738,8 @@ module ProjectLoader = } ) - let getNuGetReferences (LoadedProject project) = - project.Items + let getNuGetReferences (p: ProjectInstance) = + p.Items |> Seq.filter (fun p -> p.ItemType = "Reference" && p.GetMetadataValue "NuGetSourceType" = "Package" @@ -714,8 +756,8 @@ module ProjectLoader = } ) - let getProperties (LoadedProject project) (properties: string list) = - project.Properties + let getProperties (p: ProjectInstance) (properties: string list) = + p.Properties |> Seq.filter (fun p -> List.contains p.Name properties) |> Seq.map (fun p -> { Name = p.Name @@ -888,57 +930,66 @@ module ProjectLoader = project + [] + type LoadedProjectInfo = + | StandardProjectInfo of ProjectOptions + | TraversalProjectInfo of ProjectReference list - let getLoadedProjectInfo (path: string) customProperties project = + let getLoadedProjectInfo (path: string) customProperties project : Result = // let (LoadedProject p) = project // let path = p.FullPath - let properties = [ - "OutputType" - "IsTestProject" - "TargetPath" - "Configuration" - "IsPackable" - "TargetFramework" - "TargetFrameworkIdentifier" - "TargetFrameworkVersion" - "MSBuildAllProjects" - "ProjectAssetsFile" - "RestoreSuccess" - "Configurations" - "TargetFrameworks" - "RunArguments" - "RunCommand" - "IsPublishable" - "BaseIntermediateOutputPath" - "IntermediateOutputPath" - "TargetPath" - "TargetRefPath" - "IsCrossTargetingBuild" - "TargetFrameworks" - ] - - let p2pRefs = getP2PRefs project - - let commandLineArgs = - if path.EndsWith ".fsproj" then - getFscArgs project - else - getCscArgs project + match project with + | LoadedProject.TraversalProject t -> + LoadedProjectInfo.TraversalProjectInfo(getP2PRefs project) + |> Ok + | LoadedProject.StandardProject p -> + let properties = [ + "OutputType" + "IsTestProject" + "TargetPath" + "Configuration" + "IsPackable" + "TargetFramework" + "TargetFrameworkIdentifier" + "TargetFrameworkVersion" + "MSBuildAllProjects" + "ProjectAssetsFile" + "RestoreSuccess" + "Configurations" + "TargetFrameworks" + "RunArguments" + "RunCommand" + "IsPublishable" + "BaseIntermediateOutputPath" + "IntermediateOutputPath" + "TargetPath" + "TargetRefPath" + "IsCrossTargetingBuild" + "TargetFrameworks" + ] - let compileItems = getCompileItems project - let nuGetRefs = getNuGetReferences project - let props = getProperties project properties - let sdkInfo = getSdkInfo props - let customProps = getProperties project customProperties + let p2pRefs = getP2PRefs project - if not sdkInfo.RestoreSuccess then - Result.Error "not restored" - else + let commandLineArgs = + if path.EndsWith ".fsproj" then + getFscArgs p + else if path.EndsWith ".csproj" then + getCscArgs p + else + Seq.empty - let proj = mapToProject path commandLineArgs p2pRefs compileItems nuGetRefs sdkInfo props customProps + let compileItems = getCompileItems p + let nuGetRefs = getNuGetReferences p + let props = getProperties p properties + let sdkInfo = getSdkInfo props + let customProps = getProperties p customProperties - Result.Ok proj + if not sdkInfo.RestoreSuccess then + Error "not restored" + else + let proj = mapToProject path commandLineArgs p2pRefs compileItems nuGetRefs sdkInfo props customProps + Ok(LoadedProjectInfo.StandardProjectInfo proj) /// A type that turns project files or solution files into deconstructed options. /// Use this in conjunction with the other ProjInfo libraries to turn these options into @@ -1056,7 +1107,19 @@ type WorkspaceLoaderViaProjectGraph private (toolsPath, ?globalProperties: (stri // and tell the graph to use all as potentially an entrypoint let nodes = g.ProjectNodes - |> Seq.map (fun pn -> ProjectGraphEntryPoint pn.ProjectInstance.FullPath) + |> Seq.choose (fun pn -> + match pn.ProjectInstance.GetProperty("IsTraversal") with + | null -> + ProjectGraphEntryPoint pn.ProjectInstance.FullPath + |> Some + | p -> + match bool.TryParse(p.EvaluatedValue) with + | true, true -> None + | true, false -> + ProjectGraphEntryPoint pn.ProjectInstance.FullPath + |> Some + | false, _ -> None + ) ProjectGraph(nodes, projectCollection = per_request_collection, projectInstanceFactory = projectInstanceFactory) @@ -1186,7 +1249,7 @@ type WorkspaceLoaderViaProjectGraph private (toolsPath, ?globalProperties: (stri let projects = builtProjects - |> Seq.map (fun p -> p.FullPath, ProjectLoader.getLoadedProjectInfo p.FullPath customProperties (ProjectLoader.LoadedProject p)) + |> Seq.map (fun p -> p.FullPath, ProjectLoader.getLoadedProjectInfo p.FullPath customProperties (ProjectLoader.StandardProject p)) |> Seq.choose (fun (projectPath, projectOptionResult) -> match projectOptionResult with @@ -1207,24 +1270,32 @@ type WorkspaceLoaderViaProjectGraph private (toolsPath, ?globalProperties: (stri projects |> Seq.toList - allProjectOptions - |> Seq.iter (fun po -> - logger.info ( - Log.setMessage "Project loaded {project}" - >> Log.addContextDestructured "project" po.ProjectFileName + let allStandardProjects = + allProjectOptions + |> List.choose ( + function + | ProjectLoader.LoadedProjectInfo.StandardProjectInfo p -> Some p + | _ -> None ) - loadingNotification.Trigger( - WorkspaceProjectState.Loaded( - po, - allProjectOptions - |> Seq.toList, - false + allProjectOptions + |> List.iter (fun po -> + match po with + | ProjectLoader.LoadedProjectInfo.TraversalProjectInfo p -> + logger.info ( + Log.setMessage "Traversal project loaded and contained the following references: {references}" + >> Log.addContextDestructured "references" p ) - ) + | ProjectLoader.LoadedProjectInfo.StandardProjectInfo po -> + logger.info ( + Log.setMessage "Project loaded {project}" + >> Log.addContextDestructured "project" po.ProjectFileName + ) + + loadingNotification.Trigger(WorkspaceProjectState.Loaded(po, allStandardProjects, false)) ) - allProjectOptions :> seq<_> + allStandardProjects :> seq<_> with e -> handleError "" e @@ -1297,12 +1368,16 @@ type WorkspaceLoader private (toolsPath: ToolsPath, ?globalProperties: (string * override __.Notifications = loadingNotification.Publish override __.LoadProjects(projects: string list, customProperties, binaryLogs) = - let cache = Dictionary() + let cache = Dictionary() use per_request_collection = projectCollection () let getAllKnown () = cache - |> Seq.map (fun n -> n.Value) + |> Seq.choose (fun (KeyValue(k, v)) -> + match v with + | ProjectLoader.LoadedProjectInfo.StandardProjectInfo p -> Some p + | _ -> None + ) |> Seq.toList let rec loadProject p = @@ -1328,8 +1403,13 @@ type WorkspaceLoader private (toolsPath: ToolsPath, ?globalProperties: (string * | Ok project -> cache.Add(p, project) + let referencedProjects = + match project with + | ProjectLoader.LoadedProjectInfo.StandardProjectInfo p -> p.ReferencedProjects + | ProjectLoader.LoadedProjectInfo.TraversalProjectInfo p -> p + let lst = - project.ReferencedProjects + referencedProjects |> Seq.choose (fun n -> if cache.ContainsKey n.ProjectFileName then None @@ -1338,7 +1418,11 @@ type WorkspaceLoader private (toolsPath: ToolsPath, ?globalProperties: (string * ) |> Seq.toList - let info = Some project + let info = + match project with + | ProjectLoader.LoadedProjectInfo.StandardProjectInfo p -> Some p + | ProjectLoader.LoadedProjectInfo.TraversalProjectInfo p -> None + lst, info | Error msg -> loadingNotification.Trigger(WorkspaceProjectState.Failed(p, GenericError(p, msg))) @@ -1349,10 +1433,19 @@ type WorkspaceLoader private (toolsPath: ToolsPath, ?globalProperties: (string * let newList, toTrigger = if cache.ContainsKey p then let project = cache.[p] - loadingNotification.Trigger(WorkspaceProjectState.Loaded(project, getAllKnown (), true)) //TODO: Should it even notify here? + + match project with + | ProjectLoader.LoadedProjectInfo.StandardProjectInfo p -> loadingNotification.Trigger(WorkspaceProjectState.Loaded(p, getAllKnown (), true)) + + | ProjectLoader.LoadedProjectInfo.TraversalProjectInfo p -> () + + let referencedProjects = + match project with + | ProjectLoader.LoadedProjectInfo.StandardProjectInfo p -> p.ReferencedProjects + | ProjectLoader.LoadedProjectInfo.TraversalProjectInfo p -> p let lst = - project.ReferencedProjects + referencedProjects |> Seq.choose (fun n -> if cache.ContainsKey n.ProjectFileName then None @@ -1369,13 +1462,13 @@ type WorkspaceLoader private (toolsPath: ToolsPath, ?globalProperties: (string * loadProjectList newList + toTrigger |> Option.iter (fun project -> loadingNotification.Trigger(WorkspaceProjectState.Loaded(project, getAllKnown (), false))) loadProjectList projects - cache - |> Seq.map (fun n -> n.Value) + getAllKnown () override this.LoadProjects(projects) = this.LoadProjects(projects, [], BinaryLogGeneration.Off) diff --git a/test/Ionide.ProjInfo.Tests/FileUtils.fs b/test/Ionide.ProjInfo.Tests/FileUtils.fs index 312fa4eb..c941fcb3 100644 --- a/test/Ionide.ProjInfo.Tests/FileUtils.fs +++ b/test/Ionide.ProjInfo.Tests/FileUtils.fs @@ -141,7 +141,7 @@ let createFile (logger: Logger) path setContent = setContent f () -let unzip (logger: Logger) file dir = +let unzip (logger: Logger) (file: string) (dir: string) = logger.debug ( eventX "unzip '{file}' to {directory}" >> setField "file" file diff --git a/test/Ionide.ProjInfo.Tests/TestAssets.fs b/test/Ionide.ProjInfo.Tests/TestAssets.fs index cdcfbaae..968260ef 100644 --- a/test/Ionide.ProjInfo.Tests/TestAssets.fs +++ b/test/Ionide.ProjInfo.Tests/TestAssets.fs @@ -310,3 +310,14 @@ let ``Console app with missing direct Import`` = { TargetFrameworks = Map.ofList [ "net6.0", sourceFiles [ "Program.fs" ] ] ProjectReferences = [] } + +let ``traversal project`` = { + ProjDir = "traversal-project" + AssemblyName = "" + ProjectFile = "dirs.proj" + TargetFrameworks = Map.empty + ProjectReferences = [ + yield ``sample3 Netsdk projs`` + yield! ``sample3 Netsdk projs``.ProjectReferences + ] +} diff --git a/test/Ionide.ProjInfo.Tests/Tests.fs b/test/Ionide.ProjInfo.Tests/Tests.fs index 24691586..4f9d3bc0 100644 --- a/test/Ionide.ProjInfo.Tests/Tests.fs +++ b/test/Ionide.ProjInfo.Tests/Tests.fs @@ -1770,7 +1770,8 @@ let testLoadProject toolsPath = | Result.Error err -> failwith $"{err}" | Result.Ok proj -> match ProjectLoader.getLoadedProjectInfo projPath [] proj with - | Result.Ok proj -> Expect.equal proj.ProjectFileName projPath "project file names" + | Ok(ProjectLoader.LoadedProjectInfo.StandardProjectInfo proj) -> Expect.equal proj.ProjectFileName projPath "project file names" + | Ok(ProjectLoader.LoadedProjectInfo.TraversalProjectInfo refs) -> failwith "expected standard project, not a traversal project" | Result.Error err -> failwith $"{err}" ) @@ -2225,6 +2226,31 @@ let canLoadMissingImports toolsPath loaderType (workspaceFactory: ToolsPath -> I Expect.stringEnds parsed.SourceFiles[2] "Program.fs" "Filename should be Program.fs" ) +let traversalProjectTest toolsPath loaderType workspaceFactory = + testCase + $"can crack traversal projects - {loaderType}" + (fun () -> + let logger = Log.create "Test 'can crack traversal projects'" + let fs = FileUtils(logger) + let projPath = pathForProject ``traversal project`` + // // need to build the projects first so that there's something to latch on to + // dotnet fs [ + // "build" + // projPath + // "-bl" + // ] + // |> checkExitCodeZero + + let loader: IWorkspaceLoader = workspaceFactory toolsPath + + let parsed = + loader.LoadProjects [ projPath ] + |> Seq.toList + + Expect.hasLength parsed 3 "Should have loaded the 3 referenced projects from the traversal project" + + ) + let tests toolsPath = let testSample3WorkspaceLoaderExpected = [ ExpectNotification.loading "c1.fsproj" @@ -2348,4 +2374,7 @@ let tests toolsPath = // tests that cover our ability to handle missing imports canLoadMissingImports toolsPath (nameof (WorkspaceLoader)) WorkspaceLoader.Create canLoadMissingImports toolsPath (nameof (WorkspaceLoaderViaProjectGraph)) WorkspaceLoaderViaProjectGraph.Create + + traversalProjectTest toolsPath (nameof (WorkspaceLoader)) WorkspaceLoader.Create + traversalProjectTest toolsPath (nameof (WorkspaceLoaderViaProjectGraph)) WorkspaceLoaderViaProjectGraph.Create ] diff --git a/test/examples/traversal-project/dirs.proj b/test/examples/traversal-project/dirs.proj new file mode 100644 index 00000000..6aa2f3a8 --- /dev/null +++ b/test/examples/traversal-project/dirs.proj @@ -0,0 +1,5 @@ + + + + +