From bc7a69b207fafef1f2994e87267df0c3dfdf9ae2 Mon Sep 17 00:00:00 2001 From: Joery Droppers Date: Tue, 5 Oct 2021 23:47:31 +0200 Subject: [PATCH] Initial commit --- .editorconfig | 60 + .gitignore | 397 ++++ .gitmodules | 3 + LICENSE | 21 + README.md | 218 +++ RefScout.sln | 132 ++ RefScout.sln.DotSettings | 23 + art/logo.svg | 182 ++ art/screenshot.png | Bin 0 -> 90728 bytes build.cake | 191 ++ src/RefScout.Analyzer/AnalyzeRuntime.cs | 9 + .../Analyzers/Assemblies/IAssemblyAnalyzer.cs | 9 + .../ReferencedAssembliesAnalyzer.cs | 58 + .../UnreferencedAssembliesAnalyzer.cs | 87 + .../Compatibility/CompatibilityAnalyzer.cs | 57 + .../CompatibilityAnalyzerFactory.cs | 19 + .../CoreCompatibilityAnalyzer.cs | 15 + .../Compatibility/DefaultVersionComparer.cs | 63 + .../Compatibility/ICompatibilityAnalyzer.cs | 6 + .../ICompatibilityAnalyzerFactory.cs | 8 + .../Compatibility/IVersionComparer.cs | 22 + .../SharedFrameworkCompatibilityAnalyzer.cs | 56 + .../Analyzers/Environment/Core/CoreRuntime.cs | 52 + .../Environment/Core/CoreRuntimeAnalyzer.cs | 274 +++ .../Core/CoreRuntimeAnalyzerResult.cs | 5 + .../Environment/Core/ICoreRuntimeAnalyzer.cs | 21 + .../Analyzers/Environment/Core/RuntimePack.cs | 9 + .../Environment/EnvironmentAnalyzer.cs | 52 + .../Analyzers/Environment/EnvironmentInfo.cs | 10 + .../Environment/Framework/FrameworkRuntime.cs | 7 + .../Framework/FrameworkRuntimeAnalyzer.cs | 171 ++ .../FrameworkRuntimeAnalyzerResult.cs | 5 + .../Framework/IFrameworkRuntimeAnalyzer.cs | 8 + .../Environment/IEnvironmentAnalyzer.cs | 14 + .../Analyzers/Environment/IRuntimeAnalyzer.cs | 6 + .../Environment/Mono/IMonoRuntimeAnalyzer.cs | 10 + .../Environment/Mono/MonoRuntimeAnalyzer.cs | 147 ++ src/RefScout.Analyzer/Assembly.cs | 118 ++ src/RefScout.Analyzer/AssemblyIdentity.cs | 26 + src/RefScout.Analyzer/AssemblyRef.cs | 41 + src/RefScout.Analyzer/Config/ConfigError.cs | 3 + .../Config/Core/CoreConfig.cs | 23 + .../Config/Core/CoreConfigErrorReport.cs | 11 + .../Config/Core/CoreConfigParser.cs | 76 + src/RefScout.Analyzer/Config/Core/DepsFile.cs | 23 + .../Config/Core/RuntimeConfig.cs | 36 + .../Config/Framework/BindingIdentity.cs | 6 + .../Config/Framework/BindingRedirect.cs | 9 + .../Config/Framework/CodeBase.cs | 5 + .../Config/Framework/FameworkConfigError.cs | 4 + .../Config/Framework/FrameworkConfig.cs | 13 + .../Framework/FrameworkConfigErrorReport.cs | 63 + .../Config/Framework/FrameworkConfigParser.cs | 286 +++ src/RefScout.Analyzer/Config/IConfig.cs | 6 + .../Config/IConfigErrorReport.cs | 9 + src/RefScout.Analyzer/Config/IConfigParser.cs | 6 + .../Config/IConfigParserFactory.cs | 36 + src/RefScout.Analyzer/Context/Context.cs | 118 ++ .../Context/ContextFactory.cs | 127 ++ src/RefScout.Analyzer/Context/CoreContext.cs | 60 + .../Context/FrameworkContext.cs | 36 + src/RefScout.Analyzer/Context/IContext.cs | 24 + .../Context/IContextFactory.cs | 20 + src/RefScout.Analyzer/Context/IContext{T}.cs | 9 + src/RefScout.Analyzer/Context/ICoreContext.cs | 9 + .../Context/IFrameworkContext.cs | 10 + src/RefScout.Analyzer/Context/IMonoContext.cs | 8 + .../Context/ISharedFrameworkContext.cs | 14 + src/RefScout.Analyzer/Context/MonoContext.cs | 32 + .../Context/SharedFrameworkContext.cs | 136 ++ src/RefScout.Analyzer/Filter/FilterParser.cs | 123 ++ .../Filter/PredicateBuilder.cs | 40 + .../Filter/StringSplitEnumerator.cs | 53 + .../Helpers/AssemblyHelper.cs | 38 + .../Helpers/BinaryKmpSearch.cs | 86 + .../Helpers/EnvironmentWrapper.cs | 11 + src/RefScout.Analyzer/Helpers/Extensions.cs | 8 + src/RefScout.Analyzer/Helpers/IEnvironment.cs | 15 + .../Helpers/MicrosoftHelper.cs | 728 +++++++ .../Helpers/VersionJsonConverter.cs | 34 + src/RefScout.Analyzer/IAnalyzer.cs | 34 + src/RefScout.Analyzer/IAnalyzerResult.cs | 10 + .../MicrosoftAssemblies.json | 713 +++++++ src/RefScout.Analyzer/Notes/ConflictNote.cs | 6 + src/RefScout.Analyzer/Notes/INoteGenerator.cs | 8 + .../Core/CoreMissingRuntimeMessage.cs | 54 + .../Core/CoreVersionMismatchWarningMessage.cs | 33 + .../Framework/CoreMissingRuntimeMessage.cs | 33 + .../Notes/Messages/IMessage.cs | 9 + .../Notes/Messages/Message.cs | 21 + .../Notes/Messages/Message{T}.cs | 42 + .../Mono/CoreMissingRuntimeMessage.cs | 26 + .../Shared/ArchitectureMismatchMessage.cs | 13 + .../Shared/ConfigParseErrorMessage.cs | 15 + .../Notes/Messages/Shared/ErrorMessage.cs | 15 + .../Messages/Shared/LoadNotFoundMessage.cs | 19 + .../Messages/Shared/UnreferencedMessage.cs | 13 + .../Messages/Shared/VersionMismatchMessage.cs | 35 + .../FrameworkVersionMismatchFatalMessage.cs | 38 + .../FrameworkVersionMismatchWarningMessage.cs | 35 + .../SharedFramework/RedirectFailedMessage.cs | 30 + .../RedirectFailedWrongVersionMessage.cs | 33 + .../SharedFramework/RedirectSuccessMessage.cs | 23 + .../SharedFramework/UnificationMessage.cs | 13 + src/RefScout.Analyzer/Notes/NoteGenerator.cs | 70 + src/RefScout.Analyzer/Notes/NoteLevel.cs | 11 + src/RefScout.Analyzer/Notes/NoteType.cs | 27 + .../Properties/AssemblyInfo.cs | 6 + src/RefScout.Analyzer/PublicKeyToken.cs | 145 ++ .../Readers/AssemblyReader.cs | 99 + .../Readers/Cecil/CecilAssemblyReader.cs | 20 + .../Readers/Cecil/CecilMetadataReader.cs | 190 ++ .../Readers/Cecil/GeneratedNameHelper.cs | 107 + .../Readers/Cecil/LanguageDetector.cs | 117 ++ .../Readers/IAssemblyReader.cs | 12 + .../Readers/IMetadataReader.cs | 13 + .../RefScout.Analyzer.csproj | 27 + src/RefScout.Analyzer/ReferenceAnalyzer.cs | 323 +++ .../Resolvers/CoreResolver.cs | 63 + .../Resolvers/FrameworkResolver.cs | 41 + src/RefScout.Analyzer/Resolvers/IResolver.cs | 18 + .../Resolvers/IResolverFactory.cs | 24 + .../Resolvers/MonoResolver.cs | 33 + src/RefScout.Analyzer/Resolvers/Resolver.cs | 92 + .../Resolvers/ResolverFactory.cs | 99 + .../Core/CoreNuGetPackageResolverStrategy.cs | 153 ++ .../Core/CoreSharedResolverStrategy.cs | 56 + .../Framework/CorLibResolverStrategy.cs | 86 + .../FileSystemGacResolverStrategy.cs | 73 + .../FrameworkProxyGacResolverStrategy.cs | 130 ++ .../Framework/FusionGacResolverStrategy.cs | 220 +++ .../Framework/SilverlightResolverStrategy.cs | 60 + .../WindowsMetadataResolverStrategy.cs | 78 + .../Resolvers/Strategies/IResolverStrategy.cs | 12 + .../Mono/MonoCorLibResolverStrategy.cs | 63 + .../Mono/MonoGacResolverStrategy.cs | 41 + .../Mono/MonoRuntimeResolverStrategy.cs | 44 + .../Resolvers/Strategies/ResolverHelper.cs | 76 + .../Shared/DirectoryResolverStrategy.cs | 28 + src/RefScout.Analyzer/TargetFramework.cs | 108 ++ .../ConsoleConflictVisualizerCommand.cs | 23 + .../Commands/DotConflictVisualizerCommand.cs | 26 + .../Commands/IVisualizerCommand.cs | 15 + .../Commands/VisualizerCommand.cs | 31 + src/RefScout.Cli/Program.cs | 134 ++ src/RefScout.Cli/RefScout.Cli.csproj | 39 + src/RefScout.Cli/Resources/Icon.ico | Bin 0 -> 381534 bytes src/RefScout.Cli/TrimmerRoots.xml | 5 + src/RefScout.Core/Constants.cs | 6 + .../Extensions/ListExtensions.cs | 20 + src/RefScout.Core/Helpers/ConsoleHelper.cs | 28 + src/RefScout.Core/Helpers/ResourceHelper.cs | 33 + src/RefScout.Core/Logging/ConsoleLogger.cs | 27 + src/RefScout.Core/Logging/ILogger.cs | 14 + src/RefScout.Core/Logging/LogEntry.cs | 5 + src/RefScout.Core/Logging/Logger.cs | 81 + src/RefScout.Core/RefScout.Core.csproj | 13 + src/RefScout.Ipc.FrameworkRuntime/Program.cs | 40 + .../RefScout.Ipc.FrameworkRuntime.csproj | 26 + src/RefScout.Ipc/Client/IIpcClient.cs | 16 + src/RefScout.Ipc/Client/IpcClient.cs | 76 + src/RefScout.Ipc/Client/PipeIpcClient.cs | 55 + src/RefScout.Ipc/Client/TcpIpcClient.cs | 60 + src/RefScout.Ipc/IsExternalInit.cs | 6 + src/RefScout.Ipc/RefScout.Ipc.csproj | 14 + src/RefScout.Ipc/Server/IIpcRequest.cs | 10 + src/RefScout.Ipc/Server/IIpcServer.cs | 11 + src/RefScout.Ipc/Server/PipeIpcRequest.cs | 13 + src/RefScout.Ipc/Server/PipeIpcServer.cs | 57 + src/RefScout.Ipc/Server/TcpIpcRequest.cs | 15 + src/RefScout.Ipc/Server/TcpIpcServer.cs | 56 + .../AssemblyTreeFilter.cs | 167 ++ .../Console/ConsoleVisualizer.cs | 251 +++ .../Dgml/DgmlConflictVisualizer.cs | 177 ++ src/RefScout.Visualizers/Dgml/DgmlStyles.cs | 190 ++ src/RefScout.Visualizers/Dgml/XmlHelper.cs | 23 + .../Dot/Compiler/DotEdge.cs | 19 + .../Dot/Compiler/DotElement.cs | 46 + .../Dot/Compiler/DotGraph.cs | 47 + .../Dot/Compiler/DotHelpers.cs | 22 + .../Dot/Compiler/DotNode.cs | 15 + .../Dot/Compiler/IDotCompilable.cs | 6 + .../Dot/DotConflictVisualizer.cs | 198 ++ src/RefScout.Visualizers/Dot/DotThemes.cs | 83 + .../IVisualizerOptions.cs | 3 + .../RefScout.Visualizers.csproj | 19 + src/RefScout.Visualizers/Visualizer.cs | 78 + src/RefScout.Wpf/App.xaml | 30 + src/RefScout.Wpf/App.xaml.cs | 75 + src/RefScout.Wpf/AppSettings.cs | 96 + src/RefScout.Wpf/AssemblyInfo.cs | 9 + ...ullOrEmptyCollectionVisibilityConverter.cs | 24 + ...ullOrEmptyCollectionVisibilityConverter.cs | 24 + .../Converters/NullVisibilityConverter.cs | 15 + .../SourceLanguageToTextConverter.cs | 29 + src/RefScout.Wpf/Helpers/GraphVizHelper.cs | 152 ++ src/RefScout.Wpf/Helpers/ICloseable.cs | 6 + .../Helpers/ImageSvgConverterEx.cs | 801 ++++++++ src/RefScout.Wpf/Helpers/ProcessHelper.cs | 60 + src/RefScout.Wpf/Images/ReferenceIcon.png | Bin 0 -> 214 bytes src/RefScout.Wpf/Models/BindingProxy.cs | 17 + src/RefScout.Wpf/Models/ComboBoxEntry.cs | 6 + src/RefScout.Wpf/Models/FrameworkGroup.cs | 17 + src/RefScout.Wpf/Models/LanguageGroup.cs | 17 + src/RefScout.Wpf/RefScout.Wpf.csproj | 30 + src/RefScout.Wpf/Resources/Icon.ico | Bin 0 -> 381534 bytes src/RefScout.Wpf/Services/ContextService.cs | 43 + src/RefScout.Wpf/Services/IContextService.cs | 10 + src/RefScout.Wpf/Services/ILoggingService.cs | 13 + src/RefScout.Wpf/Services/ISettingsService.cs | 10 + src/RefScout.Wpf/Services/LoggingService.cs | 65 + src/RefScout.Wpf/Services/SettingsService.cs | 73 + src/RefScout.Wpf/Styles/All.xaml | 23 + src/RefScout.Wpf/Styles/Brushes.Dark.xaml | 191 ++ src/RefScout.Wpf/Styles/Brushes.Light.xaml | 186 ++ src/RefScout.Wpf/Styles/Buttons.xaml | 251 +++ src/RefScout.Wpf/Styles/ComboBox.xaml | 224 +++ src/RefScout.Wpf/Styles/Icons.xaml | 111 ++ src/RefScout.Wpf/Styles/Misc.xaml | 93 + src/RefScout.Wpf/Styles/ScrollBar.xaml | 117 ++ src/RefScout.Wpf/Styles/TabControl.xaml | 286 +++ src/RefScout.Wpf/Styles/TreeView.xaml | 164 ++ src/RefScout.Wpf/Styles/Window.xaml | 7 + src/RefScout.Wpf/Themes/Dark.xaml | 7 + src/RefScout.Wpf/Themes/Generic.xaml | 3 + src/RefScout.Wpf/Themes/Light.xaml | 7 + .../ViewModels/AssemblyListTabViewModel.cs | 97 + .../ViewModels/DetailsWindowViewModel.cs | 30 + .../ViewModels/EnvironmentTabViewModel.cs | 128 ++ .../ViewModels/GraphContainerViewModel.cs | 263 +++ .../ViewModels/LoggingWindowViewModel.cs | 27 + .../ViewModels/MainWindowViewModel.cs | 159 ++ .../ViewModels/SettingsWindowViewModel.cs | 84 + .../ViewModels/TechnologiesTabViewModel.cs | 69 + .../ViewModels/ViewModelLocator.cs | 30 + src/RefScout.Wpf/Views/AssemblyListTab.xaml | 138 ++ .../Views/AssemblyListTab.xaml.cs | 36 + .../Views/Controls/AssemblyView.xaml | 154 ++ .../Views/Controls/AssemblyView.xaml.cs | 35 + .../Views/Controls/GraphViewer.xaml | 67 + .../Views/Controls/GraphViewer.xaml.cs | 285 +++ src/RefScout.Wpf/Views/Controls/Icon.xaml | 48 + src/RefScout.Wpf/Views/Controls/Icon.xaml.cs | 36 + .../Views/Controls/IconButton.xaml | 22 + .../Views/Controls/IconButton.xaml.cs | 77 + .../Views/Controls/ReferenceList.xaml | 141 ++ .../Views/Controls/ReferenceList.xaml.cs | 39 + src/RefScout.Wpf/Views/Controls/Spinner.xaml | 138 ++ .../Views/Controls/Spinner.xaml.cs | 11 + src/RefScout.Wpf/Views/DetailsWindow.xaml | 365 ++++ src/RefScout.Wpf/Views/DetailsWindow.xaml.cs | 16 + src/RefScout.Wpf/Views/EnvironmentTab.xaml | 46 + src/RefScout.Wpf/Views/EnvironmentTab.xaml.cs | 33 + src/RefScout.Wpf/Views/GraphContainer.xaml | 171 ++ src/RefScout.Wpf/Views/GraphContainer.xaml.cs | 54 + src/RefScout.Wpf/Views/LoggingWindow.xaml | 81 + src/RefScout.Wpf/Views/LoggingWindow.xaml.cs | 11 + src/RefScout.Wpf/Views/MainWindow.xaml | 257 +++ src/RefScout.Wpf/Views/MainWindow.xaml.cs | 49 + src/RefScout.Wpf/Views/SettingsWindow.xaml | 57 + src/RefScout.Wpf/Views/SettingsWindow.xaml.cs | 12 + src/RefScout.Wpf/Views/TechnologiesTab.xaml | 170 ++ .../Views/TechnologiesTab.xaml.cs | 33 + src/RefScout.Wpf/Views/TreeTab.xaml | 37 + src/RefScout.Wpf/Views/TreeTab.xaml.cs | 43 + .../RefScout.Analyzer.FunctionalTests.csproj | 27 + .../ReferenceAnalyzerTests.cs | 81 + .../ReferencedAssembliesAnalyzerTests.cs | 113 ++ .../UnreferencedAssembliesAnalyzerTests.cs | 34 + .../CompatibilityAnalyzerTests.cs | 69 + .../DefaultVersionComparerTests.cs | 105 + .../FrameworkCompatibilityAnalyzerTests.cs | 73 + .../Core/CoreRuntimeAnalyzerTests.cs | 75 + .../Environment/Core/CoreRuntimeTests.cs | 37 + .../Environment/EnvironmentAnalyzerTests.cs | 48 + .../Mono/MonoRuntimeAnalyzerTests.cs | 39 + tests/RefScout.Analyzer.Tests/AssHelp.cs | 72 + .../AssemblyRefTests.cs | 23 + .../RefScout.Analyzer.Tests/AssemblyTests.cs | 60 + .../Config/ConfigParserFactoryTests.cs | 31 + .../Config/Core/CoreConfigParserTests.cs | 52 + .../Framework/FrameworkConfigParserTests.cs | 89 + .../Context/FrameworkContextTests.cs | 191 ++ .../FakeEnvironment.cs | 32 + .../Fakes/FakeAssemblyReader.cs | 17 + .../Fakes/FakeContext.cs | 29 + .../Fakes/FakeMonoContext.cs | 28 + .../Fakes/FakeResolver.cs | 20 + .../Filter/StringSplitEnumeratorTests.cs | 37 + .../Helpers/AssemblyHelperTests.cs | 65 + .../Helpers/EnvironmentWrapperTests.cs | 40 + .../Notes/ConflictNoteTests.cs | 14 + .../Core/CoreMissingRuntimeMessageTests.cs | 108 ++ .../CoreVersionMismatchWarningMessageTests.cs | 93 + .../Messages/Core/FakeCoreMessageContext.cs | 43 + .../CoreMissingRuntimeMessageTests.cs | 83 + .../Notes/Messages/Message{T}Tests.cs | 126 ++ .../Mono/CoreMissingRuntimeMessageTests.cs | 54 + .../ArchitectureMismatchMessageTests.cs | 49 + .../Shared/ConfigParseErrorMessageTests.cs | 83 + .../Messages/Shared/ErrorMessageTests.cs | 47 + .../Messages/Shared/NotFoundMessageTests.cs | 62 + .../Shared/UnreferencedMessageTests.cs | 49 + .../Shared/VersionMismatchMessageTests.cs | 76 + .../FakeFrameworkMessageContext.cs | 51 + ...ameworkVersionMismatchFatalMessageTests.cs | 103 + ...eworkVersionMismatchWarningMessageTests.cs | 93 + .../RedirectFailedMessageTests.cs | 76 + .../RedirectFailedWrongVersionMessageTests.cs | 76 + .../RedirectSuccessMessageTests.cs | 76 + .../UnificationMessageTests.cs | 48 + .../Notes/NoteGeneratorTests.cs | 36 + .../PublicKeyTokenTests.cs | 30 + .../Readers/AssemblyReaderTests.cs | 112 ++ .../Readers/Cecil/CecilAssemblyReaderTests.cs | 34 + .../Readers/Cecil/CecilMetadataReaderTests.cs | 121 ++ .../RefScout.Analyzer.Tests.csproj | 49 + .../CoreNuGetPackageResolverStrategyTests.cs | 97 + .../Core/CoreSharedResolverStrategyTests.cs | 61 + .../Resolvers/Strategies/FakeFileSystem.cs | 69 + .../Framework/CorLibResolverStrategyTests.cs | 70 + .../FileSystemGacResolverStrategyTests.cs | 50 + .../FrameworkProxyGacResolverStrategyTests.cs | 82 + .../FusionGacResolverStrategyTests.cs | 73 + .../SilverlightResolverStrategyTests.cs | 55 + .../WindowsMetadataResolverStrategyTests.cs | 60 + .../Mono/FakeMonoRuntimeAnalyzer.cs | 20 + .../Mono/MonoCorLibResolverStrategyTests.cs | 53 + .../Mono/MonoGacResolverStrategyTests.cs | 50 + .../Mono/MonoRuntimeResolverStrategyTests.cs | 53 + .../Shared/DirectoryResolverStrategyTests.cs | 38 + .../TargetFrameworkTests.cs | 51 + .../Testfiles/Assemblies/Managed.dll | Bin 0 -> 4608 bytes .../Testfiles/Assemblies/ShareX.exe | Bin 0 -> 2058240 bytes .../Assemblies/SharpVectors.Runtime.Wpf.dll | Bin 0 -> 73216 bytes .../Testfiles/Assemblies/System.Data.dll | Bin 0 -> 3543416 bytes .../Assemblies/System.Runtime_linux.dll | Bin 0 -> 34304 bytes .../Testfiles/Configs/Bindings.config | 186 ++ .../Testfiles/Configs/Broken.config | 185 ++ .../Testfiles/Configs/HasEverything.config | 1725 +++++++++++++++++ .../Testfiles/Configs/NoBindings.config | 69 + .../CoreConfigs/Invalid.runtimeconfig.json | 14 + .../Testfiles/CoreConfigs/Test.deps.json | 279 +++ .../CoreConfigs/Test.runtimeconfig.json | 13 + .../Testfiles/Mono/resgen.exe | Bin 0 -> 76288 bytes tests/TestData | 1 + 346 files changed, 24775 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 LICENSE create mode 100644 README.md create mode 100644 RefScout.sln create mode 100644 RefScout.sln.DotSettings create mode 100644 art/logo.svg create mode 100644 art/screenshot.png create mode 100644 build.cake create mode 100644 src/RefScout.Analyzer/AnalyzeRuntime.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Assemblies/IAssemblyAnalyzer.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Assemblies/ReferencedAssembliesAnalyzer.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Assemblies/UnreferencedAssembliesAnalyzer.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Compatibility/CompatibilityAnalyzer.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Compatibility/CompatibilityAnalyzerFactory.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Compatibility/CoreCompatibilityAnalyzer.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Compatibility/DefaultVersionComparer.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Compatibility/ICompatibilityAnalyzer.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Compatibility/ICompatibilityAnalyzerFactory.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Compatibility/IVersionComparer.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Compatibility/SharedFrameworkCompatibilityAnalyzer.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Environment/Core/CoreRuntime.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Environment/Core/CoreRuntimeAnalyzer.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Environment/Core/CoreRuntimeAnalyzerResult.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Environment/Core/ICoreRuntimeAnalyzer.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Environment/Core/RuntimePack.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Environment/EnvironmentAnalyzer.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Environment/EnvironmentInfo.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Environment/Framework/FrameworkRuntime.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Environment/Framework/FrameworkRuntimeAnalyzer.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Environment/Framework/FrameworkRuntimeAnalyzerResult.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Environment/Framework/IFrameworkRuntimeAnalyzer.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Environment/IEnvironmentAnalyzer.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Environment/IRuntimeAnalyzer.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Environment/Mono/IMonoRuntimeAnalyzer.cs create mode 100644 src/RefScout.Analyzer/Analyzers/Environment/Mono/MonoRuntimeAnalyzer.cs create mode 100644 src/RefScout.Analyzer/Assembly.cs create mode 100644 src/RefScout.Analyzer/AssemblyIdentity.cs create mode 100644 src/RefScout.Analyzer/AssemblyRef.cs create mode 100644 src/RefScout.Analyzer/Config/ConfigError.cs create mode 100644 src/RefScout.Analyzer/Config/Core/CoreConfig.cs create mode 100644 src/RefScout.Analyzer/Config/Core/CoreConfigErrorReport.cs create mode 100644 src/RefScout.Analyzer/Config/Core/CoreConfigParser.cs create mode 100644 src/RefScout.Analyzer/Config/Core/DepsFile.cs create mode 100644 src/RefScout.Analyzer/Config/Core/RuntimeConfig.cs create mode 100644 src/RefScout.Analyzer/Config/Framework/BindingIdentity.cs create mode 100644 src/RefScout.Analyzer/Config/Framework/BindingRedirect.cs create mode 100644 src/RefScout.Analyzer/Config/Framework/CodeBase.cs create mode 100644 src/RefScout.Analyzer/Config/Framework/FameworkConfigError.cs create mode 100644 src/RefScout.Analyzer/Config/Framework/FrameworkConfig.cs create mode 100644 src/RefScout.Analyzer/Config/Framework/FrameworkConfigErrorReport.cs create mode 100644 src/RefScout.Analyzer/Config/Framework/FrameworkConfigParser.cs create mode 100644 src/RefScout.Analyzer/Config/IConfig.cs create mode 100644 src/RefScout.Analyzer/Config/IConfigErrorReport.cs create mode 100644 src/RefScout.Analyzer/Config/IConfigParser.cs create mode 100644 src/RefScout.Analyzer/Config/IConfigParserFactory.cs create mode 100644 src/RefScout.Analyzer/Context/Context.cs create mode 100644 src/RefScout.Analyzer/Context/ContextFactory.cs create mode 100644 src/RefScout.Analyzer/Context/CoreContext.cs create mode 100644 src/RefScout.Analyzer/Context/FrameworkContext.cs create mode 100644 src/RefScout.Analyzer/Context/IContext.cs create mode 100644 src/RefScout.Analyzer/Context/IContextFactory.cs create mode 100644 src/RefScout.Analyzer/Context/IContext{T}.cs create mode 100644 src/RefScout.Analyzer/Context/ICoreContext.cs create mode 100644 src/RefScout.Analyzer/Context/IFrameworkContext.cs create mode 100644 src/RefScout.Analyzer/Context/IMonoContext.cs create mode 100644 src/RefScout.Analyzer/Context/ISharedFrameworkContext.cs create mode 100644 src/RefScout.Analyzer/Context/MonoContext.cs create mode 100644 src/RefScout.Analyzer/Context/SharedFrameworkContext.cs create mode 100644 src/RefScout.Analyzer/Filter/FilterParser.cs create mode 100644 src/RefScout.Analyzer/Filter/PredicateBuilder.cs create mode 100644 src/RefScout.Analyzer/Filter/StringSplitEnumerator.cs create mode 100644 src/RefScout.Analyzer/Helpers/AssemblyHelper.cs create mode 100644 src/RefScout.Analyzer/Helpers/BinaryKmpSearch.cs create mode 100644 src/RefScout.Analyzer/Helpers/EnvironmentWrapper.cs create mode 100644 src/RefScout.Analyzer/Helpers/Extensions.cs create mode 100644 src/RefScout.Analyzer/Helpers/IEnvironment.cs create mode 100644 src/RefScout.Analyzer/Helpers/MicrosoftHelper.cs create mode 100644 src/RefScout.Analyzer/Helpers/VersionJsonConverter.cs create mode 100644 src/RefScout.Analyzer/IAnalyzer.cs create mode 100644 src/RefScout.Analyzer/IAnalyzerResult.cs create mode 100644 src/RefScout.Analyzer/MicrosoftAssemblies.json create mode 100644 src/RefScout.Analyzer/Notes/ConflictNote.cs create mode 100644 src/RefScout.Analyzer/Notes/INoteGenerator.cs create mode 100644 src/RefScout.Analyzer/Notes/Messages/Core/CoreMissingRuntimeMessage.cs create mode 100644 src/RefScout.Analyzer/Notes/Messages/Core/CoreVersionMismatchWarningMessage.cs create mode 100644 src/RefScout.Analyzer/Notes/Messages/Framework/CoreMissingRuntimeMessage.cs create mode 100644 src/RefScout.Analyzer/Notes/Messages/IMessage.cs create mode 100644 src/RefScout.Analyzer/Notes/Messages/Message.cs create mode 100644 src/RefScout.Analyzer/Notes/Messages/Message{T}.cs create mode 100644 src/RefScout.Analyzer/Notes/Messages/Mono/CoreMissingRuntimeMessage.cs create mode 100644 src/RefScout.Analyzer/Notes/Messages/Shared/ArchitectureMismatchMessage.cs create mode 100644 src/RefScout.Analyzer/Notes/Messages/Shared/ConfigParseErrorMessage.cs create mode 100644 src/RefScout.Analyzer/Notes/Messages/Shared/ErrorMessage.cs create mode 100644 src/RefScout.Analyzer/Notes/Messages/Shared/LoadNotFoundMessage.cs create mode 100644 src/RefScout.Analyzer/Notes/Messages/Shared/UnreferencedMessage.cs create mode 100644 src/RefScout.Analyzer/Notes/Messages/Shared/VersionMismatchMessage.cs create mode 100644 src/RefScout.Analyzer/Notes/Messages/SharedFramework/FrameworkVersionMismatchFatalMessage.cs create mode 100644 src/RefScout.Analyzer/Notes/Messages/SharedFramework/FrameworkVersionMismatchWarningMessage.cs create mode 100644 src/RefScout.Analyzer/Notes/Messages/SharedFramework/RedirectFailedMessage.cs create mode 100644 src/RefScout.Analyzer/Notes/Messages/SharedFramework/RedirectFailedWrongVersionMessage.cs create mode 100644 src/RefScout.Analyzer/Notes/Messages/SharedFramework/RedirectSuccessMessage.cs create mode 100644 src/RefScout.Analyzer/Notes/Messages/SharedFramework/UnificationMessage.cs create mode 100644 src/RefScout.Analyzer/Notes/NoteGenerator.cs create mode 100644 src/RefScout.Analyzer/Notes/NoteLevel.cs create mode 100644 src/RefScout.Analyzer/Notes/NoteType.cs create mode 100644 src/RefScout.Analyzer/Properties/AssemblyInfo.cs create mode 100644 src/RefScout.Analyzer/PublicKeyToken.cs create mode 100644 src/RefScout.Analyzer/Readers/AssemblyReader.cs create mode 100644 src/RefScout.Analyzer/Readers/Cecil/CecilAssemblyReader.cs create mode 100644 src/RefScout.Analyzer/Readers/Cecil/CecilMetadataReader.cs create mode 100644 src/RefScout.Analyzer/Readers/Cecil/GeneratedNameHelper.cs create mode 100644 src/RefScout.Analyzer/Readers/Cecil/LanguageDetector.cs create mode 100644 src/RefScout.Analyzer/Readers/IAssemblyReader.cs create mode 100644 src/RefScout.Analyzer/Readers/IMetadataReader.cs create mode 100644 src/RefScout.Analyzer/RefScout.Analyzer.csproj create mode 100644 src/RefScout.Analyzer/ReferenceAnalyzer.cs create mode 100644 src/RefScout.Analyzer/Resolvers/CoreResolver.cs create mode 100644 src/RefScout.Analyzer/Resolvers/FrameworkResolver.cs create mode 100644 src/RefScout.Analyzer/Resolvers/IResolver.cs create mode 100644 src/RefScout.Analyzer/Resolvers/IResolverFactory.cs create mode 100644 src/RefScout.Analyzer/Resolvers/MonoResolver.cs create mode 100644 src/RefScout.Analyzer/Resolvers/Resolver.cs create mode 100644 src/RefScout.Analyzer/Resolvers/ResolverFactory.cs create mode 100644 src/RefScout.Analyzer/Resolvers/Strategies/Core/CoreNuGetPackageResolverStrategy.cs create mode 100644 src/RefScout.Analyzer/Resolvers/Strategies/Core/CoreSharedResolverStrategy.cs create mode 100644 src/RefScout.Analyzer/Resolvers/Strategies/Framework/CorLibResolverStrategy.cs create mode 100644 src/RefScout.Analyzer/Resolvers/Strategies/Framework/FileSystemGacResolverStrategy.cs create mode 100644 src/RefScout.Analyzer/Resolvers/Strategies/Framework/FrameworkProxyGacResolverStrategy.cs create mode 100644 src/RefScout.Analyzer/Resolvers/Strategies/Framework/FusionGacResolverStrategy.cs create mode 100644 src/RefScout.Analyzer/Resolvers/Strategies/Framework/SilverlightResolverStrategy.cs create mode 100644 src/RefScout.Analyzer/Resolvers/Strategies/Framework/WindowsMetadataResolverStrategy.cs create mode 100644 src/RefScout.Analyzer/Resolvers/Strategies/IResolverStrategy.cs create mode 100644 src/RefScout.Analyzer/Resolvers/Strategies/Mono/MonoCorLibResolverStrategy.cs create mode 100644 src/RefScout.Analyzer/Resolvers/Strategies/Mono/MonoGacResolverStrategy.cs create mode 100644 src/RefScout.Analyzer/Resolvers/Strategies/Mono/MonoRuntimeResolverStrategy.cs create mode 100644 src/RefScout.Analyzer/Resolvers/Strategies/ResolverHelper.cs create mode 100644 src/RefScout.Analyzer/Resolvers/Strategies/Shared/DirectoryResolverStrategy.cs create mode 100644 src/RefScout.Analyzer/TargetFramework.cs create mode 100644 src/RefScout.Cli/Commands/ConsoleConflictVisualizerCommand.cs create mode 100644 src/RefScout.Cli/Commands/DotConflictVisualizerCommand.cs create mode 100644 src/RefScout.Cli/Commands/IVisualizerCommand.cs create mode 100644 src/RefScout.Cli/Commands/VisualizerCommand.cs create mode 100644 src/RefScout.Cli/Program.cs create mode 100644 src/RefScout.Cli/RefScout.Cli.csproj create mode 100644 src/RefScout.Cli/Resources/Icon.ico create mode 100644 src/RefScout.Cli/TrimmerRoots.xml create mode 100644 src/RefScout.Core/Constants.cs create mode 100644 src/RefScout.Core/Extensions/ListExtensions.cs create mode 100644 src/RefScout.Core/Helpers/ConsoleHelper.cs create mode 100644 src/RefScout.Core/Helpers/ResourceHelper.cs create mode 100644 src/RefScout.Core/Logging/ConsoleLogger.cs create mode 100644 src/RefScout.Core/Logging/ILogger.cs create mode 100644 src/RefScout.Core/Logging/LogEntry.cs create mode 100644 src/RefScout.Core/Logging/Logger.cs create mode 100644 src/RefScout.Core/RefScout.Core.csproj create mode 100644 src/RefScout.Ipc.FrameworkRuntime/Program.cs create mode 100644 src/RefScout.Ipc.FrameworkRuntime/RefScout.Ipc.FrameworkRuntime.csproj create mode 100644 src/RefScout.Ipc/Client/IIpcClient.cs create mode 100644 src/RefScout.Ipc/Client/IpcClient.cs create mode 100644 src/RefScout.Ipc/Client/PipeIpcClient.cs create mode 100644 src/RefScout.Ipc/Client/TcpIpcClient.cs create mode 100644 src/RefScout.Ipc/IsExternalInit.cs create mode 100644 src/RefScout.Ipc/RefScout.Ipc.csproj create mode 100644 src/RefScout.Ipc/Server/IIpcRequest.cs create mode 100644 src/RefScout.Ipc/Server/IIpcServer.cs create mode 100644 src/RefScout.Ipc/Server/PipeIpcRequest.cs create mode 100644 src/RefScout.Ipc/Server/PipeIpcServer.cs create mode 100644 src/RefScout.Ipc/Server/TcpIpcRequest.cs create mode 100644 src/RefScout.Ipc/Server/TcpIpcServer.cs create mode 100644 src/RefScout.Visualizers/AssemblyTreeFilter.cs create mode 100644 src/RefScout.Visualizers/Console/ConsoleVisualizer.cs create mode 100644 src/RefScout.Visualizers/Dgml/DgmlConflictVisualizer.cs create mode 100644 src/RefScout.Visualizers/Dgml/DgmlStyles.cs create mode 100644 src/RefScout.Visualizers/Dgml/XmlHelper.cs create mode 100644 src/RefScout.Visualizers/Dot/Compiler/DotEdge.cs create mode 100644 src/RefScout.Visualizers/Dot/Compiler/DotElement.cs create mode 100644 src/RefScout.Visualizers/Dot/Compiler/DotGraph.cs create mode 100644 src/RefScout.Visualizers/Dot/Compiler/DotHelpers.cs create mode 100644 src/RefScout.Visualizers/Dot/Compiler/DotNode.cs create mode 100644 src/RefScout.Visualizers/Dot/Compiler/IDotCompilable.cs create mode 100644 src/RefScout.Visualizers/Dot/DotConflictVisualizer.cs create mode 100644 src/RefScout.Visualizers/Dot/DotThemes.cs create mode 100644 src/RefScout.Visualizers/IVisualizerOptions.cs create mode 100644 src/RefScout.Visualizers/RefScout.Visualizers.csproj create mode 100644 src/RefScout.Visualizers/Visualizer.cs create mode 100644 src/RefScout.Wpf/App.xaml create mode 100644 src/RefScout.Wpf/App.xaml.cs create mode 100644 src/RefScout.Wpf/AppSettings.cs create mode 100644 src/RefScout.Wpf/AssemblyInfo.cs create mode 100644 src/RefScout.Wpf/Converters/InverseNullOrEmptyCollectionVisibilityConverter.cs create mode 100644 src/RefScout.Wpf/Converters/NullOrEmptyCollectionVisibilityConverter.cs create mode 100644 src/RefScout.Wpf/Converters/NullVisibilityConverter.cs create mode 100644 src/RefScout.Wpf/Converters/SourceLanguageToTextConverter.cs create mode 100644 src/RefScout.Wpf/Helpers/GraphVizHelper.cs create mode 100644 src/RefScout.Wpf/Helpers/ICloseable.cs create mode 100644 src/RefScout.Wpf/Helpers/ImageSvgConverterEx.cs create mode 100644 src/RefScout.Wpf/Helpers/ProcessHelper.cs create mode 100644 src/RefScout.Wpf/Images/ReferenceIcon.png create mode 100644 src/RefScout.Wpf/Models/BindingProxy.cs create mode 100644 src/RefScout.Wpf/Models/ComboBoxEntry.cs create mode 100644 src/RefScout.Wpf/Models/FrameworkGroup.cs create mode 100644 src/RefScout.Wpf/Models/LanguageGroup.cs create mode 100644 src/RefScout.Wpf/RefScout.Wpf.csproj create mode 100644 src/RefScout.Wpf/Resources/Icon.ico create mode 100644 src/RefScout.Wpf/Services/ContextService.cs create mode 100644 src/RefScout.Wpf/Services/IContextService.cs create mode 100644 src/RefScout.Wpf/Services/ILoggingService.cs create mode 100644 src/RefScout.Wpf/Services/ISettingsService.cs create mode 100644 src/RefScout.Wpf/Services/LoggingService.cs create mode 100644 src/RefScout.Wpf/Services/SettingsService.cs create mode 100644 src/RefScout.Wpf/Styles/All.xaml create mode 100644 src/RefScout.Wpf/Styles/Brushes.Dark.xaml create mode 100644 src/RefScout.Wpf/Styles/Brushes.Light.xaml create mode 100644 src/RefScout.Wpf/Styles/Buttons.xaml create mode 100644 src/RefScout.Wpf/Styles/ComboBox.xaml create mode 100644 src/RefScout.Wpf/Styles/Icons.xaml create mode 100644 src/RefScout.Wpf/Styles/Misc.xaml create mode 100644 src/RefScout.Wpf/Styles/ScrollBar.xaml create mode 100644 src/RefScout.Wpf/Styles/TabControl.xaml create mode 100644 src/RefScout.Wpf/Styles/TreeView.xaml create mode 100644 src/RefScout.Wpf/Styles/Window.xaml create mode 100644 src/RefScout.Wpf/Themes/Dark.xaml create mode 100644 src/RefScout.Wpf/Themes/Generic.xaml create mode 100644 src/RefScout.Wpf/Themes/Light.xaml create mode 100644 src/RefScout.Wpf/ViewModels/AssemblyListTabViewModel.cs create mode 100644 src/RefScout.Wpf/ViewModels/DetailsWindowViewModel.cs create mode 100644 src/RefScout.Wpf/ViewModels/EnvironmentTabViewModel.cs create mode 100644 src/RefScout.Wpf/ViewModels/GraphContainerViewModel.cs create mode 100644 src/RefScout.Wpf/ViewModels/LoggingWindowViewModel.cs create mode 100644 src/RefScout.Wpf/ViewModels/MainWindowViewModel.cs create mode 100644 src/RefScout.Wpf/ViewModels/SettingsWindowViewModel.cs create mode 100644 src/RefScout.Wpf/ViewModels/TechnologiesTabViewModel.cs create mode 100644 src/RefScout.Wpf/ViewModels/ViewModelLocator.cs create mode 100644 src/RefScout.Wpf/Views/AssemblyListTab.xaml create mode 100644 src/RefScout.Wpf/Views/AssemblyListTab.xaml.cs create mode 100644 src/RefScout.Wpf/Views/Controls/AssemblyView.xaml create mode 100644 src/RefScout.Wpf/Views/Controls/AssemblyView.xaml.cs create mode 100644 src/RefScout.Wpf/Views/Controls/GraphViewer.xaml create mode 100644 src/RefScout.Wpf/Views/Controls/GraphViewer.xaml.cs create mode 100644 src/RefScout.Wpf/Views/Controls/Icon.xaml create mode 100644 src/RefScout.Wpf/Views/Controls/Icon.xaml.cs create mode 100644 src/RefScout.Wpf/Views/Controls/IconButton.xaml create mode 100644 src/RefScout.Wpf/Views/Controls/IconButton.xaml.cs create mode 100644 src/RefScout.Wpf/Views/Controls/ReferenceList.xaml create mode 100644 src/RefScout.Wpf/Views/Controls/ReferenceList.xaml.cs create mode 100644 src/RefScout.Wpf/Views/Controls/Spinner.xaml create mode 100644 src/RefScout.Wpf/Views/Controls/Spinner.xaml.cs create mode 100644 src/RefScout.Wpf/Views/DetailsWindow.xaml create mode 100644 src/RefScout.Wpf/Views/DetailsWindow.xaml.cs create mode 100644 src/RefScout.Wpf/Views/EnvironmentTab.xaml create mode 100644 src/RefScout.Wpf/Views/EnvironmentTab.xaml.cs create mode 100644 src/RefScout.Wpf/Views/GraphContainer.xaml create mode 100644 src/RefScout.Wpf/Views/GraphContainer.xaml.cs create mode 100644 src/RefScout.Wpf/Views/LoggingWindow.xaml create mode 100644 src/RefScout.Wpf/Views/LoggingWindow.xaml.cs create mode 100644 src/RefScout.Wpf/Views/MainWindow.xaml create mode 100644 src/RefScout.Wpf/Views/MainWindow.xaml.cs create mode 100644 src/RefScout.Wpf/Views/SettingsWindow.xaml create mode 100644 src/RefScout.Wpf/Views/SettingsWindow.xaml.cs create mode 100644 src/RefScout.Wpf/Views/TechnologiesTab.xaml create mode 100644 src/RefScout.Wpf/Views/TechnologiesTab.xaml.cs create mode 100644 src/RefScout.Wpf/Views/TreeTab.xaml create mode 100644 src/RefScout.Wpf/Views/TreeTab.xaml.cs create mode 100644 tests/RefScout.Analyzer.FunctionalTests/RefScout.Analyzer.FunctionalTests.csproj create mode 100644 tests/RefScout.Analyzer.FunctionalTests/ReferenceAnalyzerTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Analyzers/Assemblies/ReferencedAssembliesAnalyzerTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Analyzers/Assemblies/UnreferencedAssembliesAnalyzerTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Analyzers/Compatibility/CompatibilityAnalyzerTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Analyzers/Compatibility/DefaultVersionComparerTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Analyzers/Compatibility/FrameworkCompatibilityAnalyzerTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Analyzers/Environment/Core/CoreRuntimeAnalyzerTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Analyzers/Environment/Core/CoreRuntimeTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Analyzers/Environment/EnvironmentAnalyzerTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Analyzers/Environment/Mono/MonoRuntimeAnalyzerTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/AssHelp.cs create mode 100644 tests/RefScout.Analyzer.Tests/AssemblyRefTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/AssemblyTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Config/ConfigParserFactoryTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Config/Core/CoreConfigParserTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Config/Framework/FrameworkConfigParserTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Context/FrameworkContextTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/FakeEnvironment.cs create mode 100644 tests/RefScout.Analyzer.Tests/Fakes/FakeAssemblyReader.cs create mode 100644 tests/RefScout.Analyzer.Tests/Fakes/FakeContext.cs create mode 100644 tests/RefScout.Analyzer.Tests/Fakes/FakeMonoContext.cs create mode 100644 tests/RefScout.Analyzer.Tests/Fakes/FakeResolver.cs create mode 100644 tests/RefScout.Analyzer.Tests/Filter/StringSplitEnumeratorTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Helpers/AssemblyHelperTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Helpers/EnvironmentWrapperTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/ConflictNoteTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/Messages/Core/CoreMissingRuntimeMessageTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/Messages/Core/CoreVersionMismatchWarningMessageTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/Messages/Core/FakeCoreMessageContext.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/Messages/Framework/CoreMissingRuntimeMessageTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/Messages/Message{T}Tests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/Messages/Mono/CoreMissingRuntimeMessageTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/Messages/Shared/ArchitectureMismatchMessageTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/Messages/Shared/ConfigParseErrorMessageTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/Messages/Shared/ErrorMessageTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/Messages/Shared/NotFoundMessageTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/Messages/Shared/UnreferencedMessageTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/Messages/Shared/VersionMismatchMessageTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/Messages/SharedFramework/FakeFrameworkMessageContext.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/Messages/SharedFramework/FrameworkVersionMismatchFatalMessageTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/Messages/SharedFramework/FrameworkVersionMismatchWarningMessageTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/Messages/SharedFramework/RedirectFailedMessageTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/Messages/SharedFramework/RedirectFailedWrongVersionMessageTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/Messages/SharedFramework/RedirectSuccessMessageTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/Messages/SharedFramework/UnificationMessageTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Notes/NoteGeneratorTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/PublicKeyTokenTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Readers/AssemblyReaderTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Readers/Cecil/CecilAssemblyReaderTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Readers/Cecil/CecilMetadataReaderTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/RefScout.Analyzer.Tests.csproj create mode 100644 tests/RefScout.Analyzer.Tests/Resolvers/Strategies/Core/CoreNuGetPackageResolverStrategyTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Resolvers/Strategies/Core/CoreSharedResolverStrategyTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Resolvers/Strategies/FakeFileSystem.cs create mode 100644 tests/RefScout.Analyzer.Tests/Resolvers/Strategies/Framework/CorLibResolverStrategyTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Resolvers/Strategies/Framework/FileSystemGacResolverStrategyTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Resolvers/Strategies/Framework/FrameworkProxyGacResolverStrategyTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Resolvers/Strategies/Framework/FusionGacResolverStrategyTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Resolvers/Strategies/Framework/SilverlightResolverStrategyTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Resolvers/Strategies/Framework/WindowsMetadataResolverStrategyTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Resolvers/Strategies/Mono/FakeMonoRuntimeAnalyzer.cs create mode 100644 tests/RefScout.Analyzer.Tests/Resolvers/Strategies/Mono/MonoCorLibResolverStrategyTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Resolvers/Strategies/Mono/MonoGacResolverStrategyTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Resolvers/Strategies/Mono/MonoRuntimeResolverStrategyTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Resolvers/Strategies/Shared/DirectoryResolverStrategyTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/TargetFrameworkTests.cs create mode 100644 tests/RefScout.Analyzer.Tests/Testfiles/Assemblies/Managed.dll create mode 100644 tests/RefScout.Analyzer.Tests/Testfiles/Assemblies/ShareX.exe create mode 100644 tests/RefScout.Analyzer.Tests/Testfiles/Assemblies/SharpVectors.Runtime.Wpf.dll create mode 100644 tests/RefScout.Analyzer.Tests/Testfiles/Assemblies/System.Data.dll create mode 100644 tests/RefScout.Analyzer.Tests/Testfiles/Assemblies/System.Runtime_linux.dll create mode 100644 tests/RefScout.Analyzer.Tests/Testfiles/Configs/Bindings.config create mode 100644 tests/RefScout.Analyzer.Tests/Testfiles/Configs/Broken.config create mode 100644 tests/RefScout.Analyzer.Tests/Testfiles/Configs/HasEverything.config create mode 100644 tests/RefScout.Analyzer.Tests/Testfiles/Configs/NoBindings.config create mode 100644 tests/RefScout.Analyzer.Tests/Testfiles/CoreConfigs/Invalid.runtimeconfig.json create mode 100644 tests/RefScout.Analyzer.Tests/Testfiles/CoreConfigs/Test.deps.json create mode 100644 tests/RefScout.Analyzer.Tests/Testfiles/CoreConfigs/Test.runtimeconfig.json create mode 100644 tests/RefScout.Analyzer.Tests/Testfiles/Mono/resgen.exe create mode 160000 tests/TestData diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..05cb447 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,60 @@ + +[*.proto] +indent_style = tab +indent_size = tab +tab_width = 4 + +[*.{asax,ascx,aspx,axaml,cs,cshtml,css,htm,html,js,jsx,master,paml,razor,skin,ts,tsx,vb,xaml,xamlx,xoml}] +indent_style = space +indent_size = 4 +tab_width = 4 + +[*.{appxmanifest,axml,build,config,csproj,dbml,discomap,dtd,json,jsproj,lsproj,njsproj,nuspec,proj,props,resjson,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}] +indent_style = space +indent_size = 2 +tab_width = 2 + +[*] + +# Microsoft .NET properties +csharp_new_line_before_members_in_object_initializers = false +csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_qualification_for_event = false:suggestion +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +# ReSharper properties +resharper_braces_for_for = required +resharper_braces_for_foreach = required +resharper_braces_for_ifelse = required +resharper_braces_for_while = required +resharper_csharp_empty_block_style = together_same_line +resharper_csharp_wrap_after_declaration_lpar = true +resharper_csharp_wrap_parameters_style = chop_if_long +resharper_local_function_body = expression_body +resharper_method_or_operator_body = expression_body +resharper_place_field_attribute_on_same_line = false + +# ReSharper inspection severities +resharper_arrange_redundant_parentheses_highlighting = hint +resharper_arrange_this_qualifier_highlighting = hint +resharper_arrange_type_member_modifiers_highlighting = hint +resharper_arrange_type_modifiers_highlighting = hint +resharper_built_in_type_reference_style_for_member_access_highlighting = hint +resharper_built_in_type_reference_style_highlighting = hint +resharper_class_never_instantiated_global_highlighting = none +resharper_redundant_base_qualifier_highlighting = warning +resharper_redundant_extends_list_entry_highlighting = none +resharper_suggest_var_or_type_built_in_types_highlighting = hint +resharper_suggest_var_or_type_elsewhere_highlighting = hint +resharper_suggest_var_or_type_simple_types_highlighting = hint diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b8473af --- /dev/null +++ b/.gitignore @@ -0,0 +1,397 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +src/Test/ + +raw/ + +# Publish related folders +_output/ +_nupkg/ + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Nuget personal access tokens and Credentials +nuget.config + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +tools/** +!tools/packages.config +.config/ + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +.idea/ +*.sln.iml \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b877fa1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tests/TestData"] + path = tests/TestData + url = https://github.com/Droppers/RefScout-TestData diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..be31aad --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Joery Droppers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..959ee8e --- /dev/null +++ b/README.md @@ -0,0 +1,218 @@ +

  RefScout - Reference Analyzer


+

+ RefScout is a desktop and command-line application that assists you with identifying conflicting assembly references in your .NET Core and .NET Framework applications.

+ +

+ +
+    + .NET target   +   By Joery Droppers +
+ +## Features + - .NET Core support (+ single file applications) + - Interactive desktop application + - Strong named assemblies + - .NET Framework config parsing (probing paths, codebase, binding redirects) + - Reports various conflicts: binding redirect misconfiguration, version mismatch, processor architecture mismatch, missing runtime versions, etc. + +## Install + +**Windows**
+[win-RefScout-1.0.zip](refana-windows-0.1.zip) (Requires .NET 6.0, CLI+Desktop)
+[win-contained-RefScout-1.0.zip](refana-windows-0.1.zip) (Self-contained, CLI)
+[win-contained-desktop-RefScout-1.0.zip](refana-windows-0.1.zip) (Self-contained, Desktop, large: 120MB)
+ +**Linux**
+[linux-RefScout-1.0.zip](refana-linux-0.1.zip) (Requires .NET 6.0, CLI)
+[linux-contained-RefScout-1.0.zip](refana-linux-0.1.zip) (Self-contained, CLI) + +**dotnet tool**
+`dotnet tool install -g refscout` + +## Usage +### Desktop + +Drag and drop the application executable or use the "Select an assembly" button to analyze the application. After analyzing, you can click on an assembly in the graph on the right side or in the "Overview" tab to open a pop-up window with more detailed information about that assembly. + +Using the desktop application should speak for itself, otherwise you can find a description of the options below. + +### Command-line + +``` +refscout assembly.exe +refscout assembly.dll +``` + +For .NET Core applications supply RefScout with the Application.dll rather than the executable (Application.exe) + +#### Options +```sh +refscout [assembly] [options] + +assembly - The .NET assembly (.dll or .exe) to be analyzed. +-c, --config [path] - specificy the location of the app.config or web.config file, defaults to the config named the same as the input assembly. +``` + +**Analyzer options** + +``` +-a, --analyze [mode] - Specify to which degree assembly references should be analyzed + - Values: AppDirectSystem, App, All +-r, --runtime [runtime] - Specify which runtime type should be used for analyzing. Useful for analyzing Mono applications on Windows. + - Values: Default, Core, Framework, Mono +-sv, --system-versions [mode] - Specify how strict version mismatches of system assemblies should be reported. + - Values: Off, Loose, Strict +-f, --filter [filter] - Specify a query string to filter the results of the analyzer. +``` + +**Visualizer options** + +``` +-v, --visualize [mode] - Specify which type of assemblies should be visualized by the visualizer. + - Values: Default, All, Conflicts, App +Console +-cd, --console-detail - Show a more detailed information in the console such as target framework and source language. + +Dot (GraphViz) +--dot [graph.dot] - Export analyzer results to a GraphViz dot graph. +-dotf, --dot-framework - Include an overview of target frameworks used in assemblies. + +DGML (Visual Studio graph) +--dgml [graph.dgml] - Export analyzer results to a Visual Studio DGML graph. +``` + +## Screenshots + +Todo (maybe collapsible) + +## Detailed options +These options apply to both the desktop application and the command-line application. + +#### Desktop + - Press the "graph" icon on the top right to toggle the assembly tree direction from top -> bottom to left -> right. + - Press the ".NET" icon in the top right corner to toggle the visibility of used target frameworks in your application. + - Press the "cog wheel" icon next to the analyze button to open the settings dialog for all other options. + +#### Analyze mode + +By default, the direct system references of the assemblies are analyzed. You can choose to analyze only the application-related assemblies or also to analyze the references of system assemblies. + +```sh +-a, --analyze AppDirectSystem # Only analyzes application and direct system references. + App # Only analyzes application related references, not system assemblies. + All # Analyzes everything, including references of system assemblies. +``` + +#### Runtime type + +By default the application will determine which runtime to use for analyzing based on the given assembly. However, for Mono applications on Windows, it will always use .NET Framework as runtime type. Specifiy this option in order to override this behavior to use Mono. +```sh +-r, --runtime Default # It automatically determines which runtime the application targets. + Core # .NET Core + Framework # .NET Framework + Mono # Mono Runtime +``` + +#### Visualize mode + +Depending on the number of references, visualizing everything can lead to a cluttered result; therefore, you can specify to which degree the result of the analyzer will be visualized. The default value for the console visualizer is "conflicts," for all other visualizers, "all." + +```sh +-v, --visualize Default # default `conflicts` for console, `all` for others + All # Visualizes all results from the analyzer. + Conflicts # Only visualizes assemblies and references with conflicts. + App # Only Visualizes application-related assemblies and references. +``` + +#### System version cecking + +When this option is enabled, the analyzer will also consider differences in referenced system assemblies as conflicts. + +```sh +-sv, --system-versions Off # (default) ignore version mismatches for system assemblies. + Loose # mark breaking version mismatches as non-breaking, ignore minor mismatches. + Strict # mark breaking version mismatches as breaking, and minor mismatches as information. +``` + +#### Filtering results + +Filter the analyzer result by providing the `--filter` option or using the desktop application's search input. + +The following filter options are available, precede an option with an exclamation mark to do a NOT search (`!by:PaintDotNet`): + - `by:PainDotNet`: show assemblies that are referenced by PaintDotNet. + - `to:PaintDotNet.Data`: show all assemblies that have a reference to PaintDotNet. + - `source:GAC`: show all assemblies that have been resolved from a specific source: GAC, Local, CodeBase, NotFound, Error. + - `is:conflict`: show conflicting assemblies. + - `is:unreferenced`: show assemblies which are unreferenced. + - `is:system`: show system assemblies. + +An example search filter that will only show conflicting assemblies that are referenced by PaintDotNet is: `from:^PaintDotNet$ is:conflict` + +By default, the filter will do a contains search, precede the text query with ^ to do a startsWith search, succeed with a $ to do an endsWith search, and use both for an exact match. + +## Internals + +#### Version compatibility + +For references to strong-named assemblies, when there is any difference between the referenced and resolved version, it will be displayed as an fatal mismatch. For regular assemblies, versions are deemed compatible if the major and minor versions of the referenced and resolved assembly are identical. If the major version differs, a reference is considered a breaking mismatch, and displayed as a warning. If only the minor version differs, the reference is considered a non-breaking mismatch, and displayed as informative. + +### Architecture mismatch + +When the entry assembly targets an architecture other than Any CPU, the processor architecture of each referenced assembly is compared to that of the entry assembly and a warning message is shown in case it differs. This does not mean they are actually incompatible. + +#### Classifying System and .NET API assemblies + +Some other tools check whether the assembly name starts with System, this will fail for e.g., System.IO.Abstractions, or check for the PublicKeyToken. Non-framework assemblies like EntityFramework also have the same PublicKeyToken, making it unreliable. Instead, a static list containing names of .NET APIs and 'System' assemblies is included with the application. These were obtained through the .NET API Browser and the "RedistList/FrameworkList.xml" files. + +#### Assembly resolution + +Assemblies are resolved by incorporating a heavily modified version of the assembly resolver found in ILSpy and Mono.Cecil. To more reliably resolve assemblies from the global assembly cache, fusion.dll is invoked directly, as a backup "Assembly.Load" is used to resolve the assembly using the .NET Framework runtime. In case fusion.dll could not resolve the assembly, but the .NET runtime could, the assembly is marked as '[unification](https://patents.google.com/patent/US9009693B2/en)'. + +#### Inclusion of local System assemblies + +When system assemblies are found locally for a .NET Framework application, they will always be analyzed as part of the analyzed application. + +#### .NET Core runtime detection +The locations that the application will look for .NET Core runtime are the default installation location or the DOTNET_ROOT environment variable. Determining which runtime version will be used when executing the application is based on using the roll-forward logic described in the dotnet design document. + +## Why? + +To get more familiar with C#, WPF and .NET, since this is my first real C# project. Also, as part of my internship, one of my tasks was documenting the dependencies of .NET applications. However, I lacked a tool to generate a complete overview of an assembly and its references. I stumbled upon the popular AsmSpy tool and found it lacks many features. So I decided to try and create a tool to fill the gap of missing features and familiarize myself with how the runtime resolves assemblies. + +Compared to for example AsmSpy, RefScout has full support for binding redirects, codebase (local only), probing directories, strong-named assemblies, architecture mismatches, .NET Core (+ single file), detecting GAC unification, Linux support, and a desktop application. + +## Contributing +I am always open to contributions in this repository. However, make sure to create an issue beforehand to discuss the change. + +## Thanks to +- [adospace/fluent-ui-xaml](https://github.com/adospace/fluent-ui-xaml) for providing Fluent themed WPF styles. +- [jbevain/cecil](https://github.com/jbevain/cecil) for an awesome assembly file parser. +- [icsharpcode/ILSpy](https://github.com/icsharpcode/ILSpy) for their assembly resolver. +- [ElinamLLC/SharpVectors](https://github.com/ElinamLLC/SharpVectors) for the SVG viewer in the desktop application. + +## License +``` +MIT License + +Copyright (c) 2021 Joery Droppers (https://github.com/Droppers) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/RefScout.sln b/RefScout.sln new file mode 100644 index 0000000..3d94abe --- /dev/null +++ b/RefScout.sln @@ -0,0 +1,132 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31521.260 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RefScout.Cli", "src\RefScout.Cli\RefScout.Cli.csproj", "{73C9393B-4CF6-4FA5-9E80-C5B72621C27F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RefScout.Ipc", "src\RefScout.Ipc\RefScout.Ipc.csproj", "{5252BFE0-43DC-43B4-A942-AE42824A17A0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RefScout.Analyzer.Tests", "tests\RefScout.Analyzer.Tests\RefScout.Analyzer.Tests.csproj", "{BE44B27A-B650-47B5-A319-FAD72B132BC4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{485EF63C-ECF5-46B8-9C12-E48CD248E330}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AD42C4BB-ACA5-491D-815A-4C1528A44033}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RefScout.Analyzer", "src\RefScout.Analyzer\RefScout.Analyzer.csproj", "{14A93030-F910-4F38-BB6F-10C121CFBB99}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RefScout.Core", "src\RefScout.Core\RefScout.Core.csproj", "{7B2309E7-9560-4F90-9E3D-4F5B2DB6E485}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RefScout.Visualizers", "src\RefScout.Visualizers\RefScout.Visualizers.csproj", "{76E27D77-2D86-4AE7-B169-3F2339D5C5E4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4CDF6E78-01B9-4000-BD25-5DDCD6908D57}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + build.cake = build.cake + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RefScout.Analyzer.FunctionalTests", "tests\RefScout.Analyzer.FunctionalTests\RefScout.Analyzer.FunctionalTests.csproj", "{644A04F1-C964-4958-8BDA-46BA419E755F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RefScout.Wpf", "src\RefScout.Wpf\RefScout.Wpf.csproj", "{92FEB226-2104-4440-BC46-95D54F89FDE5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RefScout.Ipc.FrameworkRuntime", "src\RefScout.Ipc.FrameworkRuntime\RefScout.Ipc.FrameworkRuntime.csproj", "{F062EF70-4A87-4A56-9781-7659991AB117}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {73C9393B-4CF6-4FA5-9E80-C5B72621C27F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73C9393B-4CF6-4FA5-9E80-C5B72621C27F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73C9393B-4CF6-4FA5-9E80-C5B72621C27F}.Debug|x64.ActiveCfg = Debug|Any CPU + {73C9393B-4CF6-4FA5-9E80-C5B72621C27F}.Debug|x64.Build.0 = Debug|Any CPU + {73C9393B-4CF6-4FA5-9E80-C5B72621C27F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73C9393B-4CF6-4FA5-9E80-C5B72621C27F}.Release|Any CPU.Build.0 = Release|Any CPU + {73C9393B-4CF6-4FA5-9E80-C5B72621C27F}.Release|x64.ActiveCfg = Release|Any CPU + {73C9393B-4CF6-4FA5-9E80-C5B72621C27F}.Release|x64.Build.0 = Release|Any CPU + {5252BFE0-43DC-43B4-A942-AE42824A17A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5252BFE0-43DC-43B4-A942-AE42824A17A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5252BFE0-43DC-43B4-A942-AE42824A17A0}.Debug|x64.ActiveCfg = Debug|Any CPU + {5252BFE0-43DC-43B4-A942-AE42824A17A0}.Debug|x64.Build.0 = Debug|Any CPU + {5252BFE0-43DC-43B4-A942-AE42824A17A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5252BFE0-43DC-43B4-A942-AE42824A17A0}.Release|Any CPU.Build.0 = Release|Any CPU + {5252BFE0-43DC-43B4-A942-AE42824A17A0}.Release|x64.ActiveCfg = Release|Any CPU + {5252BFE0-43DC-43B4-A942-AE42824A17A0}.Release|x64.Build.0 = Release|Any CPU + {BE44B27A-B650-47B5-A319-FAD72B132BC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE44B27A-B650-47B5-A319-FAD72B132BC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE44B27A-B650-47B5-A319-FAD72B132BC4}.Debug|x64.ActiveCfg = Debug|Any CPU + {BE44B27A-B650-47B5-A319-FAD72B132BC4}.Debug|x64.Build.0 = Debug|Any CPU + {BE44B27A-B650-47B5-A319-FAD72B132BC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE44B27A-B650-47B5-A319-FAD72B132BC4}.Release|Any CPU.Build.0 = Release|Any CPU + {BE44B27A-B650-47B5-A319-FAD72B132BC4}.Release|x64.ActiveCfg = Release|Any CPU + {BE44B27A-B650-47B5-A319-FAD72B132BC4}.Release|x64.Build.0 = Release|Any CPU + {14A93030-F910-4F38-BB6F-10C121CFBB99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14A93030-F910-4F38-BB6F-10C121CFBB99}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14A93030-F910-4F38-BB6F-10C121CFBB99}.Debug|x64.ActiveCfg = Debug|Any CPU + {14A93030-F910-4F38-BB6F-10C121CFBB99}.Debug|x64.Build.0 = Debug|Any CPU + {14A93030-F910-4F38-BB6F-10C121CFBB99}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14A93030-F910-4F38-BB6F-10C121CFBB99}.Release|Any CPU.Build.0 = Release|Any CPU + {14A93030-F910-4F38-BB6F-10C121CFBB99}.Release|x64.ActiveCfg = Release|Any CPU + {14A93030-F910-4F38-BB6F-10C121CFBB99}.Release|x64.Build.0 = Release|Any CPU + {7B2309E7-9560-4F90-9E3D-4F5B2DB6E485}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B2309E7-9560-4F90-9E3D-4F5B2DB6E485}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B2309E7-9560-4F90-9E3D-4F5B2DB6E485}.Debug|x64.ActiveCfg = Debug|Any CPU + {7B2309E7-9560-4F90-9E3D-4F5B2DB6E485}.Debug|x64.Build.0 = Debug|Any CPU + {7B2309E7-9560-4F90-9E3D-4F5B2DB6E485}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B2309E7-9560-4F90-9E3D-4F5B2DB6E485}.Release|Any CPU.Build.0 = Release|Any CPU + {7B2309E7-9560-4F90-9E3D-4F5B2DB6E485}.Release|x64.ActiveCfg = Release|Any CPU + {7B2309E7-9560-4F90-9E3D-4F5B2DB6E485}.Release|x64.Build.0 = Release|Any CPU + {76E27D77-2D86-4AE7-B169-3F2339D5C5E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {76E27D77-2D86-4AE7-B169-3F2339D5C5E4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {76E27D77-2D86-4AE7-B169-3F2339D5C5E4}.Debug|x64.ActiveCfg = Debug|Any CPU + {76E27D77-2D86-4AE7-B169-3F2339D5C5E4}.Debug|x64.Build.0 = Debug|Any CPU + {76E27D77-2D86-4AE7-B169-3F2339D5C5E4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {76E27D77-2D86-4AE7-B169-3F2339D5C5E4}.Release|Any CPU.Build.0 = Release|Any CPU + {76E27D77-2D86-4AE7-B169-3F2339D5C5E4}.Release|x64.ActiveCfg = Release|Any CPU + {76E27D77-2D86-4AE7-B169-3F2339D5C5E4}.Release|x64.Build.0 = Release|Any CPU + {644A04F1-C964-4958-8BDA-46BA419E755F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {644A04F1-C964-4958-8BDA-46BA419E755F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {644A04F1-C964-4958-8BDA-46BA419E755F}.Debug|x64.ActiveCfg = Debug|Any CPU + {644A04F1-C964-4958-8BDA-46BA419E755F}.Debug|x64.Build.0 = Debug|Any CPU + {644A04F1-C964-4958-8BDA-46BA419E755F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {644A04F1-C964-4958-8BDA-46BA419E755F}.Release|Any CPU.Build.0 = Release|Any CPU + {644A04F1-C964-4958-8BDA-46BA419E755F}.Release|x64.ActiveCfg = Release|Any CPU + {644A04F1-C964-4958-8BDA-46BA419E755F}.Release|x64.Build.0 = Release|Any CPU + {92FEB226-2104-4440-BC46-95D54F89FDE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {92FEB226-2104-4440-BC46-95D54F89FDE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {92FEB226-2104-4440-BC46-95D54F89FDE5}.Debug|x64.ActiveCfg = Debug|Any CPU + {92FEB226-2104-4440-BC46-95D54F89FDE5}.Debug|x64.Build.0 = Debug|Any CPU + {92FEB226-2104-4440-BC46-95D54F89FDE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {92FEB226-2104-4440-BC46-95D54F89FDE5}.Release|Any CPU.Build.0 = Release|Any CPU + {92FEB226-2104-4440-BC46-95D54F89FDE5}.Release|x64.ActiveCfg = Release|Any CPU + {92FEB226-2104-4440-BC46-95D54F89FDE5}.Release|x64.Build.0 = Release|Any CPU + {F062EF70-4A87-4A56-9781-7659991AB117}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F062EF70-4A87-4A56-9781-7659991AB117}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F062EF70-4A87-4A56-9781-7659991AB117}.Debug|x64.ActiveCfg = Debug|Any CPU + {F062EF70-4A87-4A56-9781-7659991AB117}.Debug|x64.Build.0 = Debug|Any CPU + {F062EF70-4A87-4A56-9781-7659991AB117}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F062EF70-4A87-4A56-9781-7659991AB117}.Release|Any CPU.Build.0 = Release|Any CPU + {F062EF70-4A87-4A56-9781-7659991AB117}.Release|x64.ActiveCfg = Release|Any CPU + {F062EF70-4A87-4A56-9781-7659991AB117}.Release|x64.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {73C9393B-4CF6-4FA5-9E80-C5B72621C27F} = {485EF63C-ECF5-46B8-9C12-E48CD248E330} + {5252BFE0-43DC-43B4-A942-AE42824A17A0} = {485EF63C-ECF5-46B8-9C12-E48CD248E330} + {BE44B27A-B650-47B5-A319-FAD72B132BC4} = {AD42C4BB-ACA5-491D-815A-4C1528A44033} + {14A93030-F910-4F38-BB6F-10C121CFBB99} = {485EF63C-ECF5-46B8-9C12-E48CD248E330} + {7B2309E7-9560-4F90-9E3D-4F5B2DB6E485} = {485EF63C-ECF5-46B8-9C12-E48CD248E330} + {76E27D77-2D86-4AE7-B169-3F2339D5C5E4} = {485EF63C-ECF5-46B8-9C12-E48CD248E330} + {644A04F1-C964-4958-8BDA-46BA419E755F} = {AD42C4BB-ACA5-491D-815A-4C1528A44033} + {92FEB226-2104-4440-BC46-95D54F89FDE5} = {485EF63C-ECF5-46B8-9C12-E48CD248E330} + {F062EF70-4A87-4A56-9781-7659991AB117} = {485EF63C-ECF5-46B8-9C12-E48CD248E330} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {137C118A-547A-4675-A3D6-BAAA2978AE5D} + EndGlobalSection +EndGlobal diff --git a/RefScout.sln.DotSettings b/RefScout.sln.DotSettings new file mode 100644 index 0000000..4602056 --- /dev/null +++ b/RefScout.sln.DotSettings @@ -0,0 +1,23 @@ + + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True \ No newline at end of file diff --git a/art/logo.svg b/art/logo.svg new file mode 100644 index 0000000..c60cf71 --- /dev/null +++ b/art/logo.svg @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/screenshot.png b/art/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..aa9dfdaa437589ae446c78e6a1baee34e386d859 GIT binary patch literal 90728 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 literal 0 HcmV?d00001 diff --git a/build.cake b/build.cake new file mode 100644 index 0000000..8e67a62 --- /dev/null +++ b/build.cake @@ -0,0 +1,191 @@ +#addin Cake.FileHelpers&version=4.0.1 +#tool nuget:?package=NuGet.CommandLine&version=5.9.1 +#tool nuget:?package=xunit.runner.console&version=2.4.1 + +var target = Argument("target", "Pack"); +var configuration = Argument("configuration", "Release"); +var targetFramework = Argument("targetFrameworkNetCore", "net6.0"); + + +var _baseName = "RefScout"; +var _publishName = "refscout"; +var _solution = "./RefScout.sln"; +var _appVersion = "0.1.0"; +var _outputDir = Directory($"_output"); +var _testOutputDir = Directory($"_testOutput"); + +Task("Clean") + .Description("Cleans all directories that are used during the build process.") + .Does(() => +{ + CleanDirectory(_outputDir); + CleanDirectories("./**/bin/" + configuration); + CleanDirectories("./**/obj/"); +}); + +Task("Restore") + .Description("Restores all the NuGet packages that are used by the specified solution.") + .Does(() => +{ + Information("Restoring {0}...", _solution); + NuGetRestore(_solution); +}); + + +Task("Build") + .Description("Builds all the different parts of the project.") + .IsDependentOn("Clean") + .IsDependentOn("Restore") + .Does(() => +{ + Information("Building {0} for tests", _solution); + + MSBuild($"./src/{_baseName}.IPC.FrameworkRuntime/{_baseName}.IPC.FrameworkRuntime.csproj", settings => + settings.SetPlatformTarget(PlatformTarget.MSIL) + .SetMSBuildPlatform(MSBuildPlatform.x64) + .UseToolVersion(MSBuildToolVersion.VS2019) + .WithTarget("Build") + .SetConfiguration(configuration)); + + // MSBuild(_solution, settings => + // settings.SetPlatformTarget(PlatformTarget.MSIL) + // .SetMSBuildPlatform(MSBuildPlatform.x64) + // .UseToolVersion(MSBuildToolVersion.VS2019) // 2022 when cake updated, 2019 does not support 6.0 + // .WithTarget("Build") + // .SetConfiguration(configuration)); +}); + +Task("Test") + .Does(() => +{ + var settings = new DotNetCoreTestSettings + { + Configuration = "Release" + }; + + var projectFiles = GetFiles("./tests/**/*.csproj"); + foreach(var file in projectFiles) + { + DotNetCoreTest(file.FullPath, settings); + } +}); + +// Publishes CLI variant for Linux (x64) +Task("Publish-Linux") + .Does(() => +{ + var settings = new DotNetCorePublishSettings + { + Framework = targetFramework, + Configuration = "Release", + Runtime = "linux-x64", + SelfContained = false, + PublishSingleFile = true, + PublishReadyToRun = false, + }; + settings.OutputDirectory = $"{_outputDir.Path}/{settings.Runtime}/"; + DotNetCorePublish($"./src/{_baseName}.Cli/{_baseName}.Cli.csproj", settings); + MoveFile($"{settings.OutputDirectory}/{_baseName}.Cli", $"{settings.OutputDirectory}/{_publishName}"); +}); + +// Publishes Linux-contained CLI variant for Windows (x64) +Task("Publish-Linux-SelfContained") + .Does(() => +{ + var settings = new DotNetCorePublishSettings + { + Framework = targetFramework, + Configuration = "Release", + Runtime = "linux-x64", + PublishSingleFile = true, + PublishReadyToRun = false, + SelfContained = true, + PublishTrimmed = true + }; + settings.OutputDirectory = $"{_outputDir.Path}/{settings.Runtime}-contained/"; + DotNetCorePublish($"./src/{_baseName}.Cli/{_baseName}.Cli.csproj", settings); + MoveFile($"{settings.OutputDirectory}/{_baseName}.Cli", $"{settings.OutputDirectory}/{_publishName}"); +}); + +// Publishes Desktop, CLI variants for Windows (x64) +Task("Publish-Windows") + .Does(() => +{ + var settings = new DotNetCorePublishSettings + { + Framework = targetFramework, + Configuration = "Release", + Runtime = "win-x64", + SelfContained = false, + PublishSingleFile = true, + PublishReadyToRun = false, // TODO: investigate why this does not work + }; + settings.OutputDirectory = $"{_outputDir.Path}/{settings.Runtime}/"; + DotNetCorePublish($"./src/{_baseName}.Cli/{_baseName}.Cli.csproj", settings); + MoveFile($"{settings.OutputDirectory}/{_baseName}.Cli.exe", $"{settings.OutputDirectory}/{_publishName}.exe"); + + settings.Framework = $"{targetFramework}-windows"; + DotNetCorePublish($"./src/{_baseName}.Wpf/{_baseName}.Wpf.csproj", settings); + MoveFile($"{settings.OutputDirectory}/{_baseName}.Wpf.exe", $"{settings.OutputDirectory}/{_publishName}-desktop.exe"); +}); + +// Publishes self-contained CLI and Desktop variant for Windows (x64) +Task("Publish-Windows-SelfContained") + .Does(() => +{ + var settings = new DotNetCorePublishSettings + { + Framework = targetFramework, + Configuration = "Release", + Runtime = "win-x64", + IncludeNativeLibrariesForSelfExtract = true, + PublishSingleFile = true, + PublishReadyToRun = false, + SelfContained = true, + PublishTrimmed = true + }; + settings.OutputDirectory = $"{_outputDir.Path}/{settings.Runtime}-contained/"; + DotNetCorePublish($"./src/{_baseName}.CLI/{_baseName}.CLI.csproj", settings); + MoveFile($"{settings.OutputDirectory}/{_baseName}.CLI.exe", $"{settings.OutputDirectory}/{_publishName}.exe"); + + settings.PublishTrimmed = false; + settings.Framework = $"{targetFramework}-windows"; + settings.ArgumentCustomization = args => args.Append("-p:EnableCompressionInSingleFile=True"); + DotNetCorePublish($"./src/{_baseName}.Wpf/{_baseName}.Wpf.csproj", settings); + MoveFile($"{settings.OutputDirectory}/{_baseName}.Wpf.exe", $"{settings.OutputDirectory}/{_publishName}-desktop.exe"); +}); + +Task("Publish") + .IsDependentOn("Build") + .IsDependentOn("Test") + .IsDependentOn("Publish-Linux") + .IsDependentOn("Publish-Linux-SelfContained") + .IsDependentOn("Publish-Windows") + .IsDependentOn("Publish-Windows-SelfContained") + .Does(() => +{ + foreach (var extension in new string[]{"pdb", "config", "xml"}) + DeleteFiles(_outputDir.Path + "/**/*." + extension); +}); + +Task("Pack") + .IsDependentOn("Publish") + .Does(() => +{ + var appFiles = GetFiles($"{_outputDir}/linux-x64/**/*"); + Zip(_outputDir.Path, _outputDir.Path + $"/linux-{_publishName}-{_appVersion}.zip", appFiles); + + appFiles = GetFiles($"{_outputDir}/win-x64/**/*"); + Zip(_outputDir.Path, _outputDir.Path + $"/win-{_publishName}-{_appVersion}.zip", appFiles); + + appFiles = GetFiles($"{_outputDir}/linux-x64-contained/{_publishName}"); + Zip(_outputDir.Path, _outputDir.Path + $"/linux-contained-{_publishName}-{_appVersion}.zip", appFiles); + + appFiles = GetFiles($"{_outputDir}/win-x64-contained/{_publishName}.exe"); + Zip(_outputDir.Path, _outputDir.Path + $"/win-contained-{_publishName}-{_appVersion}.zip", appFiles); + + appFiles = GetFiles($"{_outputDir}/win-x64-contained/{_publishName}-desktop.exe"); + Zip(_outputDir.Path, _outputDir.Path + $"/win-contained-desktop-{_publishName}-{_appVersion}.zip", appFiles); +}); + +RunTarget(target); \ No newline at end of file diff --git a/src/RefScout.Analyzer/AnalyzeRuntime.cs b/src/RefScout.Analyzer/AnalyzeRuntime.cs new file mode 100644 index 0000000..7b9b3c6 --- /dev/null +++ b/src/RefScout.Analyzer/AnalyzeRuntime.cs @@ -0,0 +1,9 @@ +namespace RefScout.Analyzer; + +public enum AnalyzeRuntime +{ + Default, + Framework, + Mono, + Core +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Assemblies/IAssemblyAnalyzer.cs b/src/RefScout.Analyzer/Analyzers/Assemblies/IAssemblyAnalyzer.cs new file mode 100644 index 0000000..d2e5164 --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Assemblies/IAssemblyAnalyzer.cs @@ -0,0 +1,9 @@ +using System.Threading; +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Analyzers.Assemblies; + +internal interface IAssemblyAnalyzer +{ + void Analyze(IContext context, AnalyzerOptions options, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Assemblies/ReferencedAssembliesAnalyzer.cs b/src/RefScout.Analyzer/Analyzers/Assemblies/ReferencedAssembliesAnalyzer.cs new file mode 100644 index 0000000..e83d937 --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Assemblies/ReferencedAssembliesAnalyzer.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Threading; +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Analyzers.Assemblies; + +internal class ReferencedAssembliesAnalyzer : IAssemblyAnalyzer +{ + public void Analyze(IContext context, AnalyzerOptions options, CancellationToken cancellationToken) + { + var assembliesToVisit = new Queue(); + assembliesToVisit.Enqueue(context.EntryPoint); + while (assembliesToVisit.Count > 0) + { + var assembly = assembliesToVisit.Dequeue(); + foreach (var reference in assembly.RawReferences) + { + cancellationToken.ThrowIfCancellationRequested(); + + var existingAssembly = context.Find(reference); + var toAssembly = existingAssembly ?? context.Resolve(reference); + + if (ShouldNotAnalyzeReference(options.AnalyzeMode, toAssembly)) + { + continue; + } + + // Create a reference to the assembly + var assemblyRef = new AssemblyRef(assembly, toAssembly, reference.Version); + assembly.References.Add(assemblyRef); + toAssembly.ReferencedBy.Add(assemblyRef); + + if (ShouldEnqueue(context, options.AnalyzeMode, toAssembly)) + { + assembliesToVisit.Enqueue(toAssembly); + } + + if (existingAssembly == null) + { + context.Add(toAssembly); + } + } + } + } + + // Local system assemblies are always checked for .NET framework, the assumption is made that + // when an application ships with system assemblies this is done for a reason (ILSpy for example). + private static bool ShouldNotAnalyzeReference(AnalyzeMode analyzeMode, Assembly assembly) => + analyzeMode is not (AnalyzeMode.AppDirectSystem or AnalyzeMode.All) && + assembly.IsSystem && !assembly.IsLocalSystem; + + private static bool ShouldEnqueue(IContext context, AnalyzeMode analyzeMode, Assembly assembly) + { + var enqueueSystemAssembly = analyzeMode == AnalyzeMode.All || + !assembly.IsSystem || assembly.IsLocalSystem; + return assembly.RawReferences.Count > 0 && !context.Contains(assembly) && enqueueSystemAssembly; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Assemblies/UnreferencedAssembliesAnalyzer.cs b/src/RefScout.Analyzer/Analyzers/Assemblies/UnreferencedAssembliesAnalyzer.cs new file mode 100644 index 0000000..ad88e0a --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Assemblies/UnreferencedAssembliesAnalyzer.cs @@ -0,0 +1,87 @@ +using System; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using System.Reflection; +using System.Threading; +using RefScout.Analyzer.Context; +using RefScout.Analyzer.Helpers; + +namespace RefScout.Analyzer.Analyzers.Assemblies; + +internal class UnreferencedAssembliesAnalyzer : IAssemblyAnalyzer +{ + private readonly IFileSystem _fileSystem; + + public UnreferencedAssembliesAnalyzer(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + public void Analyze(IContext context, AnalyzerOptions options, CancellationToken cancellationToken) + { + foreach (var directory in context.Resolver.SearchDirectories) + { + if (!_fileSystem.Directory.Exists(directory)) + { + continue; + } + + FindUnreferencedInDirectory(context, directory, cancellationToken); + } + } + + private void FindUnreferencedInDirectory(IContext context, string directory, CancellationToken cancellationToken) + { + var directoryInfo = _fileSystem.DirectoryInfo.FromDirectoryName(directory); + var files = directoryInfo.GetFiles("*.dll", SearchOption.TopDirectoryOnly) + .Concat(directoryInfo.GetFiles("*.exe", SearchOption.TopDirectoryOnly)); + + foreach (var file in files) + { + cancellationToken.ThrowIfCancellationRequested(); + + var name = Path.GetFileNameWithoutExtension(file.FullName); + if (context.Contains(name) || ShouldExcludeName(name)) + { + continue; + } + + var identity = ReadAssemblyIdentity(file.FullName); + if (identity == null) + { + continue; + } + + context.Add(new Assembly(identity, file.FullName, AssemblySource.Local) + { + IsUnreferenced = true + }); + } + } + + private static AssemblyIdentity? ReadAssemblyIdentity(string fileName) + { + try + { + // Use System.Reflection.AssemblyName.GetAssemblyName rather than Mono.Cecil because it's a lot faster + var name = AssemblyName.GetAssemblyName(fileName); + if (name.Name == null || name.Version == null || MicrosoftHelper.IsSystemAssembly(name.Name)) + { + return null; + } + + var culture = name.CultureName ?? AssemblyIdentity.CultureNeutral; + var token = name.GetPublicKeyToken(); + var publicKeyToken = token == null ? PublicKeyToken.Empty : new PublicKeyToken(token); + return new AssemblyIdentity(name.Name, culture, publicKeyToken, name.Version); + } + catch + { + return null; + } + } + + private static bool ShouldExcludeName(string name) + => name.StartsWith("api-", StringComparison.Ordinal); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Compatibility/CompatibilityAnalyzer.cs b/src/RefScout.Analyzer/Analyzers/Compatibility/CompatibilityAnalyzer.cs new file mode 100644 index 0000000..91e4586 --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Compatibility/CompatibilityAnalyzer.cs @@ -0,0 +1,57 @@ +using System.Linq; +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Analyzers.Compatibility; + +internal class CompatibilityAnalyzer : ICompatibilityAnalyzer +{ + private static readonly string[] IgnoreArchitectureMismatch = + { + "mscorlib", + "PresentationCore", + "System.Data", + "System.Data.OracleClient", + "System.EnterpriseServices", + "System.Printing", + "System.Transactions", + "System.Web" + }; + + private readonly IContext _context; + protected readonly IVersionComparer Comparer; + + protected CompatibilityAnalyzer(IContext context, IVersionComparer comparer) + { + _context = context; + Comparer = comparer; + } + + public void Analyze() + { + foreach (var assembly in _context.Assemblies) + { + foreach (var reference in assembly.References) + { + CheckReference(reference); + } + + CheckAssembly(assembly); + } + } + + protected virtual void CheckAssembly(Assembly assembly) + { + assembly.IsArchitectureMismatch = IsArchitectureMismatch(assembly, _context.EntryPoint); + } + + protected virtual void CheckReference(AssemblyRef reference) + { + reference.Compatibility = Comparer.Compare(reference.ActualVersion, reference.To, false); + } + + private static bool IsArchitectureMismatch(Assembly assembly, Assembly entryAssembly) => + assembly.ProcessorArchitecture != ProcessorArchitecture.Unknown && + assembly.ProcessorArchitecture != ProcessorArchitecture.Cil && + assembly.ProcessorArchitecture != entryAssembly.ProcessorArchitecture && + !IgnoreArchitectureMismatch.Contains(assembly.Name) && !assembly.IsSystem; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Compatibility/CompatibilityAnalyzerFactory.cs b/src/RefScout.Analyzer/Analyzers/Compatibility/CompatibilityAnalyzerFactory.cs new file mode 100644 index 0000000..39a770f --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Compatibility/CompatibilityAnalyzerFactory.cs @@ -0,0 +1,19 @@ +using System; +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Analyzers.Compatibility; + +internal class CompatibilityAnalyzerFactory : ICompatibilityAnalyzerFactory +{ + public CompatibilityAnalyzer Create(IContext context, IVersionComparer comparer) + { + return context switch + { + ICoreContext => new CoreCompatibilityAnalyzer(context, comparer), + ISharedFrameworkContext frameworkContext => new SharedFrameworkCompatibilityAnalyzer(frameworkContext, + comparer), + _ => throw new NotSupportedException( + $"Context type {context.GetType().Name} not supported for {GetType().Name}") + }; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Compatibility/CoreCompatibilityAnalyzer.cs b/src/RefScout.Analyzer/Analyzers/Compatibility/CoreCompatibilityAnalyzer.cs new file mode 100644 index 0000000..40daf9b --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Compatibility/CoreCompatibilityAnalyzer.cs @@ -0,0 +1,15 @@ +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Analyzers.Compatibility; + +internal class CoreCompatibilityAnalyzer : CompatibilityAnalyzer +{ + public CoreCompatibilityAnalyzer(IContext context, IVersionComparer comparer) : base(context, + comparer) { } + + + protected override void CheckReference(AssemblyRef reference) + { + reference.Compatibility = Comparer.Compare(reference.ActualVersion, reference.To, true); + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Compatibility/DefaultVersionComparer.cs b/src/RefScout.Analyzer/Analyzers/Compatibility/DefaultVersionComparer.cs new file mode 100644 index 0000000..8d07670 --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Compatibility/DefaultVersionComparer.cs @@ -0,0 +1,63 @@ +using System; + +namespace RefScout.Analyzer.Analyzers.Compatibility; + +public class DefaultVersionComparer : IVersionComparer +{ + private readonly VersionCompatibilityMode _systemVersionMode; + + public DefaultVersionComparer(VersionCompatibilityMode systemVersionMode) + { + _systemVersionMode = systemVersionMode; + } + + public ReferenceCompatibility Compare(Version fromVersion, Assembly toAssembly, bool ignoreStrongNamed) + { + _ = fromVersion ?? throw new ArgumentNullException(nameof(fromVersion)); + _ = toAssembly ?? throw new ArgumentNullException(nameof(toAssembly)); + + var toVersion = toAssembly.ActualVersion; + var strongNamed = toAssembly.IsStrongNamed && !ignoreStrongNamed; + + var isUnification = toAssembly.IsUnification && toAssembly.Source == AssemblySource.Gac; + var originalVersionCompatible = isUnification && toAssembly.OriginalVersion != null && + AreVersionsCompatible(fromVersion, toVersion, strongNamed); + if (AreVersionsCompatible(fromVersion, toVersion, strongNamed) || originalVersionCompatible) + { + return ReferenceCompatibility.Compatible; + } + + if (toAssembly.IsSystem || toAssembly.IsNetApi) + { + return _systemVersionMode switch + { + VersionCompatibilityMode.Loose => IsVersionHigher(fromVersion, toVersion) + ? ReferenceCompatibility.Mismatch + : ReferenceCompatibility.Compatible, + VersionCompatibilityMode.Strict => IsVersionHigher(fromVersion, toVersion) + ? ReferenceCompatibility.MismatchBreaking + : ReferenceCompatibility.Mismatch, + _ => ReferenceCompatibility.Compatible + }; + } + + return fromVersion.Major != toVersion.Major || strongNamed + ? ReferenceCompatibility.MismatchBreaking + : ReferenceCompatibility.Mismatch; + } + + private static bool IsVersionHigher(Version version1, Version version2) => + version1.Major > version2.Major + || version1.Major == version2.Major + && version1.Minor > version2.Minor; + + internal static bool AreVersionsCompatible(Version version1, Version version2, bool strict = false) + { + if (strict) + { + return version1 == version2; + } + + return version1.Major == version2.Major && version1.Minor == version2.Minor; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Compatibility/ICompatibilityAnalyzer.cs b/src/RefScout.Analyzer/Analyzers/Compatibility/ICompatibilityAnalyzer.cs new file mode 100644 index 0000000..bfaf9e9 --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Compatibility/ICompatibilityAnalyzer.cs @@ -0,0 +1,6 @@ +namespace RefScout.Analyzer.Analyzers.Compatibility; + +internal interface ICompatibilityAnalyzer +{ + void Analyze(); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Compatibility/ICompatibilityAnalyzerFactory.cs b/src/RefScout.Analyzer/Analyzers/Compatibility/ICompatibilityAnalyzerFactory.cs new file mode 100644 index 0000000..16fc4be --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Compatibility/ICompatibilityAnalyzerFactory.cs @@ -0,0 +1,8 @@ +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Analyzers.Compatibility; + +internal interface ICompatibilityAnalyzerFactory +{ + CompatibilityAnalyzer Create(IContext context, IVersionComparer comparer); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Compatibility/IVersionComparer.cs b/src/RefScout.Analyzer/Analyzers/Compatibility/IVersionComparer.cs new file mode 100644 index 0000000..af331c4 --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Compatibility/IVersionComparer.cs @@ -0,0 +1,22 @@ +using System; + +namespace RefScout.Analyzer.Analyzers.Compatibility; + +public interface IVersionComparer +{ + ReferenceCompatibility Compare(Version fromVersion, Assembly toAssembly, bool ignoredStrongNamed); +} + +public enum ReferenceCompatibility +{ + Compatible, + Mismatch, + MismatchBreaking +} + +public enum VersionCompatibilityMode +{ + Off, + Loose, + Strict +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Compatibility/SharedFrameworkCompatibilityAnalyzer.cs b/src/RefScout.Analyzer/Analyzers/Compatibility/SharedFrameworkCompatibilityAnalyzer.cs new file mode 100644 index 0000000..4615258 --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Compatibility/SharedFrameworkCompatibilityAnalyzer.cs @@ -0,0 +1,56 @@ +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Analyzers.Compatibility; + +internal class SharedFrameworkCompatibilityAnalyzer : CompatibilityAnalyzer +{ + private readonly ISharedFrameworkContext _context; + + public SharedFrameworkCompatibilityAnalyzer(ISharedFrameworkContext context, IVersionComparer comparer) : base( + context, + comparer) + { + _context = context; + } + + protected override void CheckReference(AssemblyRef reference) + { + reference.BindingRedirect = _context.FindBindingRedirect(reference.To.Name, reference.Version); + reference.CodeBase = + _context.FindCodeBase(reference.To, reference.BindingRedirect?.NewVersion ?? reference.Version); + + // Don't threat strong named assemblies as special for Silverlight applications, it runs in its own sandbox + var ignoreStrongNamed = reference.From.TargetFramework?.Runtime == NetRuntime.Silverlight; + reference.Compatibility = + Comparer.Compare(reference.ActualVersion, reference.To, ignoreStrongNamed); + + // Always run this after the version compatibility check + reference.BindingRedirectStatus = + FindBindingRedirectStatus(_context, reference, reference.Compatibility); + } + + private static BindingRedirectStatus FindBindingRedirectStatus( + ISharedFrameworkContext context, + AssemblyRef reference, + ReferenceCompatibility compatibility) + { + var allBindingRedirects = context.FindBindingRedirects(reference.To); + + // We don't care about binding redirects when: + // - there are no binding redirects + // - version is covered by binding redirect but it is redirected to the already referenced version + if (allBindingRedirects.Count == 0 || + reference.BindingRedirect?.NewVersion == reference.Version) + { + return BindingRedirectStatus.Default; + } + + if (reference.BindingRedirect != null && compatibility != ReferenceCompatibility.Compatible) + { + // Redirect worked, but redirected to wrong version (they are not compatible) + return BindingRedirectStatus.FailedWrongVersion; + } + + return reference.BindingRedirect != null ? BindingRedirectStatus.Success : BindingRedirectStatus.Failed; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Environment/Core/CoreRuntime.cs b/src/RefScout.Analyzer/Analyzers/Environment/Core/CoreRuntime.cs new file mode 100644 index 0000000..e890c27 --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Environment/Core/CoreRuntime.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; + +namespace RefScout.Analyzer.Analyzers.Environment.Core; + +public class CoreRuntime +{ + internal CoreRuntime(Version version, string versionName, string path) : + this(version, -1, -1, versionName, path, true) { } + + public CoreRuntime( + Version version, + int preview, + int previewBuild, + string versionName, + string path, + bool is64Bit) + { + Version = version; + Preview = preview; + PreviewBuild = previewBuild; + VersionName = versionName; + Path = path; + Is64Bit = is64Bit; + } + + public Version Version { get; } + public int Preview { get; } + public int PreviewBuild { get; } + public string VersionName { get; } + public string Path { get; } + public bool Is64Bit { get; } + + public bool IsPreview => Preview != -1; + + public HashSet Packs { get; } = new(); + + public override string ToString() => $"{VersionName}{(!Is64Bit ? " (x86)" : "")}"; + + public override bool Equals(object? obj) + { + if (obj is CoreRuntime other) + { + return Version == other.Version && Preview == other.Preview && PreviewBuild == other.PreviewBuild && + Is64Bit == other.Is64Bit; + } + + return false; + } + + public override int GetHashCode() => HashCode.Combine(Version, Preview, PreviewBuild, Is64Bit); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Environment/Core/CoreRuntimeAnalyzer.cs b/src/RefScout.Analyzer/Analyzers/Environment/Core/CoreRuntimeAnalyzer.cs new file mode 100644 index 0000000..654c08d --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Environment/Core/CoreRuntimeAnalyzer.cs @@ -0,0 +1,274 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using RefScout.Analyzer.Config.Core; +using RefScout.Analyzer.Helpers; +using RefScout.Core.Logging; + +namespace RefScout.Analyzer.Analyzers.Environment.Core; + +internal class CoreRuntimeAnalyzer : ICoreRuntimeAnalyzer +{ + private const string PackWindowsDesktop = "Microsoft.WindowsDesktop.App"; + private const string PackAspNetCore = "Microsoft.AspNetCore.App"; + private const string PackAspNetCoreAll = "Microsoft.AspNetCore.All"; + private const string PackDefault = "Microsoft.NETCore.App"; + + public static readonly string[] Packs = + { + PackDefault, + PackWindowsDesktop, + PackAspNetCore, + PackAspNetCoreAll + }; + + public static readonly IReadOnlyDictionary StringToPack = + new Dictionary + { + { PackDefault, RuntimePack.Default }, + { PackWindowsDesktop, RuntimePack.WindowsDesktop }, + { PackAspNetCore, RuntimePack.AspNetCore }, + { PackAspNetCoreAll, RuntimePack.AspNetCoreAll } + }; + + public static readonly IReadOnlyDictionary PackToString = + new Dictionary + { + { RuntimePack.Default, PackDefault }, + { RuntimePack.WindowsDesktop, PackWindowsDesktop }, + { RuntimePack.AspNetCore, PackAspNetCore }, + { RuntimePack.AspNetCoreAll, PackAspNetCoreAll } + }; + + + private readonly IEnvironment _environment; + private readonly IFileSystem _fileSystem; + + private readonly Lazy> _dotnetLocations; + + public CoreRuntimeAnalyzer(IEnvironment environment, IFileSystem fileSystem) + { + _environment = environment; + _fileSystem = fileSystem; + _dotnetLocations = + new Lazy>(() => InitDotNetLocations().ToList()); + } + + public CoreRuntime? FindRuntime( + Version requestedVersion, + RollForwardBehavior behavior = RollForwardBehavior.Minor, + bool? is64Bit = null) + { + var runtimeVersions = LocateAll(is64Bit); + + // Environment variable overrides runtime config + var environmentBehavior = _environment.GetEnvironmentVariable("DOTNET_ROLL_FORWARD"); + if (environmentBehavior != null && + Enum.TryParse(environmentBehavior, out RollForwardBehavior parsedBehavior)) + { + behavior = parsedBehavior; + } + + return FindRuntime(requestedVersion, runtimeVersions, behavior, is64Bit); + } + + public CoreRuntimeAnalyzerResult Analyze() => new(LocateAll()); + + public IReadOnlyList LocateAll(bool? is64Bit = null) + { + var allVersions = new HashSet(); + + foreach (var location in _dotnetLocations.Value) + { + if (is64Bit != null && location.is64Bit != is64Bit) + { + Logger.Info($"Skipping {(is64Bit == true ? "32" : "64")}-bit runtime location: {location.path}"); + continue; + } + + var sharedPath = Path.Combine(location.path, "shared"); + if (!_fileSystem.Directory.Exists(sharedPath)) + { + continue; + } + + foreach (var pack in Packs) + { + var packPath = Path.Combine(sharedPath, pack); + if (!_fileSystem.Directory.Exists(packPath)) + { + continue; + } + + var versions = _fileSystem.Directory.GetDirectories(packPath) + .Select(d => ConvertNameToRuntime(Path.GetFileName(d), sharedPath, location.is64Bit)) + .OfType(); + + foreach (var version in versions) + { + var existingVersion = allVersions.FirstOrDefault(v => v.Equals(version)); + if (existingVersion != null) + { + existingVersion.Packs.Add(StringToPack[pack]); + } + else + { + version.Packs.Add(StringToPack[pack]); + allVersions.Add(version); + } + } + } + } + + return allVersions.OrderBy(x => x.Version).ThenBy(x => x.Preview).ToList(); + } + + // Based on information found at, might not be identical due to vague wording in some parts: + // https://github.com/dotnet/runtime/blob/main/docs/design/features/framework-version-resolution.md#roll-forward + public CoreRuntime? FindRuntime( + Version requestedVersion, + IEnumerable availableRuntimes, + RollForwardBehavior behavior, + bool? is64Bit = null) + { + var runtimes = availableRuntimes.Where(x => is64Bit == null || x.Is64Bit == is64Bit).ToArray(); + if (behavior == RollForwardBehavior.LatestPatch) + { + return runtimes.Where(v => + v.Version.Major == requestedVersion.Major && v.Version.Minor == requestedVersion.Minor) + .OrderByDescending(v => v.Version.Build).FirstOrDefault(); + } + + if (behavior == RollForwardBehavior.Minor) + { + if (runtimes.Any(v => + v.Version.Major == requestedVersion.Major && v.Version.Minor == requestedVersion.Minor)) + { + return FindRuntime(requestedVersion, runtimes, RollForwardBehavior.LatestPatch); + } + + return runtimes + .Where(v => v.Version.Major == requestedVersion.Major && + v.Version.Minor > requestedVersion.Minor) + .OrderBy(v => v.Version.Minor).ThenByDescending(v => v.Version.Build).FirstOrDefault(); + } + + if (behavior == RollForwardBehavior.Major) + { + if (runtimes.Any(v => + v.Version.Major == requestedVersion.Major && v.Version.Minor >= requestedVersion.Minor)) + { + return FindRuntime(requestedVersion, runtimes, RollForwardBehavior.Minor); + } + + return runtimes + .Where(v => v.Version.Major > requestedVersion.Major) + .OrderBy(v => v.Version.Major) + .ThenBy(v => v.Version.Minor) + .ThenByDescending(v => v.Version.Build) + .FirstOrDefault(); + } + + if (behavior == RollForwardBehavior.LatestMinor) + { + return runtimes + .Where(v => v.Version.Major == requestedVersion.Major) + .OrderByDescending(v => v.Version.Minor) + .ThenByDescending(v => v.Version.Build) + .FirstOrDefault(); + } + + if (behavior == RollForwardBehavior.LatestMajor) + { + return runtimes.OrderByDescending(v => v.Version.Major) + .ThenByDescending(v => v.Version.Minor) + .ThenByDescending(v => v.Version.Build) + .FirstOrDefault(); + } + + return runtimes.FirstOrDefault(v => v.Version == requestedVersion); + } + + private static CoreRuntime? ConvertNameToRuntime(string name, string path, bool is64Bit) + { + try + { + var shortName = name; + var dashIndex = shortName.IndexOf('-'); + if (dashIndex > 0) + { + shortName = shortName.Remove(dashIndex); + } + + var previewIndex = Math.Max(name.IndexOf("preview.", StringComparison.Ordinal), + name.IndexOf("rc.", StringComparison.Ordinal)); + var previewVersion = -1; + var previewBuildVersion = -1; + if (previewIndex > 0) + { + var split = name[previewIndex..].Split('.'); + previewVersion = split.Length >= 1 ? int.Parse(split[1]) : previewVersion; + previewBuildVersion = split.Length >= 2 ? int.Parse(split[2]) : previewBuildVersion; + } + + if (Version.TryParse(shortName, out var parsedVersion)) + { + return new CoreRuntime(parsedVersion, previewVersion, previewBuildVersion, name, path, + is64Bit); + } + + Logger.Warn($"Invalid version format: {shortName}."); + return null; + } + catch + { + Logger.Warn($"Could not parse runtime version name: {name} in {path}."); + return null; + } + } + + private IEnumerable<(bool is64Bit, string path)> InitDotNetLocations() + { + // User can configure a custom dotnet root where dotnet is located, which is then prioritized + var dotnetRootX64 = _environment.GetEnvironmentVariable("DOTNET_ROOT"); + if (!string.IsNullOrWhiteSpace(dotnetRootX64)) + { + yield return (true, dotnetRootX64); + } + + var dotnetRootX86 = _environment.GetEnvironmentVariable("DOTNET_ROOT(x86)"); + if (!string.IsNullOrWhiteSpace(dotnetRootX86)) + { + yield return (false, dotnetRootX86); + } + + // TODO: This is not correct + if (!string.IsNullOrWhiteSpace(dotnetRootX64) || !string.IsNullOrWhiteSpace(dotnetRootX86)) + { + yield break; + } + + switch (_environment.OSVersion.Platform) + { + case PlatformID.Unix: + yield return (true, "/usr/share/dotnet/"); + break; + case PlatformID.MacOSX: + yield return (true, "/usr/local/share/dotnet/"); + break; + case PlatformID.Win32NT: + yield return (true, Path.Combine( + _environment.GetFolderPath(System.Environment.SpecialFolder.ProgramFiles), + "dotnet")); + yield return (false, Path.Combine( + _environment.GetFolderPath(System.Environment.SpecialFolder.ProgramFilesX86), + "dotnet")); + break; + default: + Logger.Warn("This platform is not supported by the .NET Core runtime analyzer."); + break; + } + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Environment/Core/CoreRuntimeAnalyzerResult.cs b/src/RefScout.Analyzer/Analyzers/Environment/Core/CoreRuntimeAnalyzerResult.cs new file mode 100644 index 0000000..cd6a562 --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Environment/Core/CoreRuntimeAnalyzerResult.cs @@ -0,0 +1,5 @@ +using System.Collections.Generic; + +namespace RefScout.Analyzer.Analyzers.Environment.Core; + +public record CoreRuntimeAnalyzerResult(IReadOnlyList Runtimes); \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Environment/Core/ICoreRuntimeAnalyzer.cs b/src/RefScout.Analyzer/Analyzers/Environment/Core/ICoreRuntimeAnalyzer.cs new file mode 100644 index 0000000..15e0b1e --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Environment/Core/ICoreRuntimeAnalyzer.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using RefScout.Analyzer.Config.Core; + +namespace RefScout.Analyzer.Analyzers.Environment.Core; + +internal interface ICoreRuntimeAnalyzer : IRuntimeAnalyzer +{ + CoreRuntime? FindRuntime( + Version requestedVersion, + RollForwardBehavior behavior = RollForwardBehavior.Minor, + bool? is64Bit = null); + + CoreRuntime? FindRuntime( + Version requestedVersion, + IEnumerable availableRuntimes, + RollForwardBehavior behavior, + bool? is64Bit = null); + + IReadOnlyList LocateAll(bool? is64Bit = null); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Environment/Core/RuntimePack.cs b/src/RefScout.Analyzer/Analyzers/Environment/Core/RuntimePack.cs new file mode 100644 index 0000000..1f94934 --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Environment/Core/RuntimePack.cs @@ -0,0 +1,9 @@ +namespace RefScout.Analyzer.Analyzers.Environment.Core; + +public enum RuntimePack +{ + Default, + WindowsDesktop, + AspNetCore, + AspNetCoreAll +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Environment/EnvironmentAnalyzer.cs b/src/RefScout.Analyzer/Analyzers/Environment/EnvironmentAnalyzer.cs new file mode 100644 index 0000000..d5b27cb --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Environment/EnvironmentAnalyzer.cs @@ -0,0 +1,52 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO.Abstractions; +using RefScout.Analyzer.Analyzers.Environment.Core; +using RefScout.Analyzer.Analyzers.Environment.Framework; +using RefScout.Analyzer.Analyzers.Environment.Mono; +using RefScout.Analyzer.Helpers; + +namespace RefScout.Analyzer.Analyzers.Environment; + +// TODO: Refactor EnvironmentAnalyzer entirely at some point +internal class EnvironmentAnalyzer : IEnvironmentAnalyzer +{ + private readonly IEnvironment _environment; + + [ExcludeFromCodeCoverage] + public EnvironmentAnalyzer(IEnvironment environment, IFileSystem fileSystem) : this(environment, + new CoreRuntimeAnalyzer(environment, fileSystem), + new FrameworkRuntimeAnalyzer(), + new MonoRuntimeAnalyzer(environment, fileSystem)) { } + + public EnvironmentAnalyzer( + IEnvironment environment, + ICoreRuntimeAnalyzer coreRuntimeAnalyzer, + IFrameworkRuntimeAnalyzer frameworkRuntimeAnalyzer, + IMonoRuntimeAnalyzer monoRuntimeAnalyzer) + { + _environment = environment; + CoreRuntimeAnalyzer = coreRuntimeAnalyzer; + FrameworkRuntimeAnalyzer = frameworkRuntimeAnalyzer; + MonoRuntimeAnalyzer = monoRuntimeAnalyzer; + } + + public ICoreRuntimeAnalyzer CoreRuntimeAnalyzer { get; } + + public IFrameworkRuntimeAnalyzer FrameworkRuntimeAnalyzer { get; } + public IMonoRuntimeAnalyzer MonoRuntimeAnalyzer { get; } + + public EnvironmentInfo Analyze() + { + var framework = _environment.OSVersion.Platform == PlatformID.Win32NT + ? FrameworkRuntimeAnalyzer.Analyze() + : null; + + return new EnvironmentInfo + { + Core = CoreRuntimeAnalyzer.Analyze(), + Framework = framework, + Mono = MonoRuntimeAnalyzer.Analyze() + }; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Environment/EnvironmentInfo.cs b/src/RefScout.Analyzer/Analyzers/Environment/EnvironmentInfo.cs new file mode 100644 index 0000000..5b5e7ce --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Environment/EnvironmentInfo.cs @@ -0,0 +1,10 @@ +using RefScout.Analyzer.Analyzers.Environment.Core; +using RefScout.Analyzer.Analyzers.Environment.Framework; +using RefScout.Analyzer.Analyzers.Environment.Mono; + +namespace RefScout.Analyzer.Analyzers.Environment; + +public record EnvironmentInfo( + CoreRuntimeAnalyzerResult? Core = null, + FrameworkRuntimeAnalyzerResult? Framework = null, + MonoRuntimeAnalyzerResult? Mono = null); \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Environment/Framework/FrameworkRuntime.cs b/src/RefScout.Analyzer/Analyzers/Environment/Framework/FrameworkRuntime.cs new file mode 100644 index 0000000..6830b41 --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Environment/Framework/FrameworkRuntime.cs @@ -0,0 +1,7 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace RefScout.Analyzer.Analyzers.Environment.Framework; + +[ExcludeFromCodeCoverage] +public record FrameworkRuntime(Version RuntimeVersion, Version Version, int ServicePack = -1); \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Environment/Framework/FrameworkRuntimeAnalyzer.cs b/src/RefScout.Analyzer/Analyzers/Environment/Framework/FrameworkRuntimeAnalyzer.cs new file mode 100644 index 0000000..9cd1705 --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Environment/Framework/FrameworkRuntimeAnalyzer.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Runtime.Versioning; +using Microsoft.Win32; + +namespace RefScout.Analyzer.Analyzers.Environment.Framework; + +// I am definitely not going to test the registry, this art is copied from MSDN anyways +[ExcludeFromCodeCoverage] +internal class FrameworkRuntimeAnalyzer : IFrameworkRuntimeAnalyzer +{ + [SupportedOSPlatform("windows")] + public FrameworkRuntimeAnalyzerResult Analyze() => new(LocateRuntimes()); + + [SupportedOSPlatform("windows")] + public FrameworkRuntime? FindRuntime(IEnumerable targets) + { + var runtimes = LocateRuntimes().OrderByDescending(t => t.Version).ToList(); + + foreach (var target in targets) + { + foreach (var runtime in runtimes) + { + var targetsClr2 = target.Version.Major == 2 && target.Version.Minor == 0; + if (targetsClr2 && target.Version.Major == runtime.RuntimeVersion.Major && + target.Version.Minor == runtime.RuntimeVersion.Minor) + { + return runtime; + } + + if (!targetsClr2 && target.Version <= runtime.Version) + { + return runtime; + } + } + } + + return null; + } + + [SupportedOSPlatform("windows")] + private static IReadOnlyList LocateRuntimes() + { + var runtimes = GetInstalledRuntimes().ToList(); + var runtimeAfter45 = GetFrameworkVersionAfter45(); + if (runtimeAfter45 == null) + { + return runtimes; + } + + // Version later than 4.5 installed, so eliminate the previously found 4.0 from registry + runtimes.RemoveAt(runtimes.FindIndex(v => v.RuntimeVersion.Major == 4)); + runtimes.Add(runtimeAfter45); + return runtimes; + } + + [SupportedOSPlatform("windows")] + private static FrameworkRuntime? GetFrameworkVersionAfter45() + { + using var ndpKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32) + .OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\"); + return ndpKey?.GetValue("Release") != null + ? CheckFor45PlusVersion(ndpKey.GetValue("Release") as int?) + : null; + } + + private static FrameworkRuntime? CheckFor45PlusVersion(int? releaseKey) + { + var versionString = releaseKey switch + { + >= 528040 => "4.8", + >= 461808 => "4.7.2", + >= 461308 => "4.7.1", + >= 460798 => "4.7", + >= 394802 => "4.6.2", + >= 394254 => "4.6.1", + >= 393295 => "4.6", + >= 379893 => "4.5.2", + >= 378675 => "4.5.1", + >= 378389 => "4.5", + _ => null + }; + + return versionString != null + ? new FrameworkRuntime(GetRuntimeVersion("v4.0"), Version.Parse(versionString)) + : null; + } + + [SupportedOSPlatform("windows")] + private static IEnumerable GetInstalledRuntimes() + { + var runtimes = new List(); + + using var ndpKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32) + .OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\"); + if (ndpKey == null) + { + return runtimes; + } + + foreach (var versionKeyName in ndpKey.GetSubKeyNames()) + { + // Skip .NET Framework 4.5 version information. + if (versionKeyName == "v4" || !versionKeyName.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var versionKey = ndpKey.OpenSubKey(versionKeyName); + if (versionKey == null) + { + continue; + } + + var version = versionKey.GetValue("Version", "") as string; + var sp = versionKey.GetValue("SP", "")?.ToString(); + var install = versionKey.GetValue("Install", "")?.ToString(); + + // 1 = installed + if (install == "1" && !string.IsNullOrEmpty(version)) + { + _ = int.TryParse(sp, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var servicePack); + runtimes.Add(new FrameworkRuntime(GetRuntimeVersion(versionKeyName), + Version.Parse(version), servicePack)); + } + + if (!string.IsNullOrEmpty(version)) + { + continue; + } + + foreach (var subKeyName in versionKey.GetSubKeyNames()) + { + var subKey = versionKey.OpenSubKey(subKeyName); + var subVersion = subKey?.GetValue("Version", "") as string; + if (subKey == null || string.IsNullOrEmpty(subVersion)) + { + continue; + } + + sp = subKey.GetValue("SP", "")?.ToString(); + install = subKey.GetValue("Install", "")?.ToString(); + if (install != "1") + { + continue; + } + + _ = int.TryParse(sp, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var servicePack); + runtimes.Add(new FrameworkRuntime(GetRuntimeVersion(versionKeyName), + Version.Parse(subVersion), servicePack)); + } + } + + return runtimes; + } + + private static Version GetRuntimeVersion(string versionString) + { + var version = Version.Parse(versionString.TrimStart('v')); + return version.Major switch + { + 1 when version.Minor == 0 => new Version(1, 0, 3705), + 1 when version.Minor == 1 => new Version(1, 1, 4322), + >= 2 and <= 3 => new Version(2, 0, 50727), + _ => new Version(4, 0, 30319) + }; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Environment/Framework/FrameworkRuntimeAnalyzerResult.cs b/src/RefScout.Analyzer/Analyzers/Environment/Framework/FrameworkRuntimeAnalyzerResult.cs new file mode 100644 index 0000000..8a0debe --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Environment/Framework/FrameworkRuntimeAnalyzerResult.cs @@ -0,0 +1,5 @@ +using System.Collections.Generic; + +namespace RefScout.Analyzer.Analyzers.Environment.Framework; + +public record FrameworkRuntimeAnalyzerResult(IReadOnlyList Runtimes); \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Environment/Framework/IFrameworkRuntimeAnalyzer.cs b/src/RefScout.Analyzer/Analyzers/Environment/Framework/IFrameworkRuntimeAnalyzer.cs new file mode 100644 index 0000000..a5b7c52 --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Environment/Framework/IFrameworkRuntimeAnalyzer.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace RefScout.Analyzer.Analyzers.Environment.Framework; + +internal interface IFrameworkRuntimeAnalyzer : IRuntimeAnalyzer +{ + FrameworkRuntime? FindRuntime(IEnumerable targets); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Environment/IEnvironmentAnalyzer.cs b/src/RefScout.Analyzer/Analyzers/Environment/IEnvironmentAnalyzer.cs new file mode 100644 index 0000000..fb91f33 --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Environment/IEnvironmentAnalyzer.cs @@ -0,0 +1,14 @@ +using RefScout.Analyzer.Analyzers.Environment.Core; +using RefScout.Analyzer.Analyzers.Environment.Framework; +using RefScout.Analyzer.Analyzers.Environment.Mono; + +namespace RefScout.Analyzer.Analyzers.Environment; + +internal interface IEnvironmentAnalyzer +{ + ICoreRuntimeAnalyzer CoreRuntimeAnalyzer { get; } + IFrameworkRuntimeAnalyzer FrameworkRuntimeAnalyzer { get; } + IMonoRuntimeAnalyzer MonoRuntimeAnalyzer { get; } + + EnvironmentInfo Analyze(); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Environment/IRuntimeAnalyzer.cs b/src/RefScout.Analyzer/Analyzers/Environment/IRuntimeAnalyzer.cs new file mode 100644 index 0000000..bd621e6 --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Environment/IRuntimeAnalyzer.cs @@ -0,0 +1,6 @@ +namespace RefScout.Analyzer.Analyzers.Environment; + +internal interface IRuntimeAnalyzer +{ + TRuntime Analyze(); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Environment/Mono/IMonoRuntimeAnalyzer.cs b/src/RefScout.Analyzer/Analyzers/Environment/Mono/IMonoRuntimeAnalyzer.cs new file mode 100644 index 0000000..b1ecffe --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Environment/Mono/IMonoRuntimeAnalyzer.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace RefScout.Analyzer.Analyzers.Environment.Mono; + +internal interface IMonoRuntimeAnalyzer : IRuntimeAnalyzer +{ + IEnumerable GetRuntimePrefixDirectories(); + + IEnumerable GetGacPrefixDirectories(); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Analyzers/Environment/Mono/MonoRuntimeAnalyzer.cs b/src/RefScout.Analyzer/Analyzers/Environment/Mono/MonoRuntimeAnalyzer.cs new file mode 100644 index 0000000..dcc1c66 --- /dev/null +++ b/src/RefScout.Analyzer/Analyzers/Environment/Mono/MonoRuntimeAnalyzer.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.IO.MemoryMappedFiles; +using System.Text; +using RefScout.Analyzer.Helpers; + +namespace RefScout.Analyzer.Analyzers.Environment.Mono; + +internal class MonoRuntimeAnalyzer : IMonoRuntimeAnalyzer +{ + private static readonly byte[] ResgenVersionPattern = + { + 0x4D, 0x00, 0x6F, 0x00, 0x6E, 0x00, 0x6F, 0x00, 0x20, 0x00, 0x52, 0x00, + 0x65, 0x00, 0x73, 0x00, 0x6F, 0x00, 0x75, 0x00, 0x72, 0x00, 0x63, 0x00, + 0x65, 0x00, 0x20, 0x00, 0x47, 0x00, 0x65, 0x00, 0x6E, 0x00, 0x65, 0x00, + 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6F, 0x00, 0x72, 0x00, 0x20, 0x00, + 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x73, 0x00, 0x69, 0x00, 0x6F, 0x00, + 0x6E, 0x00, 0x20, 0x00 + }; + + public static readonly string[] PossibleVersionNames = + { "4.5", "4.0", "3.5", "3.0", "2.1", "2.0", "1.1", "1.0" }; + + private readonly IEnvironment _environment; + private readonly IFileSystem _fileSystem; + + public MonoRuntimeAnalyzer(IEnvironment environment, IFileSystem fileSystem) + { + _environment = environment; + _fileSystem = fileSystem; + } + + public MonoRuntimeAnalyzerResult Analyze() + { + var runtimes = new List(); + + var prefixDirectories = GetRuntimePrefixDirectories(); + foreach (var prefixDirectory in prefixDirectories) + { + var monoLibPath = Path.Combine(prefixDirectory, "lib", "mono"); + Version? version = null; + var frameworkVersions = new List(); + + foreach (var versionName in PossibleVersionNames) + { + var versionPath = Path.Combine(monoLibPath, versionName); + var resgenFileName = Path.Combine(versionPath, "resgen.exe"); + if (!_fileSystem.File.Exists(Path.Combine(resgenFileName))) + { + continue; + } + + version ??= DetermineMonoVersionFromResgen(resgenFileName); + frameworkVersions.Add(Version.Parse(versionName)); + } + + + if (frameworkVersions.Count > 0) + { + runtimes.Add(new MonoRuntime(version, prefixDirectory, frameworkVersions)); + } + } + + return new MonoRuntimeAnalyzerResult(runtimes); + } + + public IEnumerable GetRuntimePrefixDirectories() => GetPrefixDirectories("MONO_PATH"); + + public IEnumerable GetGacPrefixDirectories() => GetPrefixDirectories("MONO_GAC_PREFIX"); + + // Scans resgen.exe for a pattern to reliably determine the mono version + public static Version? DetermineMonoVersionFromResgen(string fileName) + { + const char lineFeed = (char)10; // Modern mono versions + const char space = (char)32; // Old mono versions + + using var memoryMappedFile = + MemoryMappedFile.CreateFromFile(fileName, FileMode.Open, null, 0, MemoryMappedFileAccess.Read); + using var accessor = + memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read); + + var offset = BinaryKmpSearch.SearchInFile(accessor, ResgenVersionPattern); + if (offset == -1) + { + return null; + } + + var versionOffset = offset + ResgenVersionPattern.Length; + var sb = new StringBuilder(); + while (true) + { + var asciiChar = (char)accessor.ReadInt16(versionOffset); + if (asciiChar is lineFeed or space) + { + break; + } + + sb.Append(asciiChar); + versionOffset += 2; + } + + return Version.TryParse(sb.ToString(), out var version) ? version : null; + } + + private IEnumerable GetPrefixDirectories(string environmentVariable) + { + const char monoPathSeparator = ':'; + + var monoPathVariable = _environment.GetEnvironmentVariable(environmentVariable); + if (!string.IsNullOrEmpty(monoPathVariable)) + { + foreach (var path in monoPathVariable.Split(monoPathSeparator)) + { + yield return path; + } + + yield break; + } + + // Should probably iterate path variable to find the wanted location, but who uses Mono -> not important + switch (_environment.OSVersion.Platform) + { + // TODO: this currently only modern Mono installations, older versions use folder names such as 'Mono-2.0', 'Mono-3.2.3' + case PlatformID.Win32NT: + yield return Path.Combine(_environment.GetFolderPath(System.Environment.SpecialFolder.ProgramFiles), + "Mono"); + yield return Path.Combine(_environment.GetFolderPath(System.Environment.SpecialFolder.ProgramFilesX86), + "Mono"); + break; + case PlatformID.MacOSX: + yield return "/Library/Frameworks/Mono.framework/Versions/Current/"; + break; + case PlatformID.Unix: + yield return "/usr/"; + break; + } + } +} + +public record MonoRuntimeAnalyzerResult(IReadOnlyList Runtimes); + +public record MonoRuntime( + Version? Version, + string Path, + IReadOnlyList FrameworkVersions); \ No newline at end of file diff --git a/src/RefScout.Analyzer/Assembly.cs b/src/RefScout.Analyzer/Assembly.cs new file mode 100644 index 0000000..42e4e1c --- /dev/null +++ b/src/RefScout.Analyzer/Assembly.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using RefScout.Analyzer.Config.Framework; +using RefScout.Analyzer.Helpers; +using RefScout.Analyzer.Notes; + +namespace RefScout.Analyzer; + +public record Assembly : AssemblyIdentity +{ + private readonly List _notes; + + public Assembly(AssemblyIdentity identity, string? path, AssemblySource source) + : base(identity.Name, identity.Culture, identity.PublicKeyToken, identity.Version) + { + _notes = new List(); + RawReferences = Array.Empty(); + References = new List(); + ReferencedBy = new List(); + BindingRedirects = Array.Empty(); + + OriginalVersion = identity.Version; + Path = path; + Source = source; + + IsSystem = MicrosoftHelper.IsSystemAssembly(Name); + IsNetApi = MicrosoftHelper.IsNetApi(Name); + } + + public Assembly(AssemblyIdentity identity, AssemblySource source) + : this(identity, null, source) { } + + public override string Id => + $"{Name},{(IsUnification && OriginalVersion != null ? OriginalVersion : Version)},{Culture},{PublicKeyToken}"; + + public override string FullName => + $"{Name}, Version={(IsUnification && OriginalVersion != null ? OriginalVersion : Version)}, Culture={Culture}, PublicKeyToken={PublicKeyToken}"; + + public bool IsEntryPoint { get; init; } + public bool IsArchitectureMismatch { get; internal set; } + + public IReadOnlyList RawReferences { get; internal set; } + public string? ProcessorArchitectureString { get; internal set; } + public ProcessorArchitecture ProcessorArchitecture { get; internal set; } + public bool Is64Bit { get; internal set; } + public TargetFramework? TargetFramework { get; internal set; } + public AssemblySourceLanguage SourceLanguage { get; internal set; } = AssemblySourceLanguage.Unknown; + public AssemblyKind Kind { get; internal set; } = AssemblyKind.Dll; + + public bool IsSystem { get; init; } + public bool IsLocalSystem { get; internal set; } + public bool IsNetApi { get; init; } + + public Version? OriginalVersion { get; init; } + public Version ActualVersion => IsUnification && OriginalVersion != null ? OriginalVersion : Version; + public bool IsUnification { get; init; } + + public string? Path { get; } + public CodeBase? CodeBase { get; init; } + public IReadOnlyList BindingRedirects { get; internal set; } + + public bool IsUnreferenced { get; init; } + public bool HasNotes => _notes.Count > 0; + public IReadOnlyList Notes => _notes; + + public NoteLevel Level => + _notes.Count > 0 ? (NoteLevel)_notes.Max(a => (int)a.Type) : NoteLevel.Default; + + public List References { get; internal set; } + public List ReferencedBy { get; internal set; } + + public AssemblySource Source { get; init; } + + public void AddNote(NoteType type, string message) + { + _ = message ?? throw new ArgumentNullException(nameof(message)); + _notes.Add(new ConflictNote(type, message)); + } +} + +public enum AssemblySource +{ + Local, + NotFound, + Error, + Gac, + Shared, + CodeBase +} + +public enum AssemblySourceLanguage +{ + CSharp = 1, + VbNet = 2, + FSharp = 3, + CppCli = 4, + Unknown = 5 +} + +public enum ProcessorArchitecture +{ + Unknown, + Cil, + Amd64, + Arm, + Arm64, + Ia64, + X86 +} + +public enum AssemblyKind +{ + Dll, + Web, + Console, + Windows +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/AssemblyIdentity.cs b/src/RefScout.Analyzer/AssemblyIdentity.cs new file mode 100644 index 0000000..073fdf5 --- /dev/null +++ b/src/RefScout.Analyzer/AssemblyIdentity.cs @@ -0,0 +1,26 @@ +using System; + +namespace RefScout.Analyzer; + +public record AssemblyIdentity(string Name, string Culture, PublicKeyToken PublicKeyToken, Version Version) +{ + public const string CultureNeutral = "neutral"; + + private static readonly Version ZeroVersion = new(0, 0); + + public AssemblyIdentity(string name, string culture, PublicKeyToken publicKeyToken) : this(name, culture, + publicKeyToken, ZeroVersion) { } + + public AssemblyIdentity(string name) : this(name, CultureNeutral, PublicKeyToken.Empty, ZeroVersion) { } + + public bool IsWindowsRuntime { get; init; } + + public virtual string Id => $"{Name},{Version},{Culture},{PublicKeyToken}"; + + public virtual string FullName => + Version != ZeroVersion + ? $"{Name}, Version={Version}, Culture={Culture}, PublicKeyToken={PublicKeyToken}" + : $"{Name}, Culture={Culture}, PublicKeyToken={PublicKeyToken}"; + + public bool IsStrongNamed => PublicKeyToken != PublicKeyToken.Empty; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/AssemblyRef.cs b/src/RefScout.Analyzer/AssemblyRef.cs new file mode 100644 index 0000000..379eb04 --- /dev/null +++ b/src/RefScout.Analyzer/AssemblyRef.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using RefScout.Analyzer.Analyzers.Compatibility; +using RefScout.Analyzer.Config.Framework; +using RefScout.Analyzer.Notes; + +namespace RefScout.Analyzer; + +public record AssemblyRef(Assembly From, Assembly To, Version Version) +{ + private readonly List _notes = new(); + + // ActualVersion means version with binding redirect applied + public Version ActualVersion => BindingRedirect?.NewVersion ?? Version; + public BindingRedirect? BindingRedirect { get; set; } + public CodeBase? CodeBase { get; set; } + + public ReferenceCompatibility Compatibility { get; internal set; } + public BindingRedirectStatus BindingRedirectStatus { get; internal set; } + + public bool HasNotes => _notes.Count > 0; + public IReadOnlyList Notes => _notes; + + public NoteLevel Level => + _notes.Count > 0 ? (NoteLevel)_notes.Max(a => (int)a.Type) : NoteLevel.Default; + + public void AddNote(NoteType type, string message) + { + _ = message ?? throw new ArgumentNullException(nameof(message)); + _notes.Add(new ConflictNote(type, message)); + } +} + +public enum BindingRedirectStatus +{ + Default, + Failed, + FailedWrongVersion, + Success +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Config/ConfigError.cs b/src/RefScout.Analyzer/Config/ConfigError.cs new file mode 100644 index 0000000..9decfb6 --- /dev/null +++ b/src/RefScout.Analyzer/Config/ConfigError.cs @@ -0,0 +1,3 @@ +namespace RefScout.Analyzer.Config; + +public record ConfigError(string Message, int LineNumber = -1); \ No newline at end of file diff --git a/src/RefScout.Analyzer/Config/Core/CoreConfig.cs b/src/RefScout.Analyzer/Config/Core/CoreConfig.cs new file mode 100644 index 0000000..01092eb --- /dev/null +++ b/src/RefScout.Analyzer/Config/Core/CoreConfig.cs @@ -0,0 +1,23 @@ +using System; +using RefScout.Analyzer.Analyzers.Environment.Core; + +namespace RefScout.Analyzer.Config.Core; + +public class CoreConfig : IConfig +{ + public CoreConfig(RuntimeConfig runtimeConfig, DepsFile depsFile) + { + RuntimeConfig = runtimeConfig; + DepsFile = depsFile; + } + + public RuntimePack RuntimePack { get; init; } + public RollForwardBehavior RollForward { get; init; } + public bool SelfContained { get; init; } + public Version? TargetRuntimeVersion { get; set; } + + public RuntimeConfig RuntimeConfig { get; } + public DepsFile DepsFile { get; } + + public IConfigErrorReport ErrorReport { get; } = new CoreConfigErrorReport(); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Config/Core/CoreConfigErrorReport.cs b/src/RefScout.Analyzer/Config/Core/CoreConfigErrorReport.cs new file mode 100644 index 0000000..0e09a7c --- /dev/null +++ b/src/RefScout.Analyzer/Config/Core/CoreConfigErrorReport.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace RefScout.Analyzer.Config.Core; + +internal class CoreConfigErrorReport : IConfigErrorReport +{ + public IReadOnlyList Errors { get; } = Array.Empty(); + + public override string ToString() => "Core config validation not implemented."; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Config/Core/CoreConfigParser.cs b/src/RefScout.Analyzer/Config/Core/CoreConfigParser.cs new file mode 100644 index 0000000..8cadf0c --- /dev/null +++ b/src/RefScout.Analyzer/Config/Core/CoreConfigParser.cs @@ -0,0 +1,76 @@ +using System; +using System.IO; +using System.IO.Abstractions; +using System.Text.Json; +using System.Text.Json.Serialization; +using RefScout.Analyzer.Analyzers.Environment.Core; +using RefScout.Analyzer.Helpers; +using RefScout.Core.Logging; + +namespace RefScout.Analyzer.Config.Core; + +internal class CoreConfigParser : IConfigParser +{ + private static readonly JsonSerializerOptions JsonSerializerOptions = new() + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = + { + new JsonStringEnumConverter(), + new VersionJsonConverter() + } + }; + + private readonly IFileSystem _fileSystem; + + public CoreConfigParser(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + public CoreConfig ParseFile(string assemblyFileName, string? configFileName) + { + var basePath = Path.GetDirectoryName(assemblyFileName); + var runtimeConfigFileName = Path.Combine(basePath!, + Path.GetFileNameWithoutExtension(assemblyFileName) + ".runtimeconfig.json"); + var depsFileName = + Path.Combine(basePath!, Path.GetFileNameWithoutExtension(assemblyFileName) + ".deps.json"); + var runtimeConfig = ReadJson(runtimeConfigFileName) ?? new RuntimeConfig(); + var depsFile = ReadJson(depsFileName) ?? new DepsFile(); + + var packName = runtimeConfig.RuntimeOptions?.Framework?.Name; + var pack = RuntimePack.Default; + if (packName != null) + { + _ = CoreRuntimeAnalyzer.StringToPack.TryGetValue(packName, out pack); + } + + return new CoreConfig(runtimeConfig, depsFile) + { + SelfContained = runtimeConfig.RuntimeOptions?.IncludedFrameworks.Count > 0, + RollForward = runtimeConfig.RuntimeOptions?.RollForward ?? RollForwardBehavior.Minor, + RuntimePack = pack, + TargetRuntimeVersion = runtimeConfig.RuntimeOptions?.Framework?.Version + }; + } + + private T? ReadJson(string fileName) where T : class + { + if (!_fileSystem.File.Exists(fileName)) + { + Logger.Info($"JSON configuration file does not exist: {fileName}"); + return null; + } + + try + { + return JsonSerializer.Deserialize(_fileSystem.File.ReadAllText(fileName), JsonSerializerOptions); + } + catch (Exception e) + { + Logger.Error(e, $"Error while trying to parse JSON configuration: {fileName}"); + return null; + } + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Config/Core/DepsFile.cs b/src/RefScout.Analyzer/Config/Core/DepsFile.cs new file mode 100644 index 0000000..aea39f0 --- /dev/null +++ b/src/RefScout.Analyzer/Config/Core/DepsFile.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace RefScout.Analyzer.Config.Core; + +[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] +[SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")] +public class DepsFile +{ + public Dictionary>? Targets { get; set; } + public Dictionary? Libraries { get; set; } + + public class RuntimeInfo + { + public Dictionary? Runtime { get; set; } + } + + public class Library + { + public string? Type { get; set; } + public string? Path { get; set; } + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Config/Core/RuntimeConfig.cs b/src/RefScout.Analyzer/Config/Core/RuntimeConfig.cs new file mode 100644 index 0000000..20ff645 --- /dev/null +++ b/src/RefScout.Analyzer/Config/Core/RuntimeConfig.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +// ReSharper disable UnusedAutoPropertyAccessor.Global + +namespace RefScout.Analyzer.Config.Core; + +public class RuntimeConfig +{ + public RuntimeOptions? RuntimeOptions { get; [ExcludeFromCodeCoverage] set; } +} + +public class RuntimeOptions +{ + public RollForwardBehavior RollForward { get; set; } + public RuntimeFramework? Framework { get; set; } + + public IReadOnlyList IncludedFrameworks { get; set; } = Array.Empty(); +} + +public class RuntimeFramework +{ + public string? Name { get; set; } + public Version? Version { get; set; } +} + +public enum RollForwardBehavior +{ + Minor, + Major, + LatestPatch, + LatestMinor, + LatestMajor, + Disable +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Config/Framework/BindingIdentity.cs b/src/RefScout.Analyzer/Config/Framework/BindingIdentity.cs new file mode 100644 index 0000000..2f9eb10 --- /dev/null +++ b/src/RefScout.Analyzer/Config/Framework/BindingIdentity.cs @@ -0,0 +1,6 @@ +namespace RefScout.Analyzer.Config.Framework; + +public record BindingIdentity + (string Name, string Culture, PublicKeyToken Token, bool IsMachineConfig = false) : AssemblyIdentity(Name, + Culture, + Token); \ No newline at end of file diff --git a/src/RefScout.Analyzer/Config/Framework/BindingRedirect.cs b/src/RefScout.Analyzer/Config/Framework/BindingRedirect.cs new file mode 100644 index 0000000..890ec01 --- /dev/null +++ b/src/RefScout.Analyzer/Config/Framework/BindingRedirect.cs @@ -0,0 +1,9 @@ +using System; + +namespace RefScout.Analyzer.Config.Framework; + +public record BindingRedirect( + BindingIdentity Identity, + Version NewVersion, + Version MinimalOldVersion, + Version MaximalOldVersion); \ No newline at end of file diff --git a/src/RefScout.Analyzer/Config/Framework/CodeBase.cs b/src/RefScout.Analyzer/Config/Framework/CodeBase.cs new file mode 100644 index 0000000..c5f2e56 --- /dev/null +++ b/src/RefScout.Analyzer/Config/Framework/CodeBase.cs @@ -0,0 +1,5 @@ +using System; + +namespace RefScout.Analyzer.Config.Framework; + +public record CodeBase(BindingIdentity Identity, Version Version, string RelativeHref, string AbsoluteHref); \ No newline at end of file diff --git a/src/RefScout.Analyzer/Config/Framework/FameworkConfigError.cs b/src/RefScout.Analyzer/Config/Framework/FameworkConfigError.cs new file mode 100644 index 0000000..b6c5fa1 --- /dev/null +++ b/src/RefScout.Analyzer/Config/Framework/FameworkConfigError.cs @@ -0,0 +1,4 @@ +namespace RefScout.Analyzer.Config.Framework; + +public record FrameworkConfigError(string Element, string Message, int LineNumber = -1) : ConfigError(Message, + LineNumber); \ No newline at end of file diff --git a/src/RefScout.Analyzer/Config/Framework/FrameworkConfig.cs b/src/RefScout.Analyzer/Config/Framework/FrameworkConfig.cs new file mode 100644 index 0000000..5d0d2f9 --- /dev/null +++ b/src/RefScout.Analyzer/Config/Framework/FrameworkConfig.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace RefScout.Analyzer.Config.Framework; + +public record FrameworkConfig : IConfig +{ + public IReadOnlyList SupportedRuntimes { get; init; } = Array.Empty(); + public IReadOnlyList ProbeFolders { get; init; } = Array.Empty(); + public IReadOnlyList BindingRedirects { get; init; } = Array.Empty(); + public IReadOnlyList CodeBases { get; init; } = Array.Empty(); + public IConfigErrorReport ErrorReport { get; init; } = new FrameworkConfigErrorReport(); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Config/Framework/FrameworkConfigErrorReport.cs b/src/RefScout.Analyzer/Config/Framework/FrameworkConfigErrorReport.cs new file mode 100644 index 0000000..5d061b7 --- /dev/null +++ b/src/RefScout.Analyzer/Config/Framework/FrameworkConfigErrorReport.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml; +using System.Xml.Linq; + +namespace RefScout.Analyzer.Config.Framework; + +public class FrameworkConfigErrorReport : IConfigErrorReport +{ + private readonly List _errors = new(); + + public bool HasErrors => _errors.Count > 0; + public IReadOnlyList Errors => _errors; + + internal void ReportIfAttributeMissing(XElement element, string attribute) + { + if (element.Attribute(attribute) == null) + { + Report(element, $"Required attribute '{attribute}' is missing."); + } + } + + internal void ReportIfAllAttributesMissing(XElement element, string[] attributes) + { + var allAttributesMissing = attributes.All(a => element.Attribute(a) == null); + if (allAttributesMissing) + { + Report(element, $"Specify one of following attributes: '{string.Join(", ", attributes)}'."); + } + } + + internal void Report(XElement element, string message) + { + // High-tech namespace removal + var elementString = element.ToString().Replace(" xmlns=\"urn:schemas-microsoft-com:asm.v1\"", ""); + var lineNumber = ((IXmlLineInfo)element).HasLineInfo() ? ((IXmlLineInfo)element).LineNumber : -1; + _errors.Add(new FrameworkConfigError(elementString, message, lineNumber)); + } + + + public override string ToString() + { + var groups = _errors.OrderBy(e => e.LineNumber).GroupBy(x => new { x.LineNumber, x.Element }, + (key, errors) => + { + errors = errors.ToList(); + return (key.Element, Errors: errors, errors.First().LineNumber); + }); + + var sb = new StringBuilder(); + foreach (var (element, errors, lineNumber) in groups) + { + sb.AppendLine($"Line {lineNumber}: {element}"); + foreach (var error in errors) + { + sb.AppendLine($" - {error.Message}"); + } + } + + return sb.ToString(); + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Config/Framework/FrameworkConfigParser.cs b/src/RefScout.Analyzer/Config/Framework/FrameworkConfigParser.cs new file mode 100644 index 0000000..0fa4552 --- /dev/null +++ b/src/RefScout.Analyzer/Config/Framework/FrameworkConfigParser.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using System.Xml; +using System.Xml.Linq; +using System.Xml.XPath; +using RefScout.Analyzer.Helpers; + +namespace RefScout.Analyzer.Config.Framework; + +// This is not beautiful but it works and I am not a fan of parsing XML +internal class FrameworkConfigParser : IConfigParser +{ + private static readonly string[] ValidRuntimeVersions = + { "v1.0.3705", "v1.1.4322", "v2.0.50727", "v4.0", "v4.0.30319" }; + + private static readonly string[] ValidSkuValues = + { + ".NETFramework,Version=v4.0", + ".NETFramework,Version=v4.0,Profile=Client", + ".NETFramework,Version=v4.0.1", + ".NETFramework,Version=v4.0.1,Profile=Client", + ".NETFramework,Version=v4.0.2", + ".NETFramework,Version=v4.0.2,Profile=Client", + ".NETFramework,Version=v4.0.3", + ".NETFramework,Version=v4.0.3,Profile=Client", + ".NETFramework,Version=v4.5", + ".NETFramework,Version=v4.5.1", + ".NETFramework,Version=v4.5.2", + ".NETFramework,Version=v4.6", + ".NETFramework,Version=v4.6.1", + ".NETFramework,Version=v4.6.2", + ".NETFramework,Version=v4.7", + ".NETFramework,Version=v4.7.1", + ".NETFramework,Version=v4.7.2", + ".NETFramework,Version=v4.8" + }; + + private readonly IFileSystem _fileSystem; + + public FrameworkConfigParser(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + public FrameworkConfig ParseFile(string assemblyFileName, string? configFileName) + { + if (configFileName is null || !_fileSystem.File.Exists(configFileName)) + { + return new FrameworkConfig(); + } + + var isMachineConfig = Path.GetFileName(configFileName) == "machine.config"; + + using var file = + _fileSystem.File.OpenRead(configFileName); + var document = XDocument.Load(file, LoadOptions.SetLineInfo); + + var namespaceManager = new XmlNamespaceManager(new NameTable()); + namespaceManager.AddNamespace("bind", "urn:schemas-microsoft-com:asm.v1"); + + var supportedRuntimeNodes = document.XPathSelectElements("//startup/supportedRuntime"); + var probingNode = document.XPathSelectElement("//bind:probing", namespaceManager); + var nodes = document.XPathSelectElements("//bind:dependentAssembly", namespaceManager).ToList(); + + + var errorReport = new FrameworkConfigErrorReport(); + var probeFolders = probingNode != null ? ParseProbeFolders(probingNode) : Array.Empty(); + var supportedRuntimes = ParseSupportedRuntimes(supportedRuntimeNodes, errorReport).ToList(); + var (codeBases, bindingRedirects) = + ParseBindingRelated(nodes, Path.GetDirectoryName(assemblyFileName), isMachineConfig, errorReport); + + return new FrameworkConfig + { + SupportedRuntimes = supportedRuntimes, + ProbeFolders = probeFolders, + BindingRedirects = bindingRedirects, + CodeBases = codeBases, + ErrorReport = errorReport + }; + } + + private static IReadOnlyList ParseProbeFolders( + XElement node) + { + if (!node.HasAttributes) + { + return Array.Empty(); + } + + var str = node.Attribute("privatePath")?.Value; + + return !string.IsNullOrEmpty(str) + ? str.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries).ToList() + : Array.Empty(); + } + + private static IEnumerable ParseSupportedRuntimes( + IEnumerable elements, + FrameworkConfigErrorReport errorReport) + { + foreach (var element in elements) + { + errorReport.ReportIfAllAttributesMissing(element, new[] { "version", "sku" }); + + var version = element.Attribute("version")?.Value; + var sku = element.Attribute("sku")?.Value; + + TargetFramework? supportedTarget = null; + if (version != null && !ValidRuntimeVersions.Contains(version)) + { + var versions = string.Join(", ", ValidRuntimeVersions); + errorReport.Report(element, $"Attribute 'version' expects one of: '{versions}'."); + } + else if (version != null) + { + supportedTarget = new TargetFramework(NetRuntime.Framework, + Version.Parse(version.Trim('v')).ToMajorMinor()); + } + + if (version == null && sku != null) + { + // Not displaying all SKUs, list is too long + errorReport.Report(element, + "Attribute 'version' with value 'v4.0' required when using attribute 'sku'."); + } + + if (sku != null && !ValidSkuValues.Contains(sku)) + { + // Not displaying all SKUs, list is too long + errorReport.Report(element, "Invalid value for attribute 'sku'."); + } + else if (sku != null) + { + supportedTarget = TargetFramework.Parse(sku); + } + + if (supportedTarget != null) + { + yield return supportedTarget; + } + } + } + + private static (IReadOnlyList codeBase, IReadOnlyList) ParseBindingRelated( + IEnumerable nodes, + string? baseDirectory, + bool isMachineConfig, + FrameworkConfigErrorReport errorReport) + { + var codeBases = new List(); + var bindingRedirects = new List(); + foreach (var node in nodes) + { + var identity = ParseIdentity(node, isMachineConfig, errorReport); + if (identity == null) + { + continue; + } + + var codeBase = ParseCodeBase(node, identity, baseDirectory ?? "", errorReport); + if (codeBase != null) + { + codeBases.Add(codeBase); + } + + var bindingRedirect = ParseBindingRedirect(node, identity, errorReport); + if (bindingRedirect != null) + { + bindingRedirects.Add(bindingRedirect); + } + } + + return (codeBases, bindingRedirects); + } + + private static BindingIdentity? ParseIdentity( + XElement dependentAssembly, + bool isMachineConfig, + FrameworkConfigErrorReport errorReport) + { + var identityNode = dependentAssembly.Descendants() + .FirstOrDefault(x => x.Name.LocalName == "assemblyIdentity"); + if (identityNode == null) + { + errorReport.Report(dependentAssembly, "Required element is missing."); + return null; + } + + errorReport.ReportIfAttributeMissing(identityNode, "name"); + errorReport.ReportIfAttributeMissing(identityNode, "publicKeyToken"); + + var hexString = identityNode.Attribute("publicKeyToken")?.Value; + var token = PublicKeyToken.Empty; + if (hexString != null) + { + if (PublicKeyToken.TryParse(hexString, out var parsedToken)) + { + token = parsedToken; + } + else + { + errorReport.Report(identityNode, "Invalid value for attribute 'publicKeyToken'."); + } + } + + var identityName = identityNode.Attribute("name")?.Value; + if (identityName != null) + { + return new BindingIdentity(identityName, + identityNode.Attribute("culture")?.Value ?? AssemblyIdentity.CultureNeutral, + token, isMachineConfig); + } + + return null; + } + + private static CodeBase? ParseCodeBase( + XElement node, + BindingIdentity identity, + string basePath, + FrameworkConfigErrorReport errorReport) + { + var codeBaseNode = node.Descendants().FirstOrDefault(x => x.Name.LocalName == "codeBase"); + if (codeBaseNode == null) + { + return null; + } + + // Version attribute is ignored for not strong-named assemblies + if (identity.IsStrongNamed) + { + errorReport.ReportIfAttributeMissing(codeBaseNode, "version"); + } + + errorReport.ReportIfAttributeMissing(codeBaseNode, "href"); + + var version = codeBaseNode.Attribute("version")?.Value; + var href = codeBaseNode.Attribute("href")?.Value; + + if (href == null || version == null) + { + return null; + } + + // TODO: Add better support for parsing href URI's + var absoluteHref = href.StartsWith(@"file:///") + ? href.Replace(@"file:///", "") + : Path.Combine(basePath, href); + return new CodeBase(identity, new Version(version), href, absoluteHref); + } + + private static BindingRedirect? ParseBindingRedirect( + XElement node, + BindingIdentity identity, + FrameworkConfigErrorReport errorReport) + { + var redirectNode = node.Descendants().FirstOrDefault(x => x.Name.LocalName == "bindingRedirect"); + if (redirectNode == null) + { + return null; + } + + errorReport.ReportIfAttributeMissing(redirectNode, "oldVersion"); + errorReport.ReportIfAttributeMissing(redirectNode, "newVersion"); + + var oldVersion = redirectNode.Attribute("oldVersion")?.Value; + var newVersion = redirectNode.Attribute("newVersion")?.Value; + + // TODO: Add handling for incorrect version format + if (oldVersion != null && newVersion != null) + { + var oldVersions = oldVersion.Split('-').Select(v => new Version(v)) + .ToList(); + + return new BindingRedirect(identity, + new Version(newVersion), + oldVersions[0], + oldVersions.Last()); + } + + return null; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Config/IConfig.cs b/src/RefScout.Analyzer/Config/IConfig.cs new file mode 100644 index 0000000..e20a085 --- /dev/null +++ b/src/RefScout.Analyzer/Config/IConfig.cs @@ -0,0 +1,6 @@ +namespace RefScout.Analyzer.Config; + +public interface IConfig +{ + IConfigErrorReport ErrorReport { get; } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Config/IConfigErrorReport.cs b/src/RefScout.Analyzer/Config/IConfigErrorReport.cs new file mode 100644 index 0000000..5cc62d1 --- /dev/null +++ b/src/RefScout.Analyzer/Config/IConfigErrorReport.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace RefScout.Analyzer.Config; + +public interface IConfigErrorReport +{ + bool HasErrors => Errors.Count > 0; + IReadOnlyList Errors { get; } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Config/IConfigParser.cs b/src/RefScout.Analyzer/Config/IConfigParser.cs new file mode 100644 index 0000000..9ac08a5 --- /dev/null +++ b/src/RefScout.Analyzer/Config/IConfigParser.cs @@ -0,0 +1,6 @@ +namespace RefScout.Analyzer.Config; + +internal interface IConfigParser where TConfig : IConfig +{ + TConfig ParseFile(string assemblyFileName, string? configFileName); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Config/IConfigParserFactory.cs b/src/RefScout.Analyzer/Config/IConfigParserFactory.cs new file mode 100644 index 0000000..4dccd25 --- /dev/null +++ b/src/RefScout.Analyzer/Config/IConfigParserFactory.cs @@ -0,0 +1,36 @@ +using System; +using System.IO.Abstractions; +using RefScout.Analyzer.Config.Core; +using RefScout.Analyzer.Config.Framework; + +namespace RefScout.Analyzer.Config; + +internal interface IConfigParserFactory +{ + public IConfigParser Create() where TConfig : IConfig; +} + +internal class ConfigParserFactory : IConfigParserFactory +{ + private readonly IFileSystem _fileSystem; + + public ConfigParserFactory(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + public IConfigParser Create() where TConfig : IConfig + { + if (typeof(TConfig) == typeof(CoreConfig)) + { + return (IConfigParser)new CoreConfigParser(_fileSystem); + } + + if (typeof(TConfig) == typeof(FrameworkConfig)) + { + return (IConfigParser)new FrameworkConfigParser(_fileSystem); + } + + throw new NotSupportedException("Parsing this kind of config is not yet supported."); + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Context/Context.cs b/src/RefScout.Analyzer/Context/Context.cs new file mode 100644 index 0000000..183a5b1 --- /dev/null +++ b/src/RefScout.Analyzer/Context/Context.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using RefScout.Analyzer.Analyzers.Environment; +using RefScout.Analyzer.Config; +using RefScout.Analyzer.Readers; +using RefScout.Analyzer.Resolvers; +using RefScout.Core.Logging; + +namespace RefScout.Analyzer.Context; + +internal abstract class Context : IContext +{ + protected readonly IAssemblyReader Reader; + protected readonly Dictionary Cache; + private List _assemblies; + + private bool _disposed; + + protected Context( + IAssemblyReader reader, + IResolver resolver, + EnvironmentInfo environmentInfo, + Assembly entryPoint) + { + Resolver = resolver ?? throw new ArgumentNullException(nameof(resolver)); + Reader = reader ?? throw new ArgumentNullException(nameof(reader)); + EnvironmentInfo = environmentInfo; + EntryPoint = entryPoint ?? throw new ArgumentNullException(nameof(entryPoint)); + + Cache = new Dictionary(); + _assemblies = new List(); + + Add(entryPoint); + } + + public abstract IConfig Config { get; } + + public IReadOnlyList Assemblies + { + get => _assemblies; + set => _assemblies = value.ToList(); + } + + public IResolver Resolver { get; } + + public Assembly EntryPoint { get; } + public EnvironmentInfo EnvironmentInfo { get; } + + public bool Contains(string assemblyName) => + _assemblies.Any(a => a.Name == assemblyName); + + public bool Contains(Assembly assembly) + { + _ = assembly ?? throw new ArgumentNullException(nameof(assembly)); + return _assemblies.Any(a => a.Name == assembly.Name && a.Version == assembly.Version); + } + + public void Add(Assembly assembly) + { + _ = assembly ?? throw new ArgumentNullException(nameof(assembly)); + + // Duplicates are only allowed when an assembly is unreferenced, because in some instances the same + // unused assembly is found in different probing paths + if (assembly.IsUnreferenced || !Contains(assembly)) + { + _assemblies.Add(assembly); + } + else + { + throw new Exception($"Assembly is already present: {assembly.FullName}"); + } + } + + public abstract Assembly? Find(AssemblyIdentity identity); + public abstract Assembly Resolve(AssemblyIdentity identity); + + protected Assembly ResolveFromResolver(AssemblyIdentity identity) + { + var result = Resolver.ResolvePath(identity); + if (result.Source == AssemblySource.NotFound) + { + Logger.Error( + $"Assembly \"{identity.FullName}\" could not be found in the application's working directory, probing directories or global assembly cache"); + return new Assembly(identity, result.Source); + } + + if (result.Path != null) + { + return Reader.ReadOrDefault(result.Path, result.Source, identity) with + { + IsUnification = result.Unification, + OriginalVersion = identity.Version + }; + } + + // This should technically not happen, but if for whatever reason the resolver returns null it's better to verify it + Logger.Error($"Path for resolved assembly \"{identity.FullName}\" is null"); + throw new Exception($"Path for resolved assembly \"{identity.FullName}\" is null"); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposing || _disposed) + { + return; + } + + Resolver.Dispose(); + _disposed = true; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Context/ContextFactory.cs b/src/RefScout.Analyzer/Context/ContextFactory.cs new file mode 100644 index 0000000..2004443 --- /dev/null +++ b/src/RefScout.Analyzer/Context/ContextFactory.cs @@ -0,0 +1,127 @@ +using System; +using System.IO; +using System.Linq; +using RefScout.Analyzer.Analyzers.Environment; +using RefScout.Analyzer.Analyzers.Environment.Core; +using RefScout.Analyzer.Analyzers.Environment.Framework; +using RefScout.Analyzer.Config; +using RefScout.Analyzer.Config.Core; +using RefScout.Analyzer.Config.Framework; +using RefScout.Analyzer.Readers; +using RefScout.Analyzer.Resolvers; + +namespace RefScout.Analyzer.Context; + +internal class ContextFactory : IContextFactory +{ + public IContext Create( + IConfigParserFactory configParserFactory, + IEnvironmentAnalyzer environmentAnalyzer, + IResolverFactory resolverFactory, + IAssemblyReader reader, + AnalyzeRuntime useRuntime, + string assemblyFileName, + AnalyzerOptions options, + EnvironmentInfo environmentInfo, + Assembly entryPoint) + { + switch (entryPoint.TargetFramework?.Runtime) + { + case NetRuntime.Core or NetRuntime.Standard + when useRuntime is AnalyzeRuntime.Default or AnalyzeRuntime.Core: + return CreateCoreContext(configParserFactory, environmentAnalyzer.CoreRuntimeAnalyzer, resolverFactory, + reader, assemblyFileName, options, + environmentInfo, entryPoint); + case NetRuntime.Framework or NetRuntime.Silverlight + when useRuntime is AnalyzeRuntime.Default or AnalyzeRuntime.Framework: + // Silverlight is not actually framework runtime at all + return CreateFrameworkContext(configParserFactory, environmentAnalyzer.FrameworkRuntimeAnalyzer, + resolverFactory, reader, assemblyFileName, options, + environmentInfo, entryPoint); + case NetRuntime.Framework when useRuntime is AnalyzeRuntime.Default or AnalyzeRuntime.Mono: + return CreateMonoContext(configParserFactory, resolverFactory, reader, assemblyFileName, options, + environmentInfo, entryPoint); + default: + throw new NotSupportedException( + $"Context for target framework {entryPoint.TargetFramework} cannot be created by {GetType().Name}"); + } + } + + private static IContext CreateCoreContext( + IConfigParserFactory configParserFactory, + ICoreRuntimeAnalyzer coreRuntimeAnalyzer, + IResolverFactory resolverFactory, + IAssemblyReader reader, + string assemblyFileName, + AnalyzerOptions options, + EnvironmentInfo environmentInfo, + Assembly entryPoint) + { + var config = configParserFactory.Create().ParseFile(assemblyFileName, options.Config); + + var version = config.TargetRuntimeVersion ?? + entryPoint.TargetFramework?.Version ?? new Version(5, 0); + var runtime = config.SelfContained + ? null + : coreRuntimeAnalyzer.FindRuntime(version, + config.RuntimeConfig.RuntimeOptions?.RollForward ?? default, entryPoint.Is64Bit); + var resolver = resolverFactory.CreateCoreResolver(assemblyFileName, entryPoint, config, runtime); + return new CoreContext(resolver, reader, environmentInfo, config, runtime, entryPoint); + } + + private static IContext CreateFrameworkContext( + IConfigParserFactory configParserFactory, + IFrameworkRuntimeAnalyzer frameworkRuntimeAnalyzer, + IResolverFactory resolverFactory, + IAssemblyReader reader, + string assemblyFileName, + AnalyzerOptions options, + EnvironmentInfo environmentInfo, + Assembly entryPoint) + { + var parser = configParserFactory.Create(); + var config = parser.ParseFile(assemblyFileName, options.Config); + + // Find applicable runtime + var supportedRuntimes = config.SupportedRuntimes.Count > 0 + ? config.SupportedRuntimes + : new[] { entryPoint.TargetFramework }!; + var runtime = frameworkRuntimeAnalyzer.FindRuntime(supportedRuntimes); + + // Parse machine.config + FrameworkConfig? machineConfig = null; + if (runtime != null) + { + var frameworkDirectoryName = entryPoint.Is64Bit ? "Framework64" : "Framework"; + var machineConfigFileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), + $@"Microsoft.NET\{frameworkDirectoryName}\v{runtime.RuntimeVersion}\Config\machine.config)"); + machineConfig = parser.ParseFile(assemblyFileName, machineConfigFileName); + } + + var resolver = + resolverFactory.CreateFrameworkResolver(assemblyFileName, entryPoint, config.ProbeFolders); + return new FrameworkContext(resolver, reader, environmentInfo, machineConfig, config, + supportedRuntimes, runtime, + entryPoint); + } + + private static IContext CreateMonoContext( + IConfigParserFactory configParserFactory, + IResolverFactory resolverFactory, + IAssemblyReader reader, + string assemblyFileName, + AnalyzerOptions options, + EnvironmentInfo environmentInfo, + Assembly entryPoint) + { + var parser = configParserFactory.Create(); + var config = parser.ParseFile(assemblyFileName, options.Config); + + // For now, just pick the first runtime found, not really worth it making it more advanced + var runtime = environmentInfo.Mono?.Runtimes.FirstOrDefault(); + var resolver = + resolverFactory.CreateMonoResolver(assemblyFileName, entryPoint, config.ProbeFolders); + return new MonoContext(resolver, reader, environmentInfo, config, runtime, + entryPoint); + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Context/CoreContext.cs b/src/RefScout.Analyzer/Context/CoreContext.cs new file mode 100644 index 0000000..ba8029e --- /dev/null +++ b/src/RefScout.Analyzer/Context/CoreContext.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using RefScout.Analyzer.Analyzers.Environment; +using RefScout.Analyzer.Analyzers.Environment.Core; +using RefScout.Analyzer.Config.Core; +using RefScout.Analyzer.Readers; +using RefScout.Analyzer.Resolvers; + +namespace RefScout.Analyzer.Context; + +public record CoreAnalyzerResult( + IReadOnlyList Assemblies, + CoreConfig Config, + CoreRuntime? Runtime, + EnvironmentInfo EnvironmentInfo) : IAnalyzerResult; + +internal class CoreContext : Context, ICoreContext +{ + public CoreContext( + IResolver resolver, + IAssemblyReader reader, + EnvironmentInfo environmentInfo, + CoreConfig config, + CoreRuntime? runtime, + Assembly entryPoint) : base(reader, resolver, environmentInfo, entryPoint) + { + Runtime = runtime; + Config = config; + } + + public CoreRuntime? Runtime { get; } + public override CoreConfig Config { get; } + + public override Assembly? Find(AssemblyIdentity identity) + { + _ = identity ?? throw new ArgumentNullException(nameof(identity)); + + // First try to find an exact version, then fallback to first assembly found + var assembly = + Assemblies.SingleOrDefault(a => a.Name == identity.Name && a.ActualVersion == identity.Version); + return assembly ?? Assemblies.FirstOrDefault(a => a.Name == identity.Name); + } + + public override Assembly Resolve(AssemblyIdentity identity) + { + _ = identity ?? throw new ArgumentNullException(nameof(identity)); + + // When only application assemblies are analyzed, system assemblies may still be loaded + // but are not present in the assemblies list + if (Cache.TryGetValue(identity.FullName, out var cachedAssembly)) + { + return cachedAssembly; + } + + var assembly = ResolveFromResolver(identity); + Cache[identity.FullName] = assembly; + return assembly; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Context/FrameworkContext.cs b/src/RefScout.Analyzer/Context/FrameworkContext.cs new file mode 100644 index 0000000..f357b41 --- /dev/null +++ b/src/RefScout.Analyzer/Context/FrameworkContext.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using RefScout.Analyzer.Analyzers.Environment; +using RefScout.Analyzer.Analyzers.Environment.Framework; +using RefScout.Analyzer.Config.Framework; +using RefScout.Analyzer.Readers; +using RefScout.Analyzer.Resolvers; + +namespace RefScout.Analyzer.Context; + +public record FrameworkAnalyzerResult +( + IReadOnlyList Assemblies, + FrameworkConfig? MachineConfig, + FrameworkConfig Config, + FrameworkRuntime? Runtime, + EnvironmentInfo EnvironmentInfo) : IAnalyzerResult; + +internal class FrameworkContext : SharedFrameworkContext, IFrameworkContext +{ + public FrameworkContext( + IResolver resolver, + IAssemblyReader reader, + EnvironmentInfo environmentInfo, + FrameworkConfig? machineConfig, + FrameworkConfig config, + IReadOnlyList supportedRuntimes, + FrameworkRuntime? runtime, + Assembly entryPoint) : base(resolver, reader, environmentInfo, machineConfig, config, entryPoint) + { + SupportedRuntimes = supportedRuntimes; + Runtime = runtime; + } + + public IReadOnlyList SupportedRuntimes { get; } + public FrameworkRuntime? Runtime { get; } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Context/IContext.cs b/src/RefScout.Analyzer/Context/IContext.cs new file mode 100644 index 0000000..79debf8 --- /dev/null +++ b/src/RefScout.Analyzer/Context/IContext.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using RefScout.Analyzer.Analyzers.Environment; +using RefScout.Analyzer.Config; +using RefScout.Analyzer.Resolvers; + +namespace RefScout.Analyzer.Context; + +internal interface IContext : IDisposable +{ + public IResolver Resolver { get; } + EnvironmentInfo EnvironmentInfo { get; } + Assembly EntryPoint { get; } + IConfig Config { get; } + + IReadOnlyList Assemblies { get; set; } + + bool Contains(string assemblyName); + bool Contains(Assembly assembly); + void Add(Assembly assembly); + + Assembly? Find(AssemblyIdentity identity); + Assembly Resolve(AssemblyIdentity identity); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Context/IContextFactory.cs b/src/RefScout.Analyzer/Context/IContextFactory.cs new file mode 100644 index 0000000..0dda5cb --- /dev/null +++ b/src/RefScout.Analyzer/Context/IContextFactory.cs @@ -0,0 +1,20 @@ +using RefScout.Analyzer.Analyzers.Environment; +using RefScout.Analyzer.Config; +using RefScout.Analyzer.Readers; +using RefScout.Analyzer.Resolvers; + +namespace RefScout.Analyzer.Context; + +internal interface IContextFactory +{ + IContext Create( + IConfigParserFactory configParserFactory, + IEnvironmentAnalyzer environmentAnalyzer, + IResolverFactory resolverFactory, + IAssemblyReader reader, + AnalyzeRuntime useRuntime, + string assemblyFileName, + AnalyzerOptions options, + EnvironmentInfo environmentInfo, + Assembly entryPoint); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Context/IContext{T}.cs b/src/RefScout.Analyzer/Context/IContext{T}.cs new file mode 100644 index 0000000..6fddd2a --- /dev/null +++ b/src/RefScout.Analyzer/Context/IContext{T}.cs @@ -0,0 +1,9 @@ +using RefScout.Analyzer.Config; + +namespace RefScout.Analyzer.Context; + +internal interface IContext : IContext where TConfig : IConfig +{ + new TConfig Config { get; } + IConfig IContext.Config => Config; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Context/ICoreContext.cs b/src/RefScout.Analyzer/Context/ICoreContext.cs new file mode 100644 index 0000000..249e6f8 --- /dev/null +++ b/src/RefScout.Analyzer/Context/ICoreContext.cs @@ -0,0 +1,9 @@ +using RefScout.Analyzer.Analyzers.Environment.Core; +using RefScout.Analyzer.Config.Core; + +namespace RefScout.Analyzer.Context; + +internal interface ICoreContext : IContext +{ + CoreRuntime? Runtime { get; } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Context/IFrameworkContext.cs b/src/RefScout.Analyzer/Context/IFrameworkContext.cs new file mode 100644 index 0000000..e30390d --- /dev/null +++ b/src/RefScout.Analyzer/Context/IFrameworkContext.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using RefScout.Analyzer.Analyzers.Environment.Framework; + +namespace RefScout.Analyzer.Context; + +internal interface IFrameworkContext : ISharedFrameworkContext +{ + IReadOnlyList SupportedRuntimes { get; } + FrameworkRuntime? Runtime { get; } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Context/IMonoContext.cs b/src/RefScout.Analyzer/Context/IMonoContext.cs new file mode 100644 index 0000000..9898789 --- /dev/null +++ b/src/RefScout.Analyzer/Context/IMonoContext.cs @@ -0,0 +1,8 @@ +using RefScout.Analyzer.Analyzers.Environment.Mono; + +namespace RefScout.Analyzer.Context; + +internal interface IMonoContext : ISharedFrameworkContext +{ + MonoRuntime? Runtime { get; } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Context/ISharedFrameworkContext.cs b/src/RefScout.Analyzer/Context/ISharedFrameworkContext.cs new file mode 100644 index 0000000..7c3d248 --- /dev/null +++ b/src/RefScout.Analyzer/Context/ISharedFrameworkContext.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using RefScout.Analyzer.Config.Framework; + +namespace RefScout.Analyzer.Context; + +internal interface ISharedFrameworkContext : IContext +{ + FrameworkConfig? MachineConfig { get; } + + BindingRedirect? FindBindingRedirect(string name, Version version); + IReadOnlyList FindBindingRedirects(AssemblyIdentity identity); + CodeBase? FindCodeBase(AssemblyIdentity identity, Version version); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Context/MonoContext.cs b/src/RefScout.Analyzer/Context/MonoContext.cs new file mode 100644 index 0000000..47ce20d --- /dev/null +++ b/src/RefScout.Analyzer/Context/MonoContext.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using RefScout.Analyzer.Analyzers.Environment; +using RefScout.Analyzer.Analyzers.Environment.Mono; +using RefScout.Analyzer.Config.Framework; +using RefScout.Analyzer.Readers; +using RefScout.Analyzer.Resolvers; + +namespace RefScout.Analyzer.Context; + +public record MonoAnalyzerResult +( + IReadOnlyList Assemblies, + FrameworkConfig? MachineConfig, + FrameworkConfig Config, + MonoRuntime? Runtime, + EnvironmentInfo EnvironmentInfo) : IAnalyzerResult; + +internal class MonoContext : SharedFrameworkContext, IMonoContext +{ + public MonoContext( + IResolver resolver, + IAssemblyReader reader, + EnvironmentInfo environmentInfo, + FrameworkConfig config, + MonoRuntime? runtime, + Assembly entryPoint) : base(resolver, reader, environmentInfo, null, config, entryPoint) + { + Runtime = runtime; + } + + public MonoRuntime? Runtime { get; } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Context/SharedFrameworkContext.cs b/src/RefScout.Analyzer/Context/SharedFrameworkContext.cs new file mode 100644 index 0000000..7069fb0 --- /dev/null +++ b/src/RefScout.Analyzer/Context/SharedFrameworkContext.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using RefScout.Analyzer.Analyzers.Compatibility; +using RefScout.Analyzer.Analyzers.Environment; +using RefScout.Analyzer.Config.Framework; +using RefScout.Analyzer.Readers; +using RefScout.Analyzer.Resolvers; + +namespace RefScout.Analyzer.Context; + +internal class SharedFrameworkContext : Context, ISharedFrameworkContext +{ + protected SharedFrameworkContext( + IResolver resolver, + IAssemblyReader reader, + EnvironmentInfo environmentInfo, + FrameworkConfig? machineConfig, + FrameworkConfig config, + Assembly entryPoint) : base(reader, resolver, environmentInfo, entryPoint) + { + MachineConfig = machineConfig; + Config = config; + } + + public FrameworkConfig? MachineConfig { get; } + + public override FrameworkConfig Config { get; } + + public override Assembly? Find(AssemblyIdentity identity) + { + _ = identity ?? throw new ArgumentNullException(nameof(identity)); + + var version = FindBindingRedirect(identity.Name, identity.Version)?.NewVersion ?? identity.Version; + var codeBase = FindCodeBase(identity, version); + if (codeBase != null) + { + // Only compare CodeBase assemblies to CodeBase assemblies + return identity.IsStrongNamed + ? Assemblies.SingleOrDefault(a => + a.Name == identity.Name && a.CodeBase?.Version == codeBase.Version) + : Assemblies.FirstOrDefault(a => + a.Name == identity.Name && a.CodeBase is not null); + } + + // First try to find an exact version, then fallback to first assembly found + var assembly = Assemblies.SingleOrDefault(a => a.Name == identity.Name && a.ActualVersion == version); + return assembly ?? Assemblies.FirstOrDefault(a => a.Name == identity.Name); + } + + public override Assembly Resolve(AssemblyIdentity identity) + { + _ = identity ?? throw new ArgumentNullException(nameof(identity)); + + // When only application assemblies are analyzed, system assemblies may still be loaded + // but are not present in the assemblies list + if (Cache.TryGetValue(identity.FullName, out var cachedAssembly)) + { + return cachedAssembly; + } + + _ = identity ?? throw new ArgumentNullException(nameof(identity)); + + // Use redirected version if binding redirect exists + var redirect = FindBindingRedirect(identity.Name, identity.Version); + identity = identity with + { + Version = redirect?.NewVersion ?? identity.Version + }; + + // Resolve from CodeBase if CodeBase entry exists + var codeBase = FindCodeBase(identity, identity.Version); + var assembly = codeBase != null + ? ResolveFromCodeBase(identity, codeBase) + : ResolveFromResolver(identity); + + // Apply metadata + assembly.BindingRedirects = FindBindingRedirects(identity); + assembly.IsLocalSystem = IsLocalSystem(assembly); + + Cache[identity.FullName] = assembly; + + return assembly; + } + + // Use First rather than Single because duplicate redirects can be configured (e.g. Roslyns' csi.exe.config) + public BindingRedirect? FindBindingRedirect(string name, Version version) + { + bool ShouldRedirect(BindingRedirect c) => c.Identity.Name == name + && version >= c.MinimalOldVersion + && version <= c.MaximalOldVersion; + + var machineRedirect = MachineConfig?.BindingRedirects.FirstOrDefault(ShouldRedirect); + return machineRedirect ?? Config.BindingRedirects.FirstOrDefault(ShouldRedirect); + } + + public IReadOnlyList FindBindingRedirects(AssemblyIdentity identity) + { + bool DoesApply(BindingRedirect x) => x.Identity.Name == identity.Name && + DefaultVersionComparer.AreVersionsCompatible(x.NewVersion, + identity.Version); + + var applicationRedirects = Config.BindingRedirects.Where(DoesApply).ToList(); + + var machineRedirects = MachineConfig?.BindingRedirects.Where(DoesApply); + if (machineRedirects != null) + { + applicationRedirects.InsertRange(0, machineRedirects); + } + + return applicationRedirects; + } + + public CodeBase? FindCodeBase(AssemblyIdentity identity, Version version) + { + // Use first codeBase found if not strong named identity (I am not sure anymore if this is correct, this is pretty old) + // https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/runtime/codebase-element#remarks + bool ShouldApplyCodeBase(CodeBase c) => + c.Identity.Name == identity.Name && (!identity.IsStrongNamed || c.Version == version); + + var machineRedirect = MachineConfig?.CodeBases.FirstOrDefault(ShouldApplyCodeBase); + return machineRedirect ?? Config.CodeBases.FirstOrDefault(ShouldApplyCodeBase); + } + + private Assembly ResolveFromCodeBase(AssemblyIdentity identity, CodeBase codeBase) => + Reader.ReadOrDefault(codeBase.AbsoluteHref, AssemblySource.CodeBase, identity) with + { + CodeBase = codeBase, + OriginalVersion = identity.Version + }; + + private bool IsLocalSystem(Assembly assembly) => + EntryPoint.TargetFramework?.Runtime == NetRuntime.Framework + && assembly.Source == AssemblySource.Local + && assembly.IsSystem; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Filter/FilterParser.cs b/src/RefScout.Analyzer/Filter/FilterParser.cs new file mode 100644 index 0000000..71472fa --- /dev/null +++ b/src/RefScout.Analyzer/Filter/FilterParser.cs @@ -0,0 +1,123 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using RefScout.Analyzer.Notes; + +namespace RefScout.Analyzer.Filter; + +public static class FilterParser +{ + public static Func Parse(string filter) + { + _ = filter ?? throw new ArgumentNullException(nameof(filter)); + + var query = new StringBuilder(); + var isNameQuery = true; + var builder = PredicateBuilder.True(); + foreach (var part in new StringSplitEnumerator(filter)) + { + if (AppendFilterToExpression(ref builder, part)) + { + isNameQuery = false; + } + + if (isNameQuery) + { + query + .Append(query.Length > 0 ? " " : string.Empty) + .Append(part); + } + } + + if (query.Length != 0) + { + builder = builder.And(x => CreateStringPredicate(query.ToString())(x.Name)); + } + + return builder.Compile(); + } + + private static bool AppendFilterToExpression( + ref Expression> expression, + ReadOnlySpan part) + { + if (part.IndexOf(":") == -1) + { + return false; + } + + var (negate, key, value) = ParseKeyValue(part); + if (key.Length == 0 || value.Length == 0) + { + return false; + } + + var filter = ExpressionFromKeyValue(key, value); + if (filter == null) + { + return false; + } + + expression = expression.And(negate ? filter.Not() : filter); + return true; + } + + private static (bool negate, string key, string value) ParseKeyValue(ReadOnlySpan part) + { + var delimiterIndex = part.IndexOf(':'); + var key = part[..delimiterIndex]; + var value = part[(delimiterIndex + 1)..]; + + if (key[0] != '!') + { + return (false, key.ToString(), value.ToString()); + } + + key = key[1..]; + return (true, key.ToString(), value.ToString()); + } + + private static Expression>? ExpressionFromKeyValue(string key, string value) + { + return key switch + { + "to" => a => a.References.Any(r => CreateStringPredicate(value)(r.To.Name)), + "by" => a => a.ReferencedBy.Any(r => CreateStringPredicate(value)(r.From.Name)), + "source" => a => + a.Source.ToString().StartsWith(value, StringComparison.OrdinalIgnoreCase), + "is" => value switch + { + "conflict" => a => a.Level >= NoteLevel.Info, + "unreferenced" => a => a.IsUnreferenced, + "system" => a => a.IsSystem || a.IsNetApi, + _ => null + }, + _ => null + }; + } + + private static Func CreateStringPredicate(string filter) + { + switch (filter[0]) + { + case '^' when filter[^1] == '$': + { + return str => str.Equals(filter[1..^1], StringComparison.OrdinalIgnoreCase); + } + case '^': + { + return str => str.StartsWith(filter[1..], StringComparison.OrdinalIgnoreCase); + } + default: + { + if (filter[^1] == '$') + { + return str => str.EndsWith(filter[..^1], StringComparison.OrdinalIgnoreCase); + } + + return str => str.Contains(filter, StringComparison.OrdinalIgnoreCase); + } + } + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Filter/PredicateBuilder.cs b/src/RefScout.Analyzer/Filter/PredicateBuilder.cs new file mode 100644 index 0000000..9089821 --- /dev/null +++ b/src/RefScout.Analyzer/Filter/PredicateBuilder.cs @@ -0,0 +1,40 @@ +using System; +using System.Linq.Expressions; + +namespace RefScout.Analyzer.Filter; + +internal static class PredicateBuilder +{ + public static Expression> True() + { + return _ => true; + } + + public static Expression> False() + { + return _ => false; + } + + public static Expression> Or( + this Expression> expr1, + Expression> expr2) + { + var invokedExpr = Expression.Invoke(expr2, expr1.Parameters); + return Expression.Lambda>(Expression.OrElse(expr1.Body, invokedExpr), expr1.Parameters); + } + + public static Expression> And( + this Expression> expr1, + Expression> expr2) + { + var invokedExpr = Expression.Invoke(expr2, expr1.Parameters); + return Expression.Lambda>(Expression.AndAlso(expr1.Body, invokedExpr), expr1.Parameters); + } + + public static Expression> Not(this Expression> one) + { + var candidateExpr = one.Parameters[0]; + var body = Expression.Not(one.Body); + return Expression.Lambda>(body, candidateExpr); + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Filter/StringSplitEnumerator.cs b/src/RefScout.Analyzer/Filter/StringSplitEnumerator.cs new file mode 100644 index 0000000..bce937c --- /dev/null +++ b/src/RefScout.Analyzer/Filter/StringSplitEnumerator.cs @@ -0,0 +1,53 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace RefScout.Analyzer.Filter; + +// A over optimized way of splitting a string, but it allocates zero bytes ¯\_(ツ)_/¯ +internal ref struct StringSplitEnumerator +{ + private ReadOnlySpan _str; + + public StringSplitEnumerator(ReadOnlySpan str) + { + _str = str; + Current = default; + } + + [ExcludeFromCodeCoverage] + public StringSplitEnumerator GetEnumerator() => this; + + public bool MoveNext() + { + var span = _str; + + var quoted = false; + var startIndex = -1; + for (var position = 0; position < span.Length; position++) + { + var token = span[position]; + + if (startIndex == -1 && token != ' ') + { + startIndex = token == '"' ? position + 1 : position; + } + + if (token == '"') + { + quoted = !quoted; + } + + if (startIndex >= 0 && (!quoted && token is ' ' or '"' || position == span.Length - 1)) + { + var endIndex = position == span.Length - 1 && token != '"' ? position + 1 : position; + _str = span[(position + 1)..]; + Current = span[startIndex..endIndex]; + return true; + } + } + + return false; + } + + public ReadOnlySpan Current { get; private set; } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Helpers/AssemblyHelper.cs b/src/RefScout.Analyzer/Helpers/AssemblyHelper.cs new file mode 100644 index 0000000..3ab0c29 --- /dev/null +++ b/src/RefScout.Analyzer/Helpers/AssemblyHelper.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace RefScout.Analyzer.Helpers; + +public static class AssemblyHelper +{ + public static List Clone( + IEnumerable assemblies, + Func? predicate = null) + { + var result = predicate != null + ? assemblies.Where(predicate).Select(a => a with { }).ToList() + : assemblies.Select(a => a with { }).ToList(); + foreach (var assembly in result) + { + assembly.References = CloneAssemblyReferences(result, assembly.References); + assembly.ReferencedBy = CloneAssemblyReferences(result, assembly.ReferencedBy); + } + + return result; + } + + private static List CloneAssemblyReferences( + IReadOnlyList assemblies, + IEnumerable references) + { + return references.Where(x => + assemblies.Any(a => a.FullName == x.From.FullName) && + assemblies.Any(a => a.FullName == x.To.FullName)) + .Select(r => r with + { + From = assemblies.Single(x => x.FullName == r.From.FullName), + To = assemblies.Single(x => x.FullName == r.To.FullName) + }).ToList(); + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Helpers/BinaryKmpSearch.cs b/src/RefScout.Analyzer/Helpers/BinaryKmpSearch.cs new file mode 100644 index 0000000..fe814a4 --- /dev/null +++ b/src/RefScout.Analyzer/Helpers/BinaryKmpSearch.cs @@ -0,0 +1,86 @@ +using System.IO.MemoryMappedFiles; + +namespace RefScout.Analyzer.Helpers; + +internal static class BinaryKmpSearch +{ + public static unsafe long SearchInFile(MemoryMappedViewAccessor accessor, byte[] searchPattern) + { + var safeBuffer = accessor.SafeMemoryMappedViewHandle; + return KmpSearch(searchPattern, (byte*)safeBuffer.DangerousGetHandle(), (long)safeBuffer.ByteLength); + } + + // See: https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm + private static int[] ComputeKmpFailureFunction(byte[] pattern) + { + var table = new int[pattern.Length]; + if (pattern.Length >= 1) + { + table[0] = -1; + } + + if (pattern.Length >= 2) + { + table[1] = 0; + } + + var pos = 2; + var cnd = 0; + while (pos < pattern.Length) + { + if (pattern[pos - 1] == pattern[cnd]) + { + table[pos] = cnd + 1; + cnd++; + pos++; + } + else if (cnd > 0) + { + cnd = table[cnd]; + } + else + { + table[pos] = 0; + pos++; + } + } + + return table; + } + + // See: https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm + private static unsafe long KmpSearch(byte[] pattern, byte* bytes, long bytesLength) + { + long m = 0; + long i = 0; + var table = ComputeKmpFailureFunction(pattern); + + while (m + i < bytesLength) + { + if (pattern[i] == bytes[m + i]) + { + if (i == pattern.Length - 1) + { + return m; + } + + i++; + } + else + { + if (table[i] > -1) + { + m = m + i - table[i]; + i = table[i]; + } + else + { + m++; + i = 0; + } + } + } + + return -1; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Helpers/EnvironmentWrapper.cs b/src/RefScout.Analyzer/Helpers/EnvironmentWrapper.cs new file mode 100644 index 0000000..9bd5f74 --- /dev/null +++ b/src/RefScout.Analyzer/Helpers/EnvironmentWrapper.cs @@ -0,0 +1,11 @@ +using System; + +namespace RefScout.Analyzer.Helpers; + +internal class EnvironmentWrapper : IEnvironment +{ + public bool Is64BitOperatingSystem => Environment.Is64BitOperatingSystem; + public OperatingSystem OSVersion => Environment.OSVersion; + public string GetFolderPath(Environment.SpecialFolder folder) => Environment.GetFolderPath(folder); + public string? GetEnvironmentVariable(string variable) => Environment.GetEnvironmentVariable(variable); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Helpers/Extensions.cs b/src/RefScout.Analyzer/Helpers/Extensions.cs new file mode 100644 index 0000000..368f9ca --- /dev/null +++ b/src/RefScout.Analyzer/Helpers/Extensions.cs @@ -0,0 +1,8 @@ +using System; + +namespace RefScout.Analyzer.Helpers; + +internal static class Extensions +{ + public static Version ToMajorMinor(this Version version) => new(version.Major, version.Minor); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Helpers/IEnvironment.cs b/src/RefScout.Analyzer/Helpers/IEnvironment.cs new file mode 100644 index 0000000..2ff17e1 --- /dev/null +++ b/src/RefScout.Analyzer/Helpers/IEnvironment.cs @@ -0,0 +1,15 @@ +using System; + +namespace RefScout.Analyzer.Helpers; + +internal interface IEnvironment +{ + bool Is64BitOperatingSystem { get; } + + // ReSharper disable once InconsistentNaming + OperatingSystem OSVersion { get; } + + string GetFolderPath(Environment.SpecialFolder folder); + + string? GetEnvironmentVariable(string variable); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Helpers/MicrosoftHelper.cs b/src/RefScout.Analyzer/Helpers/MicrosoftHelper.cs new file mode 100644 index 0000000..7bf3f16 --- /dev/null +++ b/src/RefScout.Analyzer/Helpers/MicrosoftHelper.cs @@ -0,0 +1,728 @@ +using System.Collections.Generic; + +namespace RefScout.Analyzer.Helpers; + +// Used to be loaded from json file, but that took way too long (350ms +) +internal static class MicrosoftHelper +{ + private static readonly HashSet MicrosoftAssemblies = new(new[] + { + "clrcompression", + "clretwrc", + "clrjit", + "coreclr", + "dbgshim", + "hostpolicy", + "Microsoft.DiaSymReader.Native.amd64", + "Microsoft.VisualBasic.Core", + "Microsoft.Win32.Registry", + "mscordaccore", + "mscordaccore_amd64_amd64_5.0.821.31504", + "mscordbi", + "mscorrc", + "System.Buffers", + "System.Collections.Immutable", + "System.Diagnostics.DiagnosticSource", + "System.Formats.Asn1", + "System.IO.Compression.Brotli", + "System.IO.FileSystem.AccessControl", + "System.IO.Pipes.AccessControl", + "System.Memory", + "System.Net.Http.Json", + "System.Net.HttpListener", + "System.Net.Mail", + "System.Net.ServicePoint", + "System.Net.WebClient", + "System.Net.WebProxy", + "System.Numerics.Vectors", + "System.Private.CoreLib", + "System.Private.DataContractSerialization", + "System.Private.Uri", + "System.Private.Xml", + "System.Private.Xml.Linq", + "System.Reflection.DispatchProxy", + "System.Reflection.Metadata", + "System.Reflection.TypeExtensions", + "System.Runtime.CompilerServices.Unsafe", + "System.Runtime.Intrinsics", + "System.Runtime.Loader", + "System.Security.AccessControl", + "System.Security.Cryptography.Cng", + "System.Security.Cryptography.OpenSsl", + "System.Security.Principal.Windows", + "System.Text.Encoding.CodePages", + "System.Text.Encodings.Web", + "System.Text.Json", + "System.Threading.Channels", + "System.Threading.Tasks.Dataflow", + "System.Threading.Tasks.Extensions", + "System.Transactions.Local", + "System.Web.HttpUtility", + "ucrtbase", + "D3DCompiler_47_cor3", + "DirectWriteForwarder", + "Microsoft.VisualBasic.Forms", + "Microsoft.Win32.Registry.AccessControl", + "Microsoft.Win32.SystemEvents", + "PenImc_cor3", + "PresentationFramework-SystemCore", + "PresentationFramework-SystemData", + "PresentationFramework-SystemDrawing", + "PresentationFramework-SystemXml", + "PresentationFramework-SystemXmlLinq", + "PresentationNative_cor3", + "System.CodeDom", + "System.Configuration.ConfigurationManager", + "System.Diagnostics.EventLog", + "System.Diagnostics.EventLog.Messages", + "System.Diagnostics.PerformanceCounter", + "System.Drawing.Common", + "System.IO.Packaging", + "System.Resources.Extensions", + "System.Security.Cryptography.Pkcs", + "System.Security.Cryptography.ProtectedData", + "System.Security.Cryptography.Xml", + "System.Security.Permissions", + "System.Threading.AccessControl", + "System.Windows.Extensions", + "System.Windows.Forms.Design", + "System.Windows.Forms.Design.Editors", + "System.Windows.Forms.Primitives", + "UIAutomationClientSideProviders", + "vcruntime140_cor3", + "wpfgfx_cor3", + "Accessibility", + "AddInProcess32", + "AddInProcess", + "AddInUtil", + "aspnet_compiler", + "aspnet_regbrowsers", + "aspnet_regsql", + "AspNetMMCExt", + "caspol", + "ComSvcConfig", + "CustomMarshalers", + "DataSvcUtil", + "dfsvc", + "EdmGen", + "InstallUtil", + "ISymWrapper", + "jsc", + "Microsoft.Activities.Build", + "Microsoft.Build", + "Microsoft.Build.Engine", + "Microsoft.Build.Framework", + "Microsoft.CSharp", + "Microsoft.Data.Entity.Build.Tasks", + "Microsoft.JScript", + "Microsoft.Transactions.Bridge", + "Microsoft.Transactions.Bridge.Dtc", + "Microsoft.VisualBasic", + "Microsoft.VisualBasic.Activities.Compiler", + "Microsoft.VisualBasic.Compatibility", + "Microsoft.VisualBasic.Compatibility.Data", + "Microsoft.VisualC", + "Microsoft.VisualC.STLCLR", + "Microsoft.Win32.Primitives", + "Microsoft.Windows.ApplicationServer.Applications", + "Microsoft.Workflow.Compiler", + "MSBuild", + "mscorlib", + "netstandard", + "PresentationBuildTasks", + "PresentationCore", + "PresentationFramework", + "PresentationFramework.Aero2", + "PresentationFramework.Aero", + "PresentationFramework.AeroLite", + "PresentationFramework.Classic", + "PresentationFramework.Luna", + "PresentationFramework.Royale", + "PresentationUI", + "ReachFramework", + "RegAsm", + "RegSvcs", + "Sentinel.v3.5Client", + "SMDiagnostics", + "SMSvcHost", + "sysglobl", + "System", + "System.Activities", + "System.Activities.Core.Presentation", + "System.Activities.DurableInstancing", + "System.Activities.Presentation", + "System.Activities.Statements", + "System.AddIn", + "System.AddIn.Contract", + "System.AppContext", + "System.Collections", + "System.Collections.Concurrent", + "System.Collections.NonGeneric", + "System.Collections.Specialized", + "System.ComponentModel", + "System.ComponentModel.Annotations", + "System.ComponentModel.Composition", + "System.ComponentModel.Composition.Registration", + "System.ComponentModel.DataAnnotations", + "System.ComponentModel.EventBasedAsync", + "System.ComponentModel.Primitives", + "System.ComponentModel.TypeConverter", + "System.Configuration", + "System.Configuration.Install", + "System.Console", + "System.Core", + "System.Data", + "System.Data.Common", + "System.Data.DataSetExtensions", + "System.Data.Entity", + "System.Data.Entity.Design", + "System.Data.Linq", + "System.Data.OracleClient", + "System.Data.Services", + "System.Data.Services.Client", + "System.Data.Services.Design", + "System.Data.SqlXml", + "System.Deployment", + "System.Design", + "System.Device", + "System.Diagnostics.Contracts", + "System.Diagnostics.Debug", + "System.Diagnostics.FileVersionInfo", + "System.Diagnostics.Process", + "System.Diagnostics.StackTrace", + "System.Diagnostics.TextWriterTraceListener", + "System.Diagnostics.Tools", + "System.Diagnostics.TraceSource", + "System.Diagnostics.Tracing", + "System.DirectoryServices", + "System.DirectoryServices.AccountManagement", + "System.DirectoryServices.Protocols", + "System.Drawing", + "System.Drawing.Design", + "System.Drawing.Primitives", + "System.Dynamic", + "System.Dynamic.Runtime", + "System.EnterpriseServices", + "System.Globalization", + "System.Globalization.Calendars", + "System.Globalization.Extensions", + "System.IdentityModel", + "System.IdentityModel.Selectors", + "System.IdentityModel.Services", + "System.IO", + "System.IO.Compression", + "System.IO.Compression.FileSystem", + "System.IO.Compression.ZipFile", + "System.IO.FileSystem", + "System.IO.FileSystem.DriveInfo", + "System.IO.FileSystem.Primitives", + "System.IO.FileSystem.Watcher", + "System.IO.IsolatedStorage", + "System.IO.Log", + "System.IO.MemoryMappedFiles", + "System.IO.Pipes", + "System.IO.UnmanagedMemoryStream", + "System.Linq", + "System.Linq.Expressions", + "System.Linq.Parallel", + "System.Linq.Queryable", + "System.Management", + "System.Management.Instrumentation", + "System.Messaging", + "System.Net", + "System.Net.Http", + "System.Net.Http.Rtc", + "System.Net.Http.WebRequest", + "System.Net.NameResolution", + "System.Net.NetworkInformation", + "System.Net.Ping", + "System.Net.Primitives", + "System.Net.Requests", + "System.Net.Security", + "System.Net.Sockets", + "System.Net.WebHeaderCollection", + "System.Net.WebSockets", + "System.Net.WebSockets.Client", + "System.Numerics", + "System.ObjectModel", + "System.Printing", + "System.Reflection", + "System.Reflection.Context", + "System.Reflection.Emit", + "System.Reflection.Emit.ILGeneration", + "System.Reflection.Emit.Lightweight", + "System.Reflection.Extensions", + "System.Reflection.Primitives", + "System.Resources.Reader", + "System.Resources.ResourceManager", + "System.Resources.Writer", + "System.Runtime", + "System.Runtime.Caching", + "System.Runtime.CompilerServices.VisualC", + "System.Runtime.DurableInstancing", + "System.Runtime.Extensions", + "System.Runtime.Handles", + "System.Runtime.InteropServices", + "System.Runtime.InteropServices.RuntimeInformation", + "System.Runtime.InteropServices.WindowsRuntime", + "System.Runtime.Numerics", + "System.Runtime.Remoting", + "System.Runtime.Serialization", + "System.Runtime.Serialization.Formatters", + "System.Runtime.Serialization.Formatters.Soap", + "System.Runtime.Serialization.Json", + "System.Runtime.Serialization.Primitives", + "System.Runtime.Serialization.Xml", + "System.Runtime.WindowsRuntime", + "System.Runtime.WindowsRuntime.UI.Xaml", + "System.Security", + "System.Security.Claims", + "System.Security.Cryptography.Algorithms", + "System.Security.Cryptography.Csp", + "System.Security.Cryptography.Encoding", + "System.Security.Cryptography.Primitives", + "System.Security.Cryptography.X509Certificates", + "System.Security.Principal", + "System.Security.SecureString", + "System.ServiceModel", + "System.ServiceModel.Activation", + "System.ServiceModel.Activities", + "System.ServiceModel.Channels", + "System.ServiceModel.Discovery", + "System.ServiceModel.Duplex", + "System.ServiceModel.Http", + "System.ServiceModel.NetTcp", + "System.ServiceModel.Primitives", + "System.ServiceModel.Routing", + "System.ServiceModel.Security", + "System.ServiceModel.ServiceMoniker40", + "System.ServiceModel.WasHosting", + "System.ServiceModel.Web", + "System.ServiceProcess", + "System.Speech", + "System.Text.Encoding", + "System.Text.Encoding.Extensions", + "System.Text.RegularExpressions", + "System.Threading", + "System.Threading.Overlapped", + "System.Threading.Tasks", + "System.Threading.Tasks.Parallel", + "System.Threading.Thread", + "System.Threading.ThreadPool", + "System.Threading.Timer", + "System.Transactions", + "System.ValueTuple", + "System.Web", + "System.Web.Abstractions", + "System.Web.ApplicationServices", + "System.Web.DataVisualization", + "System.Web.DataVisualization.Design", + "System.Web.DynamicData", + "System.Web.DynamicData.Design", + "System.Web.Entity", + "System.Web.Entity.Design", + "System.Web.Extensions", + "System.Web.Extensions.Design", + "System.Web.Mobile", + "System.Web.RegularExpressions", + "System.Web.Routing", + "System.Web.Services", + "System.Windows", + "System.Windows.Controls.Ribbon", + "System.Windows.Forms", + "System.Windows.Forms.DataVisualization", + "System.Windows.Forms.DataVisualization.Design", + "System.Windows.Input.Manipulations", + "System.Windows.Presentation", + "System.Workflow.Activities", + "System.Workflow.ComponentModel", + "System.Workflow.Runtime", + "System.WorkflowServices", + "System.Xaml", + "System.Xaml.Hosting", + "System.Xml", + "System.Xml.Linq", + "System.Xml.ReaderWriter", + "System.Xml.Serialization", + "System.Xml.XDocument", + "System.Xml.XmlDocument", + "System.Xml.XmlSerializer", + "System.Xml.XPath", + "System.Xml.XPath.XDocument", + "UIAutomationClient", + "UIAutomationClientsideProviders", + "UIAutomationProvider", + "UIAutomationTypes", + "WindowsBase", + "WindowsFormsIntegration", + "WsatConfig", + "XamlBuildTask" + }); + + private static readonly HashSet NetApiAssemblies = new(new[] + { + "Accessibility", + "Azure.Extensions.AspNetCore.DataProtection.Keys", + "cscompmgd", + "CustomMarshalers", + "ISymWrapper", + "Microsoft.Activities.Build", + "Microsoft.AspNetCore.ApiAuthorization.IdentityServer", + "Microsoft.AspNetCore.Authentication.AzureAD.UI", + "Microsoft.AspNetCore.Authentication.AzureADB2C.UI", + "Microsoft.AspNetCore.Authentication.Certificate", + "Microsoft.AspNetCore.Authentication.Facebook", + "Microsoft.AspNetCore.Authentication.Google", + "Microsoft.AspNetCore.Authentication.JwtBearer", + "Microsoft.AspNetCore.Authentication.MicrosoftAccount", + "Microsoft.AspNetCore.Authentication.Negotiate", + "Microsoft.AspNetCore.Authentication.OpenIdConnect", + "Microsoft.AspNetCore.Authentication.Twitter", + "Microsoft.AspNetCore.Authentication.WsFederation", + "Microsoft.AspNetCore.AzureAppServices.HostingStartup", + "Microsoft.AspNetCore.AzureAppServicesIntegration", + "Microsoft.AspNetCore.Components.WebAssembly.Authentication", + "Microsoft.AspNetCore.Components.WebAssembly", + "Microsoft.AspNetCore.Components.WebAssembly.Server", + "Microsoft.AspNetCore.ConcurrencyLimiter", + "Microsoft.AspNetCore.DataProtection.EntityFrameworkCore", + "Microsoft.AspNetCore.DataProtection.StackExchangeRedis", + "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore", + "Microsoft.AspNetCore.HeaderPropagation", + "Microsoft.AspNetCore.Hosting.WindowsServices", + "Microsoft.AspNetCore.Http.Connections.Client", + "Microsoft.AspNetCore.Identity.EntityFrameworkCore", + "Microsoft.AspNetCore.Identity.UI", + "Microsoft.AspNetCore.JsonPatch", + "Microsoft.AspNetCore.MiddlewareAnalysis", + "Microsoft.AspNetCore.Mvc.NewtonsoftJson", + "Microsoft.AspNetCore.Mvc.Razor.Host", + "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation", + "Microsoft.AspNetCore.Mvc.Testing", + "Microsoft.AspNetCore.Owin", + "Microsoft.AspNetCore.Razor.Language", + "Microsoft.AspNetCore.Server.Kestrel.Transport.Experimental.Quic", + "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv", + "Microsoft.AspNetCore.SignalR.Client", + "Microsoft.AspNetCore.SignalR.StackExchangeRedis", + "Microsoft.AspNetCore.SpaServices", + "Microsoft.AspNetCore.SpaServices.Extensions", + "Microsoft.AspNetCore.TestHost", + "Microsoft.Authentication.WebAssembly.Msal", + "Microsoft.Build.Conversion.v3.5", + "Microsoft.Build", + "Microsoft.Build.Engine", + "Microsoft.Build.Framework", + "Microsoft.Build.Tasks.Core", + "Microsoft.Build.Utilities.Core", + "Microsoft.CSharp", + "Microsoft.Extensions.Configuration.AzureKeyVault", + "Microsoft.Extensions.DependencyModel", + "Microsoft.Extensions.Logging.AzureAppServices", + "Microsoft.JScript", + "Microsoft.JSInterop.WebAssembly", + "Microsoft.VisualBasic.Compatibility.Data", + "Microsoft.VisualBasic.Core", + "Microsoft.VisualBasic", + "Microsoft.VisualBasic.Forms", + "Microsoft.VisualC", + "Microsoft.VisualC.STLCLR", + "mscorlib", + "PresentationBuildTasks", + "PresentationCore", + "PresentationFramework.Aero2", + "PresentationFramework.Aero", + "PresentationFramework", + "PresentationFramework.Luna", + "PresentationFramework.Royale", + "ReachFramework", + "System.Activities.Core.Presentation", + "System.Activities", + "System.Activities.Presentation", + "System.AddIn.Contract", + "System.AddIn", + "System.CodeDom", + "System.Collections.Concurrent", + "System.Collections.Immutable", + "System.Collections.NonGeneric", + "System.ComponentModel.Annotations", + "System.ComponentModel.Composition", + "System.ComponentModel.Composition.Registration", + "System.ComponentModel.DataAnnotations", + "System.ComponentModel.TypeConverter", + "System.Composition.AttributedModel", + "System.Composition.Runtime", + "System.Configuration.ConfigurationManager", + "System.Configuration.Install", + "System.Core", + "System.Data.Common", + "System.Data", + "System.Data.Entity.Design", + "System.Data.Entity", + "System.Data.Linq", + "System.Data.Odbc", + "System.Data.OleDb", + "System.Data.OracleClient", + "System.Data.Services.Client", + "System.Data.Services.Design", + "System.Data.Services", + "System.Data.SqlClient", + "System.Data.SqlXml", + "System.Deployment", + "System.Design", + "System.Device", + "System.Diagnostics.Contracts", + "System.Diagnostics.DiagnosticSource", + "System.Diagnostics.PerformanceCounter", + "System.Diagnostics.Tracing", + "System.DirectoryServices.AccountManagement", + "System.DirectoryServices", + "System.DirectoryServices.Protocols", + "System", + "System.Dynamic.Runtime", + "System.EnterpriseServices", + "System.Formats.Asn1", + "System.Globalization", + "System.IdentityModel", + "System.IdentityModel.Services", + "System.IO.Compression.Brotli", + "System.IO", + "System.IO.FileSystem", + "System.IO.IsolatedStorage", + "System.IO.Log", + "System.IO.MemoryMappedFiles", + "System.IO.Pipes", + "System.IO.Ports", + "System.Json", + "System.Linq", + "System.Linq.Expressions", + "System.Management", + "System.Memory", + "System.Messaging", + "System.Net", + "System.Net.Http", + "System.Net.Http.Json", + "System.Net.Mail", + "System.Net.NetworkInformation", + "System.Net.Primitives", + "System.Net.Requests", + "System.Net.Security", + "System.Net.WebSockets.Client", + "System.Printing", + "System.Reflection.Context", + "System.Reflection", + "System.Reflection.Emit", + "System.Reflection.Metadata", + "System.Resources.Extensions", + "System.Resources.ResourceManager", + "System.Runtime.Caching", + "System.Runtime", + "System.Runtime.DurableInstancing", + "System.Runtime.InteropServices", + "System.Runtime.Intrinsics", + "System.Runtime.Loader", + "System.Runtime.Remoting", + "System.Runtime.Serialization", + "System.Runtime.Serialization.Formatters", + "System.Runtime.Serialization.Formatters.Soap", + "System.Runtime.Serialization.Json", + "System.Runtime.Serialization.Primitives", + "System.Runtime.WindowsRuntime", + "System.Runtime.WindowsRuntime.UI.Xaml", + "System.Security.AccessControl", + "System.Security.Claims", + "System.Security.Cryptography.Algorithms", + "System.ServiceModel.Activation", + "System.ServiceModel.Activities", + "System.ServiceModel.Discovery", + "System.ServiceModel", + "System.ServiceModel.Primitives", + "System.ServiceModel.Routing", + "System.ServiceModel.Security", + "System.ServiceModel.Syndication", + "System.ServiceModel.Web", + "System.ServiceProcess.ServiceController", + "System.Speech", + "System.Text.Encoding.Extensions", + "System.Text.Encodings.Web", + "System.Text.Json", + "System.Text.RegularExpressions", + "System.Threading.Channels", + "System.Threading", + "System.Threading.Tasks.Dataflow", + "System.Threading.Tasks", + "System.Transactions", + "System.Transactions.Local", + "System.Web.DataVisualization", + "System.Web", + "System.Web.DynamicData.Design", + "System.Web.DynamicData", + "System.Web.Extensions", + "System.Web.Mobile", + "System.Web.RegularExpressions", + "System.Web.Routing", + "System.Web.Services", + "System.Windows.Controls.Ribbon", + "System.Windows.Forms.DataVisualization", + "System.Windows.Forms.Design", + "System.Windows.Forms", + "System.Windows.Forms.Primitives", + "System.Windows.Input.Manipulations", + "System.Workflow.Activities", + "System.Workflow.ComponentModel", + "System.Workflow.Runtime", + "System.WorkflowServices", + "System.Xaml", + "System.Xml", + "System.Xml.ReaderWriter", + "System.Xml.XDocument", + "System.Xml.XPath.XDocument", + "UIAutomationClient", + "UIAutomationClientSideProviders", + "UIAutomationProvider", + "WindowsBase", + "WindowsFormsIntegration", + "XamlBuildTask", + "aspnetcorev2_inprocess", + "Microsoft.AspNetCore.Antiforgery", + "Microsoft.AspNetCore.Authentication.Abstractions", + "Microsoft.AspNetCore.Authentication.Cookies", + "Microsoft.AspNetCore.Authentication.Core", + "Microsoft.AspNetCore.Authentication", + "Microsoft.AspNetCore.Authentication.OAuth", + "Microsoft.AspNetCore.Authorization", + "Microsoft.AspNetCore.Authorization.Policy", + "Microsoft.AspNetCore.Components.Authorization", + "Microsoft.AspNetCore.Components", + "Microsoft.AspNetCore.Components.Forms", + "Microsoft.AspNetCore.Components.Server", + "Microsoft.AspNetCore.Components.Web", + "Microsoft.AspNetCore.Connections.Abstractions", + "Microsoft.AspNetCore.CookiePolicy", + "Microsoft.AspNetCore.Cors", + "Microsoft.AspNetCore.Cryptography.Internal", + "Microsoft.AspNetCore.Cryptography.KeyDerivation", + "Microsoft.AspNetCore.DataProtection.Abstractions", + "Microsoft.AspNetCore.DataProtection", + "Microsoft.AspNetCore.DataProtection.Extensions", + "Microsoft.AspNetCore.Diagnostics.Abstractions", + "Microsoft.AspNetCore.Diagnostics", + "Microsoft.AspNetCore.Diagnostics.HealthChecks", + "Microsoft.AspNetCore", + "Microsoft.AspNetCore.HostFiltering", + "Microsoft.AspNetCore.Hosting.Abstractions", + "Microsoft.AspNetCore.Hosting", + "Microsoft.AspNetCore.Hosting.Server.Abstractions", + "Microsoft.AspNetCore.Html.Abstractions", + "Microsoft.AspNetCore.Http.Abstractions", + "Microsoft.AspNetCore.Http.Connections.Common", + "Microsoft.AspNetCore.Http.Connections", + "Microsoft.AspNetCore.Http", + "Microsoft.AspNetCore.Http.Extensions", + "Microsoft.AspNetCore.Http.Features", + "Microsoft.AspNetCore.HttpOverrides", + "Microsoft.AspNetCore.HttpsPolicy", + "Microsoft.AspNetCore.Identity", + "Microsoft.AspNetCore.Localization", + "Microsoft.AspNetCore.Localization.Routing", + "Microsoft.AspNetCore.Metadata", + "Microsoft.AspNetCore.Mvc.Abstractions", + "Microsoft.AspNetCore.Mvc.ApiExplorer", + "Microsoft.AspNetCore.Mvc.Core", + "Microsoft.AspNetCore.Mvc.Cors", + "Microsoft.AspNetCore.Mvc.DataAnnotations", + "Microsoft.AspNetCore.Mvc", + "Microsoft.AspNetCore.Mvc.Formatters.Json", + "Microsoft.AspNetCore.Mvc.Formatters.Xml", + "Microsoft.AspNetCore.Mvc.Localization", + "Microsoft.AspNetCore.Mvc.Razor", + "Microsoft.AspNetCore.Mvc.RazorPages", + "Microsoft.AspNetCore.Mvc.TagHelpers", + "Microsoft.AspNetCore.Mvc.ViewFeatures", + "Microsoft.AspNetCore.Razor", + "Microsoft.AspNetCore.Razor.Runtime", + "Microsoft.AspNetCore.ResponseCaching.Abstractions", + "Microsoft.AspNetCore.ResponseCaching", + "Microsoft.AspNetCore.ResponseCompression", + "Microsoft.AspNetCore.Rewrite", + "Microsoft.AspNetCore.Routing.Abstractions", + "Microsoft.AspNetCore.Routing", + "Microsoft.AspNetCore.Server.HttpSys", + "Microsoft.AspNetCore.Server.IIS", + "Microsoft.AspNetCore.Server.IISIntegration", + "Microsoft.AspNetCore.Server.Kestrel.Core", + "Microsoft.AspNetCore.Server.Kestrel", + "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets", + "Microsoft.AspNetCore.Session", + "Microsoft.AspNetCore.SignalR.Common", + "Microsoft.AspNetCore.SignalR.Core", + "Microsoft.AspNetCore.SignalR", + "Microsoft.AspNetCore.SignalR.Protocols.Json", + "Microsoft.AspNetCore.StaticFiles", + "Microsoft.AspNetCore.WebSockets", + "Microsoft.AspNetCore.WebUtilities", + "Microsoft.Extensions.Caching.Abstractions", + "Microsoft.Extensions.Caching.Memory", + "Microsoft.Extensions.Configuration.Abstractions", + "Microsoft.Extensions.Configuration.Binder", + "Microsoft.Extensions.Configuration.CommandLine", + "Microsoft.Extensions.Configuration", + "Microsoft.Extensions.Configuration.EnvironmentVariables", + "Microsoft.Extensions.Configuration.FileExtensions", + "Microsoft.Extensions.Configuration.Ini", + "Microsoft.Extensions.Configuration.Json", + "Microsoft.Extensions.Configuration.KeyPerFile", + "Microsoft.Extensions.Configuration.UserSecrets", + "Microsoft.Extensions.Configuration.Xml", + "Microsoft.Extensions.DependencyInjection.Abstractions", + "Microsoft.Extensions.DependencyInjection", + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions", + "Microsoft.Extensions.Diagnostics.HealthChecks", + "Microsoft.Extensions.FileProviders.Abstractions", + "Microsoft.Extensions.FileProviders.Composite", + "Microsoft.Extensions.FileProviders.Embedded", + "Microsoft.Extensions.FileProviders.Physical", + "Microsoft.Extensions.FileSystemGlobbing", + "Microsoft.Extensions.Hosting.Abstractions", + "Microsoft.Extensions.Hosting", + "Microsoft.Extensions.Http", + "Microsoft.Extensions.Identity.Core", + "Microsoft.Extensions.Identity.Stores", + "Microsoft.Extensions.Localization.Abstractions", + "Microsoft.Extensions.Localization", + "Microsoft.Extensions.Logging.Abstractions", + "Microsoft.Extensions.Logging.Configuration", + "Microsoft.Extensions.Logging.Console", + "Microsoft.Extensions.Logging.Debug", + "Microsoft.Extensions.Logging", + "Microsoft.Extensions.Logging.EventLog", + "Microsoft.Extensions.Logging.EventSource", + "Microsoft.Extensions.Logging.TraceSource", + "Microsoft.Extensions.ObjectPool", + "Microsoft.Extensions.Options.ConfigurationExtensions", + "Microsoft.Extensions.Options.DataAnnotations", + "Microsoft.Extensions.Options", + "Microsoft.Extensions.Primitives", + "Microsoft.Extensions.WebEncoders", + "Microsoft.JSInterop", + "Microsoft.Net.Http.Headers", + "Microsoft.Win32.SystemEvents", + "System.Diagnostics.EventLog", + "System.Diagnostics.EventLog.Messages", + "System.Drawing.Common", + "System.IO.Pipelines", + "System.Security.Cryptography.Pkcs", + "System.Security.Cryptography.Xml", + "System.Security.Permissions", + "System.Windows.Extensions", + "Microsoft.Web.Administration", + "Microsoft.Web.Deployment", + "Microsoft.Web.Management" + }); + + public static bool IsSystemAssembly(string name) => + MicrosoftAssemblies.Contains(name); + + public static bool IsNetApi(string name) => + NetApiAssemblies.Contains(name); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Helpers/VersionJsonConverter.cs b/src/RefScout.Analyzer/Helpers/VersionJsonConverter.cs new file mode 100644 index 0000000..3c319b8 --- /dev/null +++ b/src/RefScout.Analyzer/Helpers/VersionJsonConverter.cs @@ -0,0 +1,34 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace RefScout.Analyzer.Helpers; + +[ExcludeFromCodeCoverage] +public class VersionJsonConverter : JsonConverter +{ + public override Version? Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + var str = reader.GetString(); + return str != null ? Version.Parse(str) : null; + } + + public override void Write( + Utf8JsonWriter writer, + Version? version, + JsonSerializerOptions options) + { + if (version == null) + { + writer.WriteNullValue(); + } + else + { + writer.WriteStringValue(version.ToString()); + } + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/IAnalyzer.cs b/src/RefScout.Analyzer/IAnalyzer.cs new file mode 100644 index 0000000..73fc9cf --- /dev/null +++ b/src/RefScout.Analyzer/IAnalyzer.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using RefScout.Analyzer.Analyzers.Compatibility; + +namespace RefScout.Analyzer; + +public interface IAnalyzer +{ + IAnalyzerResult Analyze( + string fileName, + AnalyzerOptions? options = null, + CancellationToken? cancellationToken = null); +} + +[ExcludeFromCodeCoverage] +public record AnalyzerOptions +{ + public string? Config { get; set; } + public string? Filter { get; init; } + + public AnalyzeMode AnalyzeMode { get; init; } + + public IVersionComparer? VersionComparer { get; init; } + public VersionCompatibilityMode SystemVersionMode { get; init; } + + public AnalyzeRuntime AnalyzeRuntime { get; init; } +} + +public enum AnalyzeMode +{ + AppDirectSystem, + App, + All +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/IAnalyzerResult.cs b/src/RefScout.Analyzer/IAnalyzerResult.cs new file mode 100644 index 0000000..5ad9c8a --- /dev/null +++ b/src/RefScout.Analyzer/IAnalyzerResult.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using RefScout.Analyzer.Analyzers.Environment; + +namespace RefScout.Analyzer; + +public interface IAnalyzerResult +{ + IReadOnlyList Assemblies { get; } + EnvironmentInfo EnvironmentInfo { get; } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/MicrosoftAssemblies.json b/src/RefScout.Analyzer/MicrosoftAssemblies.json new file mode 100644 index 0000000..bdc53d9 --- /dev/null +++ b/src/RefScout.Analyzer/MicrosoftAssemblies.json @@ -0,0 +1,713 @@ +{ + "Framework": [ + "clrcompression", + "clretwrc", + "clrjit", + "coreclr", + "dbgshim", + "hostpolicy", + "Microsoft.DiaSymReader.Native.amd64", + "Microsoft.VisualBasic.Core", + "Microsoft.Win32.Registry", + "mscordaccore", + "mscordaccore_amd64_amd64_5.0.821.31504", + "mscordbi", + "mscorrc", + "System.Buffers", + "System.Collections.Immutable", + "System.Diagnostics.DiagnosticSource", + "System.Formats.Asn1", + "System.IO.Compression.Brotli", + "System.IO.FileSystem.AccessControl", + "System.IO.Pipes.AccessControl", + "System.Memory", + "System.Net.Http.Json", + "System.Net.HttpListener", + "System.Net.Mail", + "System.Net.ServicePoint", + "System.Net.WebClient", + "System.Net.WebProxy", + "System.Numerics.Vectors", + "System.Private.CoreLib", + "System.Private.DataContractSerialization", + "System.Private.Uri", + "System.Private.Xml", + "System.Private.Xml.Linq", + "System.Reflection.DispatchProxy", + "System.Reflection.Metadata", + "System.Reflection.TypeExtensions", + "System.Runtime.CompilerServices.Unsafe", + "System.Runtime.Intrinsics", + "System.Runtime.Loader", + "System.Security.AccessControl", + "System.Security.Cryptography.Cng", + "System.Security.Cryptography.OpenSsl", + "System.Security.Principal.Windows", + "System.Text.Encoding.CodePages", + "System.Text.Encodings.Web", + "System.Text.Json", + "System.Threading.Channels", + "System.Threading.Tasks.Dataflow", + "System.Threading.Tasks.Extensions", + "System.Transactions.Local", + "System.Web.HttpUtility", + "ucrtbase", + "D3DCompiler_47_cor3", + "DirectWriteForwarder", + "Microsoft.VisualBasic.Forms", + "Microsoft.Win32.Registry.AccessControl", + "Microsoft.Win32.SystemEvents", + "PenImc_cor3", + "PresentationFramework-SystemCore", + "PresentationFramework-SystemData", + "PresentationFramework-SystemDrawing", + "PresentationFramework-SystemXml", + "PresentationFramework-SystemXmlLinq", + "PresentationNative_cor3", + "System.CodeDom", + "System.Configuration.ConfigurationManager", + "System.Diagnostics.EventLog", + "System.Diagnostics.EventLog.Messages", + "System.Diagnostics.PerformanceCounter", + "System.Drawing.Common", + "System.IO.Packaging", + "System.Resources.Extensions", + "System.Security.Cryptography.Pkcs", + "System.Security.Cryptography.ProtectedData", + "System.Security.Cryptography.Xml", + "System.Security.Permissions", + "System.Threading.AccessControl", + "System.Windows.Extensions", + "System.Windows.Forms.Design", + "System.Windows.Forms.Design.Editors", + "System.Windows.Forms.Primitives", + "UIAutomationClientSideProviders", + "vcruntime140_cor3", + "wpfgfx_cor3", + "Accessibility", + "AddInProcess32", + "AddInProcess", + "AddInUtil", + "aspnet_compiler", + "aspnet_regbrowsers", + "aspnet_regsql", + "AspNetMMCExt", + "caspol", + "ComSvcConfig", + "CustomMarshalers", + "DataSvcUtil", + "dfsvc", + "EdmGen", + "InstallUtil", + "ISymWrapper", + "jsc", + "Microsoft.Activities.Build", + "Microsoft.Build", + "Microsoft.Build.Engine", + "Microsoft.Build.Framework", + "Microsoft.CSharp", + "Microsoft.Data.Entity.Build.Tasks", + "Microsoft.JScript", + "Microsoft.Transactions.Bridge", + "Microsoft.Transactions.Bridge.Dtc", + "Microsoft.VisualBasic", + "Microsoft.VisualBasic.Activities.Compiler", + "Microsoft.VisualBasic.Compatibility", + "Microsoft.VisualBasic.Compatibility.Data", + "Microsoft.VisualC", + "Microsoft.VisualC.STLCLR", + "Microsoft.Win32.Primitives", + "Microsoft.Windows.ApplicationServer.Applications", + "Microsoft.Workflow.Compiler", + "MSBuild", + "mscorlib", + "netstandard", + "PresentationBuildTasks", + "PresentationCore", + "PresentationFramework", + "PresentationFramework.Aero2", + "PresentationFramework.Aero", + "PresentationFramework.AeroLite", + "PresentationFramework.Classic", + "PresentationFramework.Luna", + "PresentationFramework.Royale", + "PresentationUI", + "ReachFramework", + "RegAsm", + "RegSvcs", + "Sentinel.v3.5Client", + "SMDiagnostics", + "SMSvcHost", + "sysglobl", + "System", + "System.Activities", + "System.Activities.Core.Presentation", + "System.Activities.DurableInstancing", + "System.Activities.Presentation", + "System.Activities.Statements", + "System.AddIn", + "System.AddIn.Contract", + "System.AppContext", + "System.Collections", + "System.Collections.Concurrent", + "System.Collections.NonGeneric", + "System.Collections.Specialized", + "System.ComponentModel", + "System.ComponentModel.Annotations", + "System.ComponentModel.Composition", + "System.ComponentModel.Composition.Registration", + "System.ComponentModel.DataAnnotations", + "System.ComponentModel.EventBasedAsync", + "System.ComponentModel.Primitives", + "System.ComponentModel.TypeConverter", + "System.Configuration", + "System.Configuration.Install", + "System.Console", + "System.Core", + "System.Data", + "System.Data.Common", + "System.Data.DataSetExtensions", + "System.Data.Entity", + "System.Data.Entity.Design", + "System.Data.Linq", + "System.Data.OracleClient", + "System.Data.Services", + "System.Data.Services.Client", + "System.Data.Services.Design", + "System.Data.SqlXml", + "System.Deployment", + "System.Design", + "System.Device", + "System.Diagnostics.Contracts", + "System.Diagnostics.Debug", + "System.Diagnostics.FileVersionInfo", + "System.Diagnostics.Process", + "System.Diagnostics.StackTrace", + "System.Diagnostics.TextWriterTraceListener", + "System.Diagnostics.Tools", + "System.Diagnostics.TraceSource", + "System.Diagnostics.Tracing", + "System.DirectoryServices", + "System.DirectoryServices.AccountManagement", + "System.DirectoryServices.Protocols", + "System.Drawing", + "System.Drawing.Design", + "System.Drawing.Primitives", + "System.Dynamic", + "System.Dynamic.Runtime", + "System.EnterpriseServices", + "System.Globalization", + "System.Globalization.Calendars", + "System.Globalization.Extensions", + "System.IdentityModel", + "System.IdentityModel.Selectors", + "System.IdentityModel.Services", + "System.IO", + "System.IO.Compression", + "System.IO.Compression.FileSystem", + "System.IO.Compression.ZipFile", + "System.IO.FileSystem", + "System.IO.FileSystem.DriveInfo", + "System.IO.FileSystem.Primitives", + "System.IO.FileSystem.Watcher", + "System.IO.IsolatedStorage", + "System.IO.Log", + "System.IO.MemoryMappedFiles", + "System.IO.Pipes", + "System.IO.UnmanagedMemoryStream", + "System.Linq", + "System.Linq.Expressions", + "System.Linq.Parallel", + "System.Linq.Queryable", + "System.Management", + "System.Management.Instrumentation", + "System.Messaging", + "System.Net", + "System.Net.Http", + "System.Net.Http.Rtc", + "System.Net.Http.WebRequest", + "System.Net.NameResolution", + "System.Net.NetworkInformation", + "System.Net.Ping", + "System.Net.Primitives", + "System.Net.Requests", + "System.Net.Security", + "System.Net.Sockets", + "System.Net.WebHeaderCollection", + "System.Net.WebSockets", + "System.Net.WebSockets.Client", + "System.Numerics", + "System.ObjectModel", + "System.Printing", + "System.Reflection", + "System.Reflection.Context", + "System.Reflection.Emit", + "System.Reflection.Emit.ILGeneration", + "System.Reflection.Emit.Lightweight", + "System.Reflection.Extensions", + "System.Reflection.Primitives", + "System.Resources.Reader", + "System.Resources.ResourceManager", + "System.Resources.Writer", + "System.Runtime", + "System.Runtime.Caching", + "System.Runtime.CompilerServices.VisualC", + "System.Runtime.DurableInstancing", + "System.Runtime.Extensions", + "System.Runtime.Handles", + "System.Runtime.InteropServices", + "System.Runtime.InteropServices.RuntimeInformation", + "System.Runtime.InteropServices.WindowsRuntime", + "System.Runtime.Numerics", + "System.Runtime.Remoting", + "System.Runtime.Serialization", + "System.Runtime.Serialization.Formatters", + "System.Runtime.Serialization.Formatters.Soap", + "System.Runtime.Serialization.Json", + "System.Runtime.Serialization.Primitives", + "System.Runtime.Serialization.Xml", + "System.Runtime.WindowsRuntime", + "System.Runtime.WindowsRuntime.UI.Xaml", + "System.Security", + "System.Security.Claims", + "System.Security.Cryptography.Algorithms", + "System.Security.Cryptography.Csp", + "System.Security.Cryptography.Encoding", + "System.Security.Cryptography.Primitives", + "System.Security.Cryptography.X509Certificates", + "System.Security.Principal", + "System.Security.SecureString", + "System.ServiceModel", + "System.ServiceModel.Activation", + "System.ServiceModel.Activities", + "System.ServiceModel.Channels", + "System.ServiceModel.Discovery", + "System.ServiceModel.Duplex", + "System.ServiceModel.Http", + "System.ServiceModel.NetTcp", + "System.ServiceModel.Primitives", + "System.ServiceModel.Routing", + "System.ServiceModel.Security", + "System.ServiceModel.ServiceMoniker40", + "System.ServiceModel.WasHosting", + "System.ServiceModel.Web", + "System.ServiceProcess", + "System.Speech", + "System.Text.Encoding", + "System.Text.Encoding.Extensions", + "System.Text.RegularExpressions", + "System.Threading", + "System.Threading.Overlapped", + "System.Threading.Tasks", + "System.Threading.Tasks.Parallel", + "System.Threading.Thread", + "System.Threading.ThreadPool", + "System.Threading.Timer", + "System.Transactions", + "System.ValueTuple", + "System.Web", + "System.Web.Abstractions", + "System.Web.ApplicationServices", + "System.Web.DataVisualization", + "System.Web.DataVisualization.Design", + "System.Web.DynamicData", + "System.Web.DynamicData.Design", + "System.Web.Entity", + "System.Web.Entity.Design", + "System.Web.Extensions", + "System.Web.Extensions.Design", + "System.Web.Mobile", + "System.Web.RegularExpressions", + "System.Web.Routing", + "System.Web.Services", + "System.Windows", + "System.Windows.Controls.Ribbon", + "System.Windows.Forms", + "System.Windows.Forms.DataVisualization", + "System.Windows.Forms.DataVisualization.Design", + "System.Windows.Input.Manipulations", + "System.Windows.Presentation", + "System.Workflow.Activities", + "System.Workflow.ComponentModel", + "System.Workflow.Runtime", + "System.WorkflowServices", + "System.Xaml", + "System.Xaml.Hosting", + "System.Xml", + "System.Xml.Linq", + "System.Xml.ReaderWriter", + "System.Xml.Serialization", + "System.Xml.XDocument", + "System.Xml.XmlDocument", + "System.Xml.XmlSerializer", + "System.Xml.XPath", + "System.Xml.XPath.XDocument", + "UIAutomationClient", + "UIAutomationClientsideProviders", + "UIAutomationProvider", + "UIAutomationTypes", + "WindowsBase", + "WindowsFormsIntegration", + "WsatConfig", + "XamlBuildTask" + ], + "NetApi": [ + "Accessibility", + "Azure.Extensions.AspNetCore.DataProtection.Keys", + "cscompmgd", + "CustomMarshalers", + "ISymWrapper", + "Microsoft.Activities.Build", + "Microsoft.AspNetCore.ApiAuthorization.IdentityServer", + "Microsoft.AspNetCore.Authentication.AzureAD.UI", + "Microsoft.AspNetCore.Authentication.AzureADB2C.UI", + "Microsoft.AspNetCore.Authentication.Certificate", + "Microsoft.AspNetCore.Authentication.Facebook", + "Microsoft.AspNetCore.Authentication.Google", + "Microsoft.AspNetCore.Authentication.JwtBearer", + "Microsoft.AspNetCore.Authentication.MicrosoftAccount", + "Microsoft.AspNetCore.Authentication.Negotiate", + "Microsoft.AspNetCore.Authentication.OpenIdConnect", + "Microsoft.AspNetCore.Authentication.Twitter", + "Microsoft.AspNetCore.Authentication.WsFederation", + "Microsoft.AspNetCore.AzureAppServices.HostingStartup", + "Microsoft.AspNetCore.AzureAppServicesIntegration", + "Microsoft.AspNetCore.Components.WebAssembly.Authentication", + "Microsoft.AspNetCore.Components.WebAssembly", + "Microsoft.AspNetCore.Components.WebAssembly.Server", + "Microsoft.AspNetCore.ConcurrencyLimiter", + "Microsoft.AspNetCore.DataProtection.EntityFrameworkCore", + "Microsoft.AspNetCore.DataProtection.StackExchangeRedis", + "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore", + "Microsoft.AspNetCore.HeaderPropagation", + "Microsoft.AspNetCore.Hosting.WindowsServices", + "Microsoft.AspNetCore.Http.Connections.Client", + "Microsoft.AspNetCore.Identity.EntityFrameworkCore", + "Microsoft.AspNetCore.Identity.UI", + "Microsoft.AspNetCore.JsonPatch", + "Microsoft.AspNetCore.MiddlewareAnalysis", + "Microsoft.AspNetCore.Mvc.NewtonsoftJson", + "Microsoft.AspNetCore.Mvc.Razor.Host", + "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation", + "Microsoft.AspNetCore.Mvc.Testing", + "Microsoft.AspNetCore.Owin", + "Microsoft.AspNetCore.Razor.Language", + "Microsoft.AspNetCore.Server.Kestrel.Transport.Experimental.Quic", + "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv", + "Microsoft.AspNetCore.SignalR.Client", + "Microsoft.AspNetCore.SignalR.StackExchangeRedis", + "Microsoft.AspNetCore.SpaServices", + "Microsoft.AspNetCore.SpaServices.Extensions", + "Microsoft.AspNetCore.TestHost", + "Microsoft.Authentication.WebAssembly.Msal", + "Microsoft.Build.Conversion.v3.5", + "Microsoft.Build", + "Microsoft.Build.Engine", + "Microsoft.Build.Framework", + "Microsoft.Build.Tasks.Core", + "Microsoft.Build.Utilities.Core", + "Microsoft.CSharp", + "Microsoft.Extensions.Configuration.AzureKeyVault", + "Microsoft.Extensions.DependencyModel", + "Microsoft.Extensions.Logging.AzureAppServices", + "Microsoft.JScript", + "Microsoft.JSInterop.WebAssembly", + "Microsoft.VisualBasic.Compatibility.Data", + "Microsoft.VisualBasic.Core", + "Microsoft.VisualBasic", + "Microsoft.VisualBasic.Forms", + "Microsoft.VisualC", + "Microsoft.VisualC.STLCLR", + "mscorlib", + "PresentationBuildTasks", + "PresentationCore", + "PresentationFramework.Aero2", + "PresentationFramework.Aero", + "PresentationFramework", + "PresentationFramework.Luna", + "PresentationFramework.Royale", + "ReachFramework", + "System.Activities.Core.Presentation", + "System.Activities", + "System.Activities.Presentation", + "System.AddIn.Contract", + "System.AddIn", + "System.CodeDom", + "System.Collections.Concurrent", + "System.Collections.Immutable", + "System.Collections.NonGeneric", + "System.ComponentModel.Annotations", + "System.ComponentModel.Composition", + "System.ComponentModel.Composition.Registration", + "System.ComponentModel.DataAnnotations", + "System.ComponentModel.TypeConverter", + "System.Composition.AttributedModel", + "System.Composition.Runtime", + "System.Configuration.ConfigurationManager", + "System.Configuration.Install", + "System.Core", + "System.Data.Common", + "System.Data", + "System.Data.Entity.Design", + "System.Data.Entity", + "System.Data.Linq", + "System.Data.Odbc", + "System.Data.OleDb", + "System.Data.OracleClient", + "System.Data.Services.Client", + "System.Data.Services.Design", + "System.Data.Services", + "System.Data.SqlClient", + "System.Data.SqlXml", + "System.Deployment", + "System.Design", + "System.Device", + "System.Diagnostics.Contracts", + "System.Diagnostics.DiagnosticSource", + "System.Diagnostics.PerformanceCounter", + "System.Diagnostics.Tracing", + "System.DirectoryServices.AccountManagement", + "System.DirectoryServices", + "System.DirectoryServices.Protocols", + "System", + "System.Dynamic.Runtime", + "System.EnterpriseServices", + "System.Formats.Asn1", + "System.Globalization", + "System.IdentityModel", + "System.IdentityModel.Services", + "System.IO.Compression.Brotli", + "System.IO", + "System.IO.FileSystem", + "System.IO.IsolatedStorage", + "System.IO.Log", + "System.IO.MemoryMappedFiles", + "System.IO.Pipes", + "System.IO.Ports", + "System.Json", + "System.Linq", + "System.Linq.Expressions", + "System.Management", + "System.Memory", + "System.Messaging", + "System.Net", + "System.Net.Http", + "System.Net.Http.Json", + "System.Net.Mail", + "System.Net.NetworkInformation", + "System.Net.Primitives", + "System.Net.Requests", + "System.Net.Security", + "System.Net.WebSockets.Client", + "System.Printing", + "System.Reflection.Context", + "System.Reflection", + "System.Reflection.Emit", + "System.Reflection.Metadata", + "System.Resources.Extensions", + "System.Resources.ResourceManager", + "System.Runtime.Caching", + "System.Runtime", + "System.Runtime.DurableInstancing", + "System.Runtime.InteropServices", + "System.Runtime.Intrinsics", + "System.Runtime.Loader", + "System.Runtime.Remoting", + "System.Runtime.Serialization", + "System.Runtime.Serialization.Formatters", + "System.Runtime.Serialization.Formatters.Soap", + "System.Runtime.Serialization.Json", + "System.Runtime.Serialization.Primitives", + "System.Runtime.WindowsRuntime", + "System.Runtime.WindowsRuntime.UI.Xaml", + "System.Security.AccessControl", + "System.Security.Claims", + "System.Security.Cryptography.Algorithms", + "System.ServiceModel.Activation", + "System.ServiceModel.Activities", + "System.ServiceModel.Discovery", + "System.ServiceModel", + "System.ServiceModel.Primitives", + "System.ServiceModel.Routing", + "System.ServiceModel.Security", + "System.ServiceModel.Syndication", + "System.ServiceModel.Web", + "System.ServiceProcess.ServiceController", + "System.Speech", + "System.Text.Encoding.Extensions", + "System.Text.Encodings.Web", + "System.Text.Json", + "System.Text.RegularExpressions", + "System.Threading.Channels", + "System.Threading", + "System.Threading.Tasks.Dataflow", + "System.Threading.Tasks", + "System.Transactions", + "System.Transactions.Local", + "System.Web.DataVisualization", + "System.Web", + "System.Web.DynamicData.Design", + "System.Web.DynamicData", + "System.Web.Extensions", + "System.Web.Mobile", + "System.Web.RegularExpressions", + "System.Web.Routing", + "System.Web.Services", + "System.Windows.Controls.Ribbon", + "System.Windows.Forms.DataVisualization", + "System.Windows.Forms.Design", + "System.Windows.Forms", + "System.Windows.Forms.Primitives", + "System.Windows.Input.Manipulations", + "System.Workflow.Activities", + "System.Workflow.ComponentModel", + "System.Workflow.Runtime", + "System.WorkflowServices", + "System.Xaml", + "System.Xml", + "System.Xml.ReaderWriter", + "System.Xml.XDocument", + "System.Xml.XPath.XDocument", + "UIAutomationClient", + "UIAutomationClientSideProviders", + "UIAutomationProvider", + "WindowsBase", + "WindowsFormsIntegration", + "XamlBuildTask", + "aspnetcorev2_inprocess", + "Microsoft.AspNetCore.Antiforgery", + "Microsoft.AspNetCore.Authentication.Abstractions", + "Microsoft.AspNetCore.Authentication.Cookies", + "Microsoft.AspNetCore.Authentication.Core", + "Microsoft.AspNetCore.Authentication", + "Microsoft.AspNetCore.Authentication.OAuth", + "Microsoft.AspNetCore.Authorization", + "Microsoft.AspNetCore.Authorization.Policy", + "Microsoft.AspNetCore.Components.Authorization", + "Microsoft.AspNetCore.Components", + "Microsoft.AspNetCore.Components.Forms", + "Microsoft.AspNetCore.Components.Server", + "Microsoft.AspNetCore.Components.Web", + "Microsoft.AspNetCore.Connections.Abstractions", + "Microsoft.AspNetCore.CookiePolicy", + "Microsoft.AspNetCore.Cors", + "Microsoft.AspNetCore.Cryptography.Internal", + "Microsoft.AspNetCore.Cryptography.KeyDerivation", + "Microsoft.AspNetCore.DataProtection.Abstractions", + "Microsoft.AspNetCore.DataProtection", + "Microsoft.AspNetCore.DataProtection.Extensions", + "Microsoft.AspNetCore.Diagnostics.Abstractions", + "Microsoft.AspNetCore.Diagnostics", + "Microsoft.AspNetCore.Diagnostics.HealthChecks", + "Microsoft.AspNetCore", + "Microsoft.AspNetCore.HostFiltering", + "Microsoft.AspNetCore.Hosting.Abstractions", + "Microsoft.AspNetCore.Hosting", + "Microsoft.AspNetCore.Hosting.Server.Abstractions", + "Microsoft.AspNetCore.Html.Abstractions", + "Microsoft.AspNetCore.Http.Abstractions", + "Microsoft.AspNetCore.Http.Connections.Common", + "Microsoft.AspNetCore.Http.Connections", + "Microsoft.AspNetCore.Http", + "Microsoft.AspNetCore.Http.Extensions", + "Microsoft.AspNetCore.Http.Features", + "Microsoft.AspNetCore.HttpOverrides", + "Microsoft.AspNetCore.HttpsPolicy", + "Microsoft.AspNetCore.Identity", + "Microsoft.AspNetCore.Localization", + "Microsoft.AspNetCore.Localization.Routing", + "Microsoft.AspNetCore.Metadata", + "Microsoft.AspNetCore.Mvc.Abstractions", + "Microsoft.AspNetCore.Mvc.ApiExplorer", + "Microsoft.AspNetCore.Mvc.Core", + "Microsoft.AspNetCore.Mvc.Cors", + "Microsoft.AspNetCore.Mvc.DataAnnotations", + "Microsoft.AspNetCore.Mvc", + "Microsoft.AspNetCore.Mvc.Formatters.Json", + "Microsoft.AspNetCore.Mvc.Formatters.Xml", + "Microsoft.AspNetCore.Mvc.Localization", + "Microsoft.AspNetCore.Mvc.Razor", + "Microsoft.AspNetCore.Mvc.RazorPages", + "Microsoft.AspNetCore.Mvc.TagHelpers", + "Microsoft.AspNetCore.Mvc.ViewFeatures", + "Microsoft.AspNetCore.Razor", + "Microsoft.AspNetCore.Razor.Runtime", + "Microsoft.AspNetCore.ResponseCaching.Abstractions", + "Microsoft.AspNetCore.ResponseCaching", + "Microsoft.AspNetCore.ResponseCompression", + "Microsoft.AspNetCore.Rewrite", + "Microsoft.AspNetCore.Routing.Abstractions", + "Microsoft.AspNetCore.Routing", + "Microsoft.AspNetCore.Server.HttpSys", + "Microsoft.AspNetCore.Server.IIS", + "Microsoft.AspNetCore.Server.IISIntegration", + "Microsoft.AspNetCore.Server.Kestrel.Core", + "Microsoft.AspNetCore.Server.Kestrel", + "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets", + "Microsoft.AspNetCore.Session", + "Microsoft.AspNetCore.SignalR.Common", + "Microsoft.AspNetCore.SignalR.Core", + "Microsoft.AspNetCore.SignalR", + "Microsoft.AspNetCore.SignalR.Protocols.Json", + "Microsoft.AspNetCore.StaticFiles", + "Microsoft.AspNetCore.WebSockets", + "Microsoft.AspNetCore.WebUtilities", + "Microsoft.Extensions.Caching.Abstractions", + "Microsoft.Extensions.Caching.Memory", + "Microsoft.Extensions.Configuration.Abstractions", + "Microsoft.Extensions.Configuration.Binder", + "Microsoft.Extensions.Configuration.CommandLine", + "Microsoft.Extensions.Configuration", + "Microsoft.Extensions.Configuration.EnvironmentVariables", + "Microsoft.Extensions.Configuration.FileExtensions", + "Microsoft.Extensions.Configuration.Ini", + "Microsoft.Extensions.Configuration.Json", + "Microsoft.Extensions.Configuration.KeyPerFile", + "Microsoft.Extensions.Configuration.UserSecrets", + "Microsoft.Extensions.Configuration.Xml", + "Microsoft.Extensions.DependencyInjection.Abstractions", + "Microsoft.Extensions.DependencyInjection", + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions", + "Microsoft.Extensions.Diagnostics.HealthChecks", + "Microsoft.Extensions.FileProviders.Abstractions", + "Microsoft.Extensions.FileProviders.Composite", + "Microsoft.Extensions.FileProviders.Embedded", + "Microsoft.Extensions.FileProviders.Physical", + "Microsoft.Extensions.FileSystemGlobbing", + "Microsoft.Extensions.Hosting.Abstractions", + "Microsoft.Extensions.Hosting", + "Microsoft.Extensions.Http", + "Microsoft.Extensions.Identity.Core", + "Microsoft.Extensions.Identity.Stores", + "Microsoft.Extensions.Localization.Abstractions", + "Microsoft.Extensions.Localization", + "Microsoft.Extensions.Logging.Abstractions", + "Microsoft.Extensions.Logging.Configuration", + "Microsoft.Extensions.Logging.Console", + "Microsoft.Extensions.Logging.Debug", + "Microsoft.Extensions.Logging", + "Microsoft.Extensions.Logging.EventLog", + "Microsoft.Extensions.Logging.EventSource", + "Microsoft.Extensions.Logging.TraceSource", + "Microsoft.Extensions.ObjectPool", + "Microsoft.Extensions.Options.ConfigurationExtensions", + "Microsoft.Extensions.Options.DataAnnotations", + "Microsoft.Extensions.Options", + "Microsoft.Extensions.Primitives", + "Microsoft.Extensions.WebEncoders", + "Microsoft.JSInterop", + "Microsoft.Net.Http.Headers", + "Microsoft.Win32.SystemEvents", + "System.Diagnostics.EventLog", + "System.Diagnostics.EventLog.Messages", + "System.Drawing.Common", + "System.IO.Pipelines", + "System.Security.Cryptography.Pkcs", + "System.Security.Cryptography.Xml", + "System.Security.Permissions", + "System.Windows.Extensions", + "Microsoft.Web.Administration", + "Microsoft.Web.Deployment", + "Microsoft.Web.Management" + ] +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/ConflictNote.cs b/src/RefScout.Analyzer/Notes/ConflictNote.cs new file mode 100644 index 0000000..1b2c3db --- /dev/null +++ b/src/RefScout.Analyzer/Notes/ConflictNote.cs @@ -0,0 +1,6 @@ +namespace RefScout.Analyzer.Notes; + +public record ConflictNote(NoteType Type, string Message) +{ + public NoteLevel Level => (NoteLevel)(int)Type; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/INoteGenerator.cs b/src/RefScout.Analyzer/Notes/INoteGenerator.cs new file mode 100644 index 0000000..a34acc9 --- /dev/null +++ b/src/RefScout.Analyzer/Notes/INoteGenerator.cs @@ -0,0 +1,8 @@ +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Notes; + +internal interface INoteGenerator +{ + void Generate(IContext context); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/Messages/Core/CoreMissingRuntimeMessage.cs b/src/RefScout.Analyzer/Notes/Messages/Core/CoreMissingRuntimeMessage.cs new file mode 100644 index 0000000..a593fc6 --- /dev/null +++ b/src/RefScout.Analyzer/Notes/Messages/Core/CoreMissingRuntimeMessage.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using RefScout.Analyzer.Analyzers.Environment.Core; +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Notes.Messages.Core; + +internal class CoreMissingRuntimeMessage : Message +{ + public override NoteType Type => NoteType.MissingRuntime; + + // Add note if no runtime is found or when the desired runtime pack is not installed. + public override bool Test(ICoreContext context, Assembly assembly) => + assembly.IsEntryPoint + && !context.Config.SelfContained + && (context.Runtime == null || !context.Runtime.Packs.Contains(context.Config.RuntimePack) || + !context.Runtime.Packs.Contains(RuntimePack.Default)); + + public override string Generate(ICoreContext context, Assembly assembly) + { + var runtimeVersion = context.Config.TargetRuntimeVersion ?? assembly.TargetFramework?.Version; + + if (context.EnvironmentInfo.Core == null) + { + return "Core runtime information is unknown."; + } + + if (runtimeVersion == null) + { + return + "Target runtime version of this assembly could not be determined, therefore runtime assemblies could not be resolved."; + } + + if (context.Runtime == null) + { + if (context.EnvironmentInfo == null) + { + throw new Exception("EnvironmentInfo on context cannot be null."); + } + + var availableVersions = + string.Join(", ", context.EnvironmentInfo.Core.Runtimes.Select(r => r.VersionName)); + return + $"Runtime version {runtimeVersion} is not installed, installed versions are: {availableVersions}."; + } + + var packs = string.Join(", ", context.Runtime.Packs); + var missingPack = !context.Runtime.Packs.Contains(RuntimePack.Default) + ? RuntimePack.Default + : context.Config.RuntimePack; + return + $"The .NET Core {context.Runtime.VersionName} runtime framework {missingPack} is required but only {packs} {(packs.Length > 1 ? "are" : "is")} installed."; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/Messages/Core/CoreVersionMismatchWarningMessage.cs b/src/RefScout.Analyzer/Notes/Messages/Core/CoreVersionMismatchWarningMessage.cs new file mode 100644 index 0000000..1c6bb4a --- /dev/null +++ b/src/RefScout.Analyzer/Notes/Messages/Core/CoreVersionMismatchWarningMessage.cs @@ -0,0 +1,33 @@ +using System.Linq; +using RefScout.Analyzer.Analyzers.Compatibility; +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Notes.Messages.Core; + +internal class CoreVersionMismatchWarningMessage : Message +{ + public override NoteType Type => NoteType.VersionMismatchWarning; + + public override bool Test(ICoreContext context, Assembly assembly) => + assembly.ReferencedBy.Any(r => r.Compatibility == ReferenceCompatibility.MismatchBreaking); + + public override string Generate(ICoreContext context, Assembly assembly) + { + var referencedVersions = assembly.ReferencedBy + .Where(r => r.Compatibility == ReferenceCompatibility.MismatchBreaking) + .Select(a => a.Version) + .Distinct().OrderBy(v => v) + .ToList(); + var versions = string.Join(", ", referencedVersions); + + return referencedVersions.Count == 1 + ? $"Major different version of this assembly is referenced: {versions}, major version differences could be incompatible." + : $"Major different versions of this assembly are referenced: {versions}, major version differences could be incompatible."; + } + + public override bool Test(ICoreContext context, AssemblyRef reference) => + reference.Compatibility == ReferenceCompatibility.MismatchBreaking; + + public override string Generate(ICoreContext context, AssemblyRef reference) => + $"Major version mismatch between reference and resolved assembly: {reference.To.Version}"; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/Messages/Framework/CoreMissingRuntimeMessage.cs b/src/RefScout.Analyzer/Notes/Messages/Framework/CoreMissingRuntimeMessage.cs new file mode 100644 index 0000000..737620b --- /dev/null +++ b/src/RefScout.Analyzer/Notes/Messages/Framework/CoreMissingRuntimeMessage.cs @@ -0,0 +1,33 @@ +using System.Linq; +using RefScout.Analyzer.Context; +using RefScout.Analyzer.Helpers; + +namespace RefScout.Analyzer.Notes.Messages.Framework; + +internal class FrameworkMissingRuntimeMessage : Message +{ + public override NoteType Type => NoteType.MissingRuntime; + + // Add note if no runtime is found or when the desired runtime pack is not installed. + public override bool Test(IFrameworkContext context, Assembly assembly) + => assembly.IsEntryPoint + && (context.EnvironmentInfo.Framework == null || context.Runtime == null); + + public override string Generate(IFrameworkContext context, Assembly assembly) + { + if (context.EnvironmentInfo.Framework == null) + { + return "Framework runtime information is unknown."; + } + + var runtimes = context.EnvironmentInfo.Framework.Runtimes; + var availableVersions = string.Join(", ", runtimes.Select(r => r.Version.ToMajorMinor())); + var supportedVersions = string.Join(", ", context.SupportedRuntimes.Select(r => r.Version)); + + var availableMessage = runtimes.Count == 1 + ? $"but only {availableVersions} is installed." + : $"but only {availableVersions} are installed"; + + return $"Application supports .NET Framework {supportedVersions}, {availableMessage}"; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/Messages/IMessage.cs b/src/RefScout.Analyzer/Notes/Messages/IMessage.cs new file mode 100644 index 0000000..c2b8838 --- /dev/null +++ b/src/RefScout.Analyzer/Notes/Messages/IMessage.cs @@ -0,0 +1,9 @@ +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Notes.Messages; + +internal interface IMessage +{ + bool Test(IContext context, T t); + string Generate(IContext context, T t); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/Messages/Message.cs b/src/RefScout.Analyzer/Notes/Messages/Message.cs new file mode 100644 index 0000000..086f197 --- /dev/null +++ b/src/RefScout.Analyzer/Notes/Messages/Message.cs @@ -0,0 +1,21 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Notes.Messages; + +[ExcludeFromCodeCoverage] +internal abstract class Message : IMessage, IMessage +{ + public abstract NoteType Type { get; } + + public virtual bool Test(IContext context, Assembly assembly) => false; + + public virtual string Generate(IContext context, Assembly assembly) => + throw new NotSupportedException(); + + public virtual bool Test(IContext context, AssemblyRef reference) => false; + + public virtual string Generate(IContext context, AssemblyRef reference) => + throw new NotSupportedException(); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/Messages/Message{T}.cs b/src/RefScout.Analyzer/Notes/Messages/Message{T}.cs new file mode 100644 index 0000000..490fcf5 --- /dev/null +++ b/src/RefScout.Analyzer/Notes/Messages/Message{T}.cs @@ -0,0 +1,42 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Notes.Messages; + +internal abstract class Message : Message where TContext : IContext +{ + public abstract override NoteType Type { get; } + + public sealed override bool Test(IContext context, Assembly assembly) => + context is TContext analyzerContext && Test(analyzerContext, assembly); + + public sealed override bool Test(IContext context, AssemblyRef reference) => + context is TContext analyzerContext && Test(analyzerContext, reference); + + public sealed override string Generate(IContext context, Assembly assembly) => + context is TContext analyzerContext + ? Generate(analyzerContext, assembly) + : throw new NotSupportedException( + $"This note generator is made contexts derived of {typeof(TContext).Name}, {context.GetType()} is not derived."); + + public sealed override string Generate(IContext context, AssemblyRef reference) => + context is TContext analyzerContext + ? Generate(analyzerContext, reference) + : throw new NotSupportedException( + $"This note generator is made contexts derived of {typeof(TContext).Name}, {context.GetType()} is not derived."); + + [ExcludeFromCodeCoverage] + public virtual bool Test(TContext context, Assembly assembly) => false; + + [ExcludeFromCodeCoverage] + public virtual string Generate(TContext context, Assembly assembly) => + throw new NotSupportedException("Implement the assembly generate method to support note for assemblies."); + + [ExcludeFromCodeCoverage] + public virtual bool Test(TContext context, AssemblyRef reference) => false; + + [ExcludeFromCodeCoverage] + public virtual string Generate(TContext context, AssemblyRef reference) => + throw new NotSupportedException("Implement the reference generate method to support note for references."); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/Messages/Mono/CoreMissingRuntimeMessage.cs b/src/RefScout.Analyzer/Notes/Messages/Mono/CoreMissingRuntimeMessage.cs new file mode 100644 index 0000000..52cc6b0 --- /dev/null +++ b/src/RefScout.Analyzer/Notes/Messages/Mono/CoreMissingRuntimeMessage.cs @@ -0,0 +1,26 @@ +using System; +using RefScout.Analyzer.Context; +using RefScout.Analyzer.Helpers; + +namespace RefScout.Analyzer.Notes.Messages.Mono; + +internal class MonoMissingRuntimeMessage : Message +{ + private readonly IEnvironment _environment; + + public MonoMissingRuntimeMessage(IEnvironment environment) + { + _environment = environment; + } + + public override NoteType Type => NoteType.MissingRuntime; + + // Add note if no runtime is found or when the desired runtime pack is not installed. + public override bool Test(IMonoContext context, Assembly assembly) => + assembly.IsEntryPoint && context.Runtime == null; + + public override string Generate(IMonoContext context, Assembly assembly) => + _environment.OSVersion.Platform == PlatformID.Win32NT + ? "No installation of Mono runtime was found, consider analyzing using the .NET Framework runtime." + : "Mono runtime is required for running .NET Framework applications on Unix-based operating systems."; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/Messages/Shared/ArchitectureMismatchMessage.cs b/src/RefScout.Analyzer/Notes/Messages/Shared/ArchitectureMismatchMessage.cs new file mode 100644 index 0000000..aa9c9a9 --- /dev/null +++ b/src/RefScout.Analyzer/Notes/Messages/Shared/ArchitectureMismatchMessage.cs @@ -0,0 +1,13 @@ +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Notes.Messages.Shared; + +internal class ArchitectureMismatchMessage : Message +{ + public override NoteType Type => NoteType.ArchitectureMismatch; + + public override bool Test(IContext context, Assembly assembly) => assembly.IsArchitectureMismatch; + + public override string Generate(IContext context, Assembly assembly) => + $"Platform target ({assembly.ProcessorArchitectureString}) differs from entry point platform target ({context.EntryPoint.ProcessorArchitectureString})."; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/Messages/Shared/ConfigParseErrorMessage.cs b/src/RefScout.Analyzer/Notes/Messages/Shared/ConfigParseErrorMessage.cs new file mode 100644 index 0000000..1605bbd --- /dev/null +++ b/src/RefScout.Analyzer/Notes/Messages/Shared/ConfigParseErrorMessage.cs @@ -0,0 +1,15 @@ +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Notes.Messages.Shared; + +// Don't generate a message for assemblies, note is added whenever the loading error occurs +internal class ConfigParseErrorMessage : Message +{ + public override NoteType Type => NoteType.ConfigParseError; + + public override bool Test(IContext context, Assembly assembly) => + assembly.IsEntryPoint && context.Config.ErrorReport.HasErrors; + + public override string Generate(IContext context, Assembly assembly) => + "Error(s) occurred while trying to parse application config."; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/Messages/Shared/ErrorMessage.cs b/src/RefScout.Analyzer/Notes/Messages/Shared/ErrorMessage.cs new file mode 100644 index 0000000..4f71210 --- /dev/null +++ b/src/RefScout.Analyzer/Notes/Messages/Shared/ErrorMessage.cs @@ -0,0 +1,15 @@ +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Notes.Messages.Shared; + +// Don't generate a message for assemblies, note is added whenever the loading error occurs +internal class ErrorMessage : Message +{ + public override NoteType Type => NoteType.LoadError; + + public override bool Test(IContext context, AssemblyRef reference) => + reference.To.Source == AssemblySource.Error; + + public override string Generate(IContext context, AssemblyRef reference) => + "Referenced assembly could not be read."; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/Messages/Shared/LoadNotFoundMessage.cs b/src/RefScout.Analyzer/Notes/Messages/Shared/LoadNotFoundMessage.cs new file mode 100644 index 0000000..8bbf921 --- /dev/null +++ b/src/RefScout.Analyzer/Notes/Messages/Shared/LoadNotFoundMessage.cs @@ -0,0 +1,19 @@ +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Notes.Messages.Shared; + +internal class LoadNotFoundMessage : Message +{ + public override NoteType Type => NoteType.LoadNotFound; + + public override bool Test(IContext context, Assembly assembly) => + assembly.Source == AssemblySource.NotFound; + + public override string Generate(IContext context, Assembly assembly) => + "Could not be found in the application's working directory, probing directories or global assembly cache."; + + public override bool Test(IContext context, AssemblyRef reference) => Test(context, reference.To); + + public override string Generate(IContext context, AssemblyRef reference) => + "Referenced assembly could not be found."; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/Messages/Shared/UnreferencedMessage.cs b/src/RefScout.Analyzer/Notes/Messages/Shared/UnreferencedMessage.cs new file mode 100644 index 0000000..efabafc --- /dev/null +++ b/src/RefScout.Analyzer/Notes/Messages/Shared/UnreferencedMessage.cs @@ -0,0 +1,13 @@ +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Notes.Messages.Shared; + +internal class UnreferencedMessage : Message +{ + public override NoteType Type => NoteType.Unreferenced; + + public override bool Test(IContext context, Assembly assembly) => assembly.IsUnreferenced; + + public override string Generate(IContext context, Assembly assembly) => + "The assembly was found in the probing directories but is not referenced by the analyzed application."; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/Messages/Shared/VersionMismatchMessage.cs b/src/RefScout.Analyzer/Notes/Messages/Shared/VersionMismatchMessage.cs new file mode 100644 index 0000000..54fe7ec --- /dev/null +++ b/src/RefScout.Analyzer/Notes/Messages/Shared/VersionMismatchMessage.cs @@ -0,0 +1,35 @@ +using System.Linq; +using RefScout.Analyzer.Analyzers.Compatibility; +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Notes.Messages.Shared; + +internal class VersionMismatchMessage : Message +{ + public override NoteType Type => NoteType.VersionMismatch; + + public override bool Test(IContext context, Assembly assembly) => + assembly.ReferencedBy.Any(r => + r.BindingRedirectStatus == BindingRedirectStatus.Default && + r.Compatibility == ReferenceCompatibility.Mismatch); + + public override string Generate(IContext context, Assembly assembly) + { + var referencedVersions = assembly.ReferencedBy + .Where(r => r.Compatibility == ReferenceCompatibility.Mismatch) + .Select(a => a.Version) + .Distinct().OrderBy(v => v) + .ToList(); + var versions = string.Join(", ", referencedVersions); + + return referencedVersions.Count == 1 + ? $"Different version of this assembly is referenced: {versions}" + : $"Different versions of this assembly are referenced: {versions}"; + } + + public override bool Test(IContext context, AssemblyRef reference) => + reference.Compatibility == ReferenceCompatibility.Mismatch; + + public override string Generate(IContext context, AssemblyRef reference) => + $"Version mismatch between reference and resolved assembly: {reference.To.Version}"; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/Messages/SharedFramework/FrameworkVersionMismatchFatalMessage.cs b/src/RefScout.Analyzer/Notes/Messages/SharedFramework/FrameworkVersionMismatchFatalMessage.cs new file mode 100644 index 0000000..12ad956 --- /dev/null +++ b/src/RefScout.Analyzer/Notes/Messages/SharedFramework/FrameworkVersionMismatchFatalMessage.cs @@ -0,0 +1,38 @@ +using System.Linq; +using RefScout.Analyzer.Analyzers.Compatibility; +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Notes.Messages.SharedFramework; + +internal class FrameworkVersionMismatchFatalMessage : Message +{ + public override NoteType Type => NoteType.VersionMismatchFatal; + + public override bool Test(ISharedFrameworkContext context, Assembly assembly) => + assembly.IsStrongNamed && + assembly.ReferencedBy.Any(r => + r.BindingRedirectStatus == BindingRedirectStatus.Default && + r.Compatibility == ReferenceCompatibility.MismatchBreaking); + + public override string Generate(ISharedFrameworkContext context, Assembly assembly) + { + var referencedVersions = assembly.ReferencedBy + .Where(r => r.Compatibility == ReferenceCompatibility.MismatchBreaking) + .Select(a => a.Version) + .Distinct().OrderBy(v => v) + .ToList(); + + var versions = string.Join(", ", referencedVersions); + return referencedVersions.Count == 1 + ? $"Breaking different version of this strong named assembly is referenced: {versions}" + : $"Breaking different versions of this strong named assembly are referenced: {versions}"; + } + + public override bool Test(ISharedFrameworkContext context, AssemblyRef reference) => + reference.Compatibility == ReferenceCompatibility.MismatchBreaking && reference.To.IsStrongNamed && + reference.BindingRedirectStatus is not (BindingRedirectStatus.FailedWrongVersion or BindingRedirectStatus + .Failed); + + public override string Generate(ISharedFrameworkContext context, AssemblyRef reference) => + $"Breaking version mismatch between reference and resolved assembly: {reference.To.Version}"; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/Messages/SharedFramework/FrameworkVersionMismatchWarningMessage.cs b/src/RefScout.Analyzer/Notes/Messages/SharedFramework/FrameworkVersionMismatchWarningMessage.cs new file mode 100644 index 0000000..61a6a67 --- /dev/null +++ b/src/RefScout.Analyzer/Notes/Messages/SharedFramework/FrameworkVersionMismatchWarningMessage.cs @@ -0,0 +1,35 @@ +using System.Linq; +using RefScout.Analyzer.Analyzers.Compatibility; +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Notes.Messages.SharedFramework; + +internal class FrameworkVersionMismatchWarningMessage : Message +{ + public override NoteType Type => NoteType.VersionMismatchWarning; + + public override bool Test(ISharedFrameworkContext context, Assembly assembly) => + !assembly.IsStrongNamed && + assembly.ReferencedBy.Any(r => + r.BindingRedirectStatus == BindingRedirectStatus.Default && + r.Compatibility == ReferenceCompatibility.MismatchBreaking); + + public override string Generate(ISharedFrameworkContext context, Assembly assembly) + { + var referencedVersions = assembly.ReferencedBy + .Where(r => r.Compatibility == ReferenceCompatibility.MismatchBreaking) + .Select(a => a.Version) + .Distinct().OrderBy(v => v) + .ToList(); + var versions = string.Join(", ", referencedVersions); + return referencedVersions.Count == 1 + ? $"Major different version of this assembly is referenced: {versions}, major version differences could be incompatible." + : $"Major different versions of this assembly are referenced: {versions}, major version differences could be incompatible."; + } + + public override bool Test(ISharedFrameworkContext context, AssemblyRef reference) => + reference.Compatibility == ReferenceCompatibility.MismatchBreaking && !reference.To.IsStrongNamed; + + public override string Generate(ISharedFrameworkContext context, AssemblyRef reference) => + $"Major version mismatch between reference and resolved assembly: {reference.To.Version}"; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/Messages/SharedFramework/RedirectFailedMessage.cs b/src/RefScout.Analyzer/Notes/Messages/SharedFramework/RedirectFailedMessage.cs new file mode 100644 index 0000000..cdad4b0 --- /dev/null +++ b/src/RefScout.Analyzer/Notes/Messages/SharedFramework/RedirectFailedMessage.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Linq; +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Notes.Messages.SharedFramework; + +internal class RedirectFailedMessage : Message +{ + public override NoteType Type => NoteType.BindingRedirectFailed; + + public override bool Test(ISharedFrameworkContext context, Assembly assembly) => + assembly.ReferencedBy.Any(r => r.BindingRedirectStatus == BindingRedirectStatus.Failed); + + public override string Generate(ISharedFrameworkContext context, Assembly assembly) => + $"Binding redirect did not cover all versions used. Modify the binding redirect old version: \"{DeterminePossibleRedirectSolution(assembly.ReferencedBy)}\"."; + + public override bool Test(ISharedFrameworkContext context, AssemblyRef reference) => + reference.BindingRedirectStatus == BindingRedirectStatus.Failed; + + public override string Generate(ISharedFrameworkContext context, AssemblyRef reference) => + "Binding redirect did not cover this reference."; + + private static string DeterminePossibleRedirectSolution(IEnumerable references) + { + var versions = references.Select(a => a.Version) + .ToList(); + var maxVersion = versions.Max(); + return $"0.0.0.0-{maxVersion}"; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/Messages/SharedFramework/RedirectFailedWrongVersionMessage.cs b/src/RefScout.Analyzer/Notes/Messages/SharedFramework/RedirectFailedWrongVersionMessage.cs new file mode 100644 index 0000000..8277b6b --- /dev/null +++ b/src/RefScout.Analyzer/Notes/Messages/SharedFramework/RedirectFailedWrongVersionMessage.cs @@ -0,0 +1,33 @@ +using System.Linq; +using RefScout.Analyzer.Config.Framework; +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Notes.Messages.SharedFramework; + +internal class RedirectFailedWrongVersionMessage : Message +{ + public override NoteType Type => NoteType.BindingRedirectFailed; + + public override bool Test(ISharedFrameworkContext context, Assembly assembly) => assembly.ReferencedBy.Any(r => + r.BindingRedirectStatus == BindingRedirectStatus.FailedWrongVersion); + + public override string Generate(ISharedFrameworkContext context, Assembly assembly) + { + var bindingRedirect = FindBindingRedirect(assembly); + return + $"Binding redirect did not redirect to the correct version: {bindingRedirect?.NewVersion}, expected: {assembly.Version}."; + } + + public override bool Test(ISharedFrameworkContext context, AssemblyRef reference) => + reference.BindingRedirectStatus == BindingRedirectStatus.FailedWrongVersion; + + public override string Generate(ISharedFrameworkContext context, AssemblyRef reference) => + "Binding redirect redirects too the incorrect version."; + + private static BindingRedirect? FindBindingRedirect(Assembly assembly) + { + return assembly.ReferencedBy.Find(r => + r.BindingRedirectStatus == BindingRedirectStatus.FailedWrongVersion) + ?.BindingRedirect; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/Messages/SharedFramework/RedirectSuccessMessage.cs b/src/RefScout.Analyzer/Notes/Messages/SharedFramework/RedirectSuccessMessage.cs new file mode 100644 index 0000000..e41268b --- /dev/null +++ b/src/RefScout.Analyzer/Notes/Messages/SharedFramework/RedirectSuccessMessage.cs @@ -0,0 +1,23 @@ +using System.Linq; +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Notes.Messages.SharedFramework; + +internal class RedirectSuccessMessage : Message +{ + public override NoteType Type => NoteType.BindingRedirectSuccess; + + public override bool Test(ISharedFrameworkContext context, Assembly assembly) => + !assembly.ReferencedBy.Any(r => + r.BindingRedirectStatus is BindingRedirectStatus.FailedWrongVersion or BindingRedirectStatus.Failed) + && assembly.ReferencedBy.Any(r => r.BindingRedirectStatus == BindingRedirectStatus.Success); + + public override string Generate(ISharedFrameworkContext context, Assembly assembly) => + "Binding redirect has solved the differences in referenced versions."; + + public override bool Test(ISharedFrameworkContext context, AssemblyRef reference) => + reference.BindingRedirectStatus == BindingRedirectStatus.Success; + + public override string Generate(ISharedFrameworkContext context, AssemblyRef reference) => + "Binding redirect has solved the incorrect version reference."; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/Messages/SharedFramework/UnificationMessage.cs b/src/RefScout.Analyzer/Notes/Messages/SharedFramework/UnificationMessage.cs new file mode 100644 index 0000000..fed4713 --- /dev/null +++ b/src/RefScout.Analyzer/Notes/Messages/SharedFramework/UnificationMessage.cs @@ -0,0 +1,13 @@ +using RefScout.Analyzer.Context; + +namespace RefScout.Analyzer.Notes.Messages.SharedFramework; + +internal class UnificationMessage : Message +{ + public override NoteType Type => NoteType.Unification; + + public override bool Test(ISharedFrameworkContext context, Assembly assembly) => assembly.IsUnification; + + public override string Generate(ISharedFrameworkContext context, Assembly assembly) => + $"The runtime resolved {assembly.OriginalVersion} to {assembly.Version} using the unification table."; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/NoteGenerator.cs b/src/RefScout.Analyzer/Notes/NoteGenerator.cs new file mode 100644 index 0000000..e49a497 --- /dev/null +++ b/src/RefScout.Analyzer/Notes/NoteGenerator.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Linq; +using RefScout.Analyzer.Context; +using RefScout.Analyzer.Helpers; +using RefScout.Analyzer.Notes.Messages; +using RefScout.Analyzer.Notes.Messages.Core; +using RefScout.Analyzer.Notes.Messages.Framework; +using RefScout.Analyzer.Notes.Messages.Mono; +using RefScout.Analyzer.Notes.Messages.Shared; +using RefScout.Analyzer.Notes.Messages.SharedFramework; + +namespace RefScout.Analyzer.Notes; + +internal class NoteGenerator : INoteGenerator +{ + private readonly IEnvironment _environment; + + private IReadOnlyList? _messages; + + public NoteGenerator(IEnvironment environment) + { + _environment = environment; + } + + public void Generate(IContext context) + { + _messages ??= InitMessages(); + + foreach (var generator in _messages) + { + foreach (var assembly in context.Assemblies) + { + foreach (var reference in assembly.ReferencedBy.Where(reference => + generator.Test(context, reference))) + { + reference.AddNote(generator.Type, generator.Generate(context, reference)); + } + + if (generator.Test(context, assembly)) + { + assembly.AddNote(generator.Type, generator.Generate(context, assembly)); + } + } + } + } + + private IReadOnlyList InitMessages() => new List + { + new ConfigParseErrorMessage(), + + new CoreMissingRuntimeMessage(), + new FrameworkMissingRuntimeMessage(), + new MonoMissingRuntimeMessage(_environment), + + new UnreferencedMessage(), + new ErrorMessage(), + new LoadNotFoundMessage(), + new ArchitectureMismatchMessage(), + new RedirectFailedWrongVersionMessage(), + new RedirectFailedMessage(), + new RedirectSuccessMessage(), + + new FrameworkVersionMismatchWarningMessage(), + new FrameworkVersionMismatchFatalMessage(), + new CoreVersionMismatchWarningMessage(), + + new VersionMismatchMessage(), + new UnificationMessage() + }; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/NoteLevel.cs b/src/RefScout.Analyzer/Notes/NoteLevel.cs new file mode 100644 index 0000000..6e9e9e5 --- /dev/null +++ b/src/RefScout.Analyzer/Notes/NoteLevel.cs @@ -0,0 +1,11 @@ +namespace RefScout.Analyzer.Notes; + +public enum NoteLevel +{ + Message = 1, // A grayed out level, don't display if it will clutter the results + Default = 2, + Success = 4, + Info = 8, + Warning = 16, + Fatal = 32 +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Notes/NoteType.cs b/src/RefScout.Analyzer/Notes/NoteType.cs new file mode 100644 index 0000000..316a8d1 --- /dev/null +++ b/src/RefScout.Analyzer/Notes/NoteType.cs @@ -0,0 +1,27 @@ +using System.Diagnostics.CodeAnalysis; + +namespace RefScout.Analyzer.Notes; + +[SuppressMessage("Design", "CA1069:Enums values should not be duplicated", + Justification = "Types can have the same warning level")] +public enum NoteType +{ + Unreferenced = NoteLevel.Message, + Unification = NoteLevel.Default, + + ConfigParseError = NoteLevel.Fatal, + + VersionMismatch = NoteLevel.Info, + VersionMismatchWarning = NoteLevel.Warning, + VersionMismatchFatal = NoteLevel.Fatal, + + ArchitectureMismatch = NoteLevel.Warning, + + BindingRedirectFailed = NoteLevel.Fatal, + BindingRedirectSuccess = NoteLevel.Success, + + MissingRuntime = NoteLevel.Fatal, + + LoadNotFound = NoteLevel.Fatal, + LoadError = NoteLevel.Fatal +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Properties/AssemblyInfo.cs b/src/RefScout.Analyzer/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..852c563 --- /dev/null +++ b/src/RefScout.Analyzer/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("RefScout.Analyzer.Tests")] +[assembly: InternalsVisibleTo("RefScout.Analyzer.FunctionalTests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +[assembly: InternalsVisibleTo("Test")] // TODO: REMOVE \ No newline at end of file diff --git a/src/RefScout.Analyzer/PublicKeyToken.cs b/src/RefScout.Analyzer/PublicKeyToken.cs new file mode 100644 index 0000000..f3ff988 --- /dev/null +++ b/src/RefScout.Analyzer/PublicKeyToken.cs @@ -0,0 +1,145 @@ +using System; +using System.Linq; + +namespace RefScout.Analyzer; + +public readonly struct PublicKeyToken +{ + private const int TokenSize = 8; + public static readonly PublicKeyToken Empty = new(); + + private readonly byte[]? _data; + + public PublicKeyToken(byte[]? data) + { + if (data is null || data.Length != TokenSize) + { + this = Empty; + } + else + { + _data = data; + } + } + + public override string ToString() + { + const string nullString = "null"; + + if (_data is not { Length: TokenSize }) + { + return nullString; + } + + return string.Create(TokenSize * 2, _data, (chars, buffer) => + { + for (var i = 0; i < buffer.Length; i++) + { + var high = buffer[i] >> 4; + chars[i * 2] = (char)(87 + high + (((high - 10) >> 31) & -39)); + var low = buffer[i] & 0xF; + chars[i * 2 + 1] = (char)(87 + low + (((low - 10) >> 31) & -39)); + } + }); + } + + public static bool operator ==(PublicKeyToken a, PublicKeyToken b) + { + if (a._data == null && b._data == null) + { + return true; + } + + if (a._data == null || b._data == null || a._data.Length != b._data.Length) + { + return false; + } + + for (var i = 0; i < TokenSize; i++) + { + if (a._data[i] != b._data[i]) + { + return false; + } + } + + return true; + } + + public static bool operator !=(PublicKeyToken a, PublicKeyToken b) => !(a == b); + + public static PublicKeyToken Parse(string hexString) => Parse(hexString.AsSpan()); + + public static PublicKeyToken Parse(ReadOnlySpan hexString) + { + var exception = TryParseInternal(hexString, out var token); + return exception != null + ? throw exception + : token; + } + + public static bool TryParse(string hexString, out PublicKeyToken token) => + TryParse(hexString.AsSpan(), out token); + + public static bool TryParse(ReadOnlySpan hexString, out PublicKeyToken token) + { + var exception = TryParseInternal(hexString, out token); + return exception == null; + } + + private static Exception? TryParseInternal(ReadOnlySpan hexString, out PublicKeyToken token) + { + token = Empty; + if (hexString.Length != TokenSize * 2) + { + return new ArgumentOutOfRangeException(nameof(hexString), + $"PublicKeyToken should always be {TokenSize * 2} hexadecimal characters"); + } + + var bytes = new byte[TokenSize]; + for (var i = 0; i < bytes.Length; i++) + { + int high = hexString[i * 2]; + int low = hexString[i * 2 + 1]; + + if (!IsValidHexCharacter(high) || !IsValidHexCharacter(low)) + { + return new ArgumentException("A public key token can only consist of hexadecimal characters", + nameof(hexString)); + } + + high = (high & 0xf) + ((high & 0x40) >> 6) * 9; + low = (low & 0xf) + ((low & 0x40) >> 6) * 9; + bytes[i] = (byte)((high << 4) | low); + } + + token = new PublicKeyToken(bytes); + return null; + } + + private static bool IsValidHexCharacter(int character) => + character is (>= '0' and <= '9') or (>= 'a' and <= 'f') or (>= 'A' and <= 'F'); + + public override bool Equals(object? obj) + { + if (obj is not PublicKeyToken other) + { + return false; + } + + return this == other; + } + + public override int GetHashCode() + { + if (_data == null) + { + return 0; + } + + unchecked + { + return _data.Aggregate(0, (current, b) => (current * 31) ^ b); + } + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Readers/AssemblyReader.cs b/src/RefScout.Analyzer/Readers/AssemblyReader.cs new file mode 100644 index 0000000..b6a644d --- /dev/null +++ b/src/RefScout.Analyzer/Readers/AssemblyReader.cs @@ -0,0 +1,99 @@ +using System; +using System.IO; +using System.Linq; +using RefScout.Analyzer.Notes; +using RefScout.Core.Logging; + +namespace RefScout.Analyzer.Readers; + +internal record ReaderResult(AssemblyIdentity Identity, IMetadataReader MetadataReader); + +internal abstract class AssemblyReader : IAssemblyReader +{ + public Assembly Read(string fileName, AssemblySource source, bool applyMetadata = true) + { + _ = fileName ?? throw new ArgumentNullException(nameof(fileName)); + + var (identity, metadataReader) = InternalRead(fileName); + var assembly = new Assembly(identity, fileName, source); + return ApplyMetadataIfRequested(assembly, metadataReader, applyMetadata); + } + + public Assembly ReadOrDefault( + string fileName, + AssemblySource source, + AssemblyIdentity defaultIdentity, + bool applyMetadata = true) + { + _ = fileName ?? throw new ArgumentNullException(nameof(fileName)); + _ = defaultIdentity ?? throw new ArgumentNullException(nameof(defaultIdentity)); + + var (assembly, metadataReader) = ReadAndFallbackToError(fileName, defaultIdentity, source); + return ApplyMetadataIfRequested(assembly, metadataReader, applyMetadata); + } + + + private (Assembly assembly, IMetadataReader? metadataReader) ReadAndFallbackToError( + string fileName, + AssemblyIdentity identity, + AssemblySource source) + { + try + { + var result = InternalRead(fileName); + return (new Assembly(result.Identity, fileName, source), result.MetadataReader); + } + catch (Exception e) when (e is DirectoryNotFoundException or FileNotFoundException) + { + return (new Assembly(identity, fileName, AssemblySource.NotFound), null); + } + catch (Exception e) + { + Logger.Error(e, $"Assembly file \"{fileName}\" could not be read."); + + var assembly = new Assembly(identity, fileName, AssemblySource.Error); + assembly.AddNote(NoteType.LoadError, + $"Assembly file could not be read: {e.Message}"); + return (assembly, null); + } + } + + private static Assembly ApplyMetadataIfRequested( + Assembly assembly, + IMetadataReader? metadataReader, + bool applyMetadata) + { + try + { + return applyMetadata && metadataReader != null + ? ApplyMetadata(assembly, metadataReader) + : assembly; + } + finally + { + metadataReader?.Dispose(); + } + } + + private static Assembly ApplyMetadata(Assembly assembly, IMetadataReader metadataReader) + { + assembly.RawReferences = metadataReader.ReadReferences().ToList(); + assembly.TargetFramework = metadataReader.ReadTargetFramework(); + + // We don't need this metadata for .NET Apis + if (!assembly.IsSystem && !assembly.IsNetApi) + { + assembly.Kind = metadataReader.ReadKind(); + assembly.SourceLanguage = metadataReader.ReadSourceLanguage(); + } + + var (archString, arch, is64Bit) = metadataReader.ReadProcessorArchitecture(); + assembly.ProcessorArchitectureString = archString; + assembly.ProcessorArchitecture = arch; + assembly.Is64Bit = is64Bit; + + return assembly; + } + + protected abstract ReaderResult InternalRead(string fileName); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Readers/Cecil/CecilAssemblyReader.cs b/src/RefScout.Analyzer/Readers/Cecil/CecilAssemblyReader.cs new file mode 100644 index 0000000..1395d72 --- /dev/null +++ b/src/RefScout.Analyzer/Readers/Cecil/CecilAssemblyReader.cs @@ -0,0 +1,20 @@ +using Mono.Cecil; + +namespace RefScout.Analyzer.Readers.Cecil; + +internal class CecilAssemblyReader : AssemblyReader +{ + protected override ReaderResult InternalRead(string fileName) + { + var definition = AssemblyDefinition.ReadAssembly(fileName); + return new ReaderResult(MapNameToIdentity(definition.Name), new CecilMetadataReader(definition)); + } + + public static AssemblyIdentity MapNameToIdentity(AssemblyNameReference name) => + new(name.Name, string.IsNullOrEmpty(name.Culture) ? AssemblyIdentity.CultureNeutral : name.Culture, + new PublicKeyToken(name.PublicKeyToken), + name.Version) + { + IsWindowsRuntime = name.IsWindowsRuntime + }; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Readers/Cecil/CecilMetadataReader.cs b/src/RefScout.Analyzer/Readers/Cecil/CecilMetadataReader.cs new file mode 100644 index 0000000..60bf0c3 --- /dev/null +++ b/src/RefScout.Analyzer/Readers/Cecil/CecilMetadataReader.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Versioning; +using Mono.Cecil; + +namespace RefScout.Analyzer.Readers.Cecil; + +internal class CecilMetadataReader : IMetadataReader +{ + private readonly AssemblyDefinition _definition; + + public CecilMetadataReader(AssemblyDefinition definition) + { + _definition = definition; + } + + public AssemblyKind ReadKind() + { + var kind = _definition.MainModule.Kind switch + { + ModuleKind.Windows => AssemblyKind.Windows, + ModuleKind.Console => AssemblyKind.Console, + _ => AssemblyKind.Dll + }; + + if (kind != AssemblyKind.Dll) + { + return kind; + } + + return _definition.MainModule.AssemblyReferences.Any(reference => + reference.Name.StartsWith("Microsoft.AspNetCore.", StringComparison.OrdinalIgnoreCase)) + ? AssemblyKind.Web + : kind; + } + + public (string architectureString, ProcessorArchitecture architecture, bool is64Bit) ReadProcessorArchitecture() + { + var architecture = (int)_definition.MainModule.Architecture; + + // Temporary fix until Mono.Cecil is updated + // See: https://github.com/dotnet/runtime/issues/36364 + // And: https://github.com/jbevain/cecil/issues/797 + if (architecture > (int)TargetArchitecture.AMD64) + { + if (Environment.OSVersion.Platform == PlatformID.MacOSX) + { + architecture ^= 0x4644; + } + else + { + architecture ^= 0x7B79; + } + } + + var characteristics = _definition.MainModule.Characteristics; + var corFlags = _definition.MainModule.Attributes; + switch ((TargetArchitecture)architecture) + { + case TargetArchitecture.I386: + if ((corFlags & ModuleAttributes.Preferred32Bit) != 0) + { + return ("Any CPU (32-bit preferred)", ProcessorArchitecture.Cil, false); + } + + if ((corFlags & ModuleAttributes.Required32Bit) != 0) + { + return ("x86", ProcessorArchitecture.X86, false); + } + + if ((corFlags & ModuleAttributes.ILOnly) == 0 && + (characteristics & ModuleCharacteristics.NXCompat) != 0) + { + return ("x86", ProcessorArchitecture.X86, false); + } + + return ("Any CPU", ProcessorArchitecture.Cil, true); + case TargetArchitecture.AMD64: + return ("x64", ProcessorArchitecture.Amd64, true); + case TargetArchitecture.IA64: + return ("Itanium", ProcessorArchitecture.Ia64, true); + case TargetArchitecture.ARMv7: + case TargetArchitecture.ARM: + return ("ARM", ProcessorArchitecture.Arm, false); + case TargetArchitecture.ARM64: + return ("ARM64", ProcessorArchitecture.Arm64, true); + default: + throw new Exception($"Target architecture {architecture} of '{_definition.FullName}' is not supported"); + } + } + + public IEnumerable ReadReferences() + => _definition.MainModule.AssemblyReferences.Select(CecilAssemblyReader.MapNameToIdentity); + + public AssemblySourceLanguage ReadSourceLanguage() + => LanguageDetector.DetectLanguageFromAssembly(_definition); + + public TargetFramework ReadTargetFramework() + { + var targetFramework = GetCustomAttributeValue(); + var productAttribute = GetCustomAttributeValue(); + + // Why did I even write this for Silverlight... + var isSilverlight = productAttribute == "Microsoft® Silverlight"; + if (isSilverlight) + { + var versionAttribute = GetCustomAttributeValue(); + if (Version.TryParse(versionAttribute, out var parsedVersion) || _definition.Name.Version.Major >= 5) + { + return new TargetFramework(NetRuntime.Silverlight, parsedVersion ?? _definition.Name.Version); + } + } + + if (targetFramework != null) + { + return TargetFramework.Parse(targetFramework); + } + + foreach (var reference in _definition.MainModule.AssemblyReferences) + { + switch (reference.Name) + { + case "netstandard": + return new TargetFramework(NetRuntime.Standard, + new Version(reference.Version.Major, reference.Version.Minor)); + case "System.Runtime" when reference.Version >= new Version(4, 2, 0): + var version = "2.0"; + if (reference.Version >= new Version(6, 0)) + { + version = "6.0"; + } + else if (reference.Version >= new Version(5, 0)) + { + version = "5.0"; + } + else if (reference.Version >= new Version(4, 2, 1)) + { + version = "3.0"; + } + else if (reference.Version >= new Version(4, 2, 2)) + { + version = "3.1"; + } + + return new TargetFramework(NetRuntime.Core, new Version(version)); + case "mscorlib" when reference.Version.Major < 5: + return new TargetFramework(NetRuntime.Framework, reference.Version); + case "mscorlib" when reference.Version.Major >= 5: + return new TargetFramework(NetRuntime.Silverlight, reference.Version); + } + } + + return new TargetFramework(NetRuntime.Framework, + new Version(_definition.MainModule.Runtime switch + { + TargetRuntime.Net_1_0 => "1.0", + TargetRuntime.Net_1_1 => "1.1", + TargetRuntime.Net_2_0 => "2.0", + TargetRuntime.Net_4_0 => "4.0", + _ => throw new Exception( + $"TargetRuntime {_definition.MainModule.Runtime} of '{_definition.FullName}' is not supported") + })); + } + + private string? GetCustomAttributeValue() + { + return _definition + .CustomAttributes + .SingleOrDefault(a => a.AttributeType.FullName == typeof(T).FullName) + ?.ConstructorArguments.FirstOrDefault().Value?.ToString(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + _definition.Dispose(); + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Readers/Cecil/GeneratedNameHelper.cs b/src/RefScout.Analyzer/Readers/Cecil/GeneratedNameHelper.cs new file mode 100644 index 0000000..6872df7 --- /dev/null +++ b/src/RefScout.Analyzer/Readers/Cecil/GeneratedNameHelper.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; +using System.Runtime.CompilerServices; +using Mono.Cecil; + +namespace RefScout.Analyzer.Readers.Cecil; + +internal static class GeneratedNameHelper +{ + private static int IndexOfBalancedParenthesis(string str, int openingOffset, char closing) + { + var opening = str[0]; + var depth = 1; + for (var i = openingOffset + 1; i < str.Length; i++) + { + var c = str[i]; + if (c == opening) + { + depth++; + } + else if (c == closing) + { + depth--; + if (depth == 0) + { + return i; + } + } + } + + return -1; + } + + public static bool IsCSharpGeneratedName(string name) + { + const string csPrefix = "CS$<"; + const char closingCharacter = '>'; + const string openingCharacter = "<"; + + if (name.StartsWith(csPrefix, StringComparison.Ordinal)) + { + return true; + } + + var openBracketOffset = -1; + if (name.StartsWith(openingCharacter, StringComparison.Ordinal)) + { + openBracketOffset = 0; + } + + if (openBracketOffset < 0) + { + return false; + } + + var closeBracketOffset = IndexOfBalancedParenthesis(name, openBracketOffset, closingCharacter); + if (closeBracketOffset < 0 || closeBracketOffset + 1 >= name.Length) + { + return false; + } + + int c = name[closeBracketOffset + 1]; + return c is (>= '1' and <= '9') or (>= 'a' and <= 'z'); + } + + //System.EventHandler`1 + public static bool IsVbBackingField(FieldDefinition field) + { + const string goodPrefix = "_"; + const string badPrefix = "__"; + + if (field.Name.StartsWith(badPrefix, StringComparison.Ordinal) || + !field.Name.StartsWith(goodPrefix, StringComparison.Ordinal) || + !field.HasCustomAttributes) + { + return false; + } + + return !field.FieldType.FullName.StartsWith(typeof(EventHandler).FullName!, + StringComparison.OrdinalIgnoreCase) && + field.CustomAttributes.Any(attribute => + attribute.AttributeType.FullName == typeof(CompilerGeneratedAttribute).FullName); + } + + public static bool IsVbNetGeneratedName(string name) + { + const string vbPrefixOne = "VB$"; + const string vbPrefixTwo = "$VB$"; + const string closurePrefix = "_Closure$"; + + return name.StartsWith(vbPrefixOne, StringComparison.Ordinal) || + name.StartsWith(vbPrefixTwo, StringComparison.Ordinal) || + name.StartsWith(closurePrefix, StringComparison.Ordinal); + } + + public static bool IsCppCliNamespace(TypeDefinition type) + { + const string cppNamespace = ""; + return type.Namespace.Equals(cppNamespace, StringComparison.Ordinal); + } + + public static bool IsFSharpNamespace(TypeDefinition type) + { + const string generatedNamespacePrefix = " AssemblyToSourceLanguageMapping = new() + { + { "FSharp.Core", AssemblySourceLanguage.FSharp }, + { "Mono.CSharp", AssemblySourceLanguage.CSharp }, + { "Microsoft.CSharp", AssemblySourceLanguage.CSharp }, + { "Microsoft.VisualBasic", AssemblySourceLanguage.VbNet } + }; + + public static AssemblySourceLanguage DetectLanguageFromAssembly(AssemblyDefinition definition) + { + _ = definition ?? throw new ArgumentNullException(nameof(definition)); + + AssemblySourceLanguage language; + foreach (var type in definition.MainModule.Types) + { + if (GeneratedNameHelper.IsCppCliNamespace(type)) + { + return AssemblySourceLanguage.CppCli; + } + + if (GeneratedNameHelper.IsFSharpNamespace(type)) + { + return AssemblySourceLanguage.FSharp; + } + + language = GetLanguageFromName(type.Name); + if (language != AssemblySourceLanguage.Unknown) + { + return language; + } + } + + language = AnalyzeTypesDeep(definition.MainModule.Types); + return language != AssemblySourceLanguage.Unknown + ? language + : DetermineFromReferences(definition.MainModule.AssemblyReferences); + } + + private static AssemblySourceLanguage DetermineFromReferences(IEnumerable references) + { + foreach (var reference in references) + { + if (AssemblyToSourceLanguageMapping.ContainsKey(reference.Name)) + { + return AssemblyToSourceLanguageMapping[reference.Name]; + } + } + + return AssemblySourceLanguage.Unknown; + } + + private static AssemblySourceLanguage GetLanguageFromName(string name) + { + if (GeneratedNameHelper.IsCSharpGeneratedName(name)) + { + return AssemblySourceLanguage.CSharp; + } + + if (GeneratedNameHelper.IsVbNetGeneratedName(name)) + { + return AssemblySourceLanguage.VbNet; + } + + return AssemblySourceLanguage.Unknown; + } + + private static AssemblySourceLanguage AnalyzeTypesDeep(IEnumerable types) + { + foreach (var type in types) + { + var language = GetLanguageFromName(type.Name); + if (language != AssemblySourceLanguage.Unknown) + { + return language; + } + + foreach (var field in type.Fields) + { + language = GetLanguageFromName(field.Name); + if (language != AssemblySourceLanguage.Unknown) + { + return language; + } + + if (GeneratedNameHelper.IsVbBackingField(field)) + { + return AssemblySourceLanguage.VbNet; + } + } + + foreach (var method in type.Methods) + { + language = GetLanguageFromName(method.Name); + if (language != AssemblySourceLanguage.Unknown) + { + return language; + } + } + + language = AnalyzeTypesDeep(type.NestedTypes); + if (language != AssemblySourceLanguage.Unknown) + { + return language; + } + } + + return AssemblySourceLanguage.Unknown; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Readers/IAssemblyReader.cs b/src/RefScout.Analyzer/Readers/IAssemblyReader.cs new file mode 100644 index 0000000..b2ec573 --- /dev/null +++ b/src/RefScout.Analyzer/Readers/IAssemblyReader.cs @@ -0,0 +1,12 @@ +namespace RefScout.Analyzer.Readers; + +internal interface IAssemblyReader +{ + public Assembly Read(string fileName, AssemblySource source, bool applyMetadata = true); + + public Assembly ReadOrDefault( + string fileName, + AssemblySource source, + AssemblyIdentity defaultIdentity, + bool applyMetadata = true); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Readers/IMetadataReader.cs b/src/RefScout.Analyzer/Readers/IMetadataReader.cs new file mode 100644 index 0000000..d8e3f25 --- /dev/null +++ b/src/RefScout.Analyzer/Readers/IMetadataReader.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace RefScout.Analyzer.Readers; + +internal interface IMetadataReader : IDisposable +{ + IEnumerable ReadReferences(); + AssemblySourceLanguage ReadSourceLanguage(); + TargetFramework ReadTargetFramework(); + AssemblyKind ReadKind(); + (string architectureString, ProcessorArchitecture architecture, bool is64Bit) ReadProcessorArchitecture(); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/RefScout.Analyzer.csproj b/src/RefScout.Analyzer/RefScout.Analyzer.csproj new file mode 100644 index 0000000..a4eb9a4 --- /dev/null +++ b/src/RefScout.Analyzer/RefScout.Analyzer.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + win-x64;linux-x64 + enable + true + + + + + + + + + + + + + + + + + RefScout.Ipc.FrameworkRuntime.exe + + + \ No newline at end of file diff --git a/src/RefScout.Analyzer/ReferenceAnalyzer.cs b/src/RefScout.Analyzer/ReferenceAnalyzer.cs new file mode 100644 index 0000000..e37ead0 --- /dev/null +++ b/src/RefScout.Analyzer/ReferenceAnalyzer.cs @@ -0,0 +1,323 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using System.Threading; +using RefScout.Analyzer.Analyzers.Assemblies; +using RefScout.Analyzer.Analyzers.Compatibility; +using RefScout.Analyzer.Analyzers.Environment; +using RefScout.Analyzer.Config; +using RefScout.Analyzer.Context; +using RefScout.Analyzer.Filter; +using RefScout.Analyzer.Helpers; +using RefScout.Analyzer.Notes; +using RefScout.Analyzer.Readers; +using RefScout.Analyzer.Readers.Cecil; +using RefScout.Analyzer.Resolvers; +using RefScout.Core.Logging; +using SingleFileExtractor.Core; + +namespace RefScout.Analyzer; + +public class ReferenceAnalyzer : IAnalyzer +{ + private readonly IFileSystem _fileSystem; + private readonly IAssemblyReader _reader; + + private readonly IResolverFactory _resolverFactory; + private readonly IConfigParserFactory _configParserFactory; + private readonly IContextFactory _contextFactory; + private readonly ICompatibilityAnalyzerFactory _compatibilityAnalyzerFactory; + + private readonly IEnvironmentAnalyzer _environmentAnalyzer; + private readonly IAssemblyAnalyzer _referencedAssembliesAnalyzer; + private readonly IAssemblyAnalyzer _unreferencedAssembliesAnalyzer; + private readonly INoteGenerator _noteGenerator; + + private string? _extractionDirectory; + + public ReferenceAnalyzer() + { + IEnvironment environment = new EnvironmentWrapper(); + _fileSystem = new FileSystem(); + _reader = new CecilAssemblyReader(); + _environmentAnalyzer = new EnvironmentAnalyzer(environment, _fileSystem); + _resolverFactory = new ResolverFactory(environment, _fileSystem, _environmentAnalyzer); + _configParserFactory = new ConfigParserFactory(_fileSystem); + _contextFactory = new ContextFactory(); + _compatibilityAnalyzerFactory = new CompatibilityAnalyzerFactory(); + _referencedAssembliesAnalyzer = new ReferencedAssembliesAnalyzer(); + _unreferencedAssembliesAnalyzer = new UnreferencedAssembliesAnalyzer(_fileSystem); + _noteGenerator = new NoteGenerator(environment); + } + + internal ReferenceAnalyzer( + IFileSystem fileSystem, + IAssemblyReader reader, + IResolverFactory resolverFactory, + IConfigParserFactory configParserFactory, + IContextFactory contextFactory, + ICompatibilityAnalyzerFactory compatibilityAnalyzerFactory, + IEnvironmentAnalyzer environmentAnalyzer, + IAssemblyAnalyzer referencedAssembliesAnalyzer, + IAssemblyAnalyzer unreferencedAssembliesAnalyzer, + INoteGenerator noteGenerator) + { + _fileSystem = fileSystem; + _reader = reader; + _resolverFactory = resolverFactory; + _configParserFactory = configParserFactory; + _contextFactory = contextFactory; + _compatibilityAnalyzerFactory = compatibilityAnalyzerFactory; + _environmentAnalyzer = environmentAnalyzer; + _referencedAssembliesAnalyzer = referencedAssembliesAnalyzer; + _unreferencedAssembliesAnalyzer = unreferencedAssembliesAnalyzer; + _noteGenerator = noteGenerator; + } + + public IAnalyzerResult Analyze( + string fileName, + AnalyzerOptions? options = null, + CancellationToken? cancellationToken = null) + { + _ = fileName ?? throw new ArgumentNullException(nameof(fileName)); + cancellationToken ??= CancellationToken.None; + + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentException("File name to be analyzed cannot be empty.", nameof(fileName)); + } + + Logger.Info($"Started analyzing \"{fileName}\"."); + + options ??= new AnalyzerOptions(); + options.Config ??= $"{fileName}.config"; + + // This only applies to .NET Core applications, but at this point we have no idea yet + // whether it is a framework or core application. + fileName = ResolveCoreEntryPoint(fileName); + + try + { + var entryPoint = ReadEntryPoint(fileName); + return RunAnalyzer(fileName, options, entryPoint, cancellationToken.Value); + } + finally + { + CleanUp(); + } + } + + public static IAnalyzerResult Run( + string fileName, + AnalyzerOptions options, + CancellationToken? cancellationToken = null) + { + var analyzer = new ReferenceAnalyzer(); + return analyzer.Analyze(fileName, options, cancellationToken); + } + + private Assembly ReadEntryPoint(string fileName) + { + if (!_fileSystem.File.Exists(fileName)) + { + throw new Exception($"Assembly file or executable does not exist: \"{fileName}\"."); + } + + return _reader.Read(fileName, AssemblySource.Local) with + { + IsEntryPoint = true + }; + } + + private IAnalyzerResult RunAnalyzer( + string fileName, + AnalyzerOptions options, + Assembly entryPoint, + CancellationToken cancellationToken) + { + var environmentInfo = _environmentAnalyzer.Analyze(); + + var comparer = options.VersionComparer ?? new DefaultVersionComparer(options.SystemVersionMode); + using var context = _contextFactory.Create(_configParserFactory, _environmentAnalyzer, _resolverFactory, + _reader, options.AnalyzeRuntime, fileName, options, environmentInfo, entryPoint); + var compatibilityAnalyzer = _compatibilityAnalyzerFactory.Create(context, comparer); + + AnalyzeReferenced(context, options, cancellationToken); + AnalyzeUnreferenced(context, options, cancellationToken); + AnalyzeCompatibility(compatibilityAnalyzer); + + GenerateNotes(context); + + Filter(context, options); + Sort(context); + + return ConvertContextToResult(context); + } + + private void AnalyzeReferenced(IContext context, AnalyzerOptions options, CancellationToken cancellationToken) + { + _referencedAssembliesAnalyzer.Analyze(context, options, cancellationToken); + } + + private void AnalyzeUnreferenced(IContext context, AnalyzerOptions options, CancellationToken cancellationToken) + { + _unreferencedAssembliesAnalyzer.Analyze(context, options, cancellationToken); + } + + private static void AnalyzeCompatibility(ICompatibilityAnalyzer compatibilityAnalyzer) + { + compatibilityAnalyzer.Analyze(); + } + + private void GenerateNotes(IContext context) + { + _noteGenerator.Generate(context); + } + + private static void Filter(IContext context, AnalyzerOptions options) + { + if (options.Filter == null) + { + return; + } + + var assemblies = context.Assemblies.ToList(); + var predicate = FilterParser.Parse(options.Filter); + context.Assemblies = assemblies.Where(predicate).ToList(); + } + + private static void Sort(IContext context) + { + var assemblies = context.Assemblies.ToList(); + foreach (var assembly in assemblies) + { + assembly.References.Sort((a, b) => string.Compare(a.To.Name, b.To.Name, StringComparison.Ordinal)); + assembly.ReferencedBy.Sort((a, b) => + string.Compare(a.From.Name, b.From.Name, StringComparison.Ordinal)); + } + + assemblies = assemblies.OrderBy(assembly => + { + return assembly switch + { + { IsEntryPoint: true } => 10, + { IsUnreferenced: true } => 100, + not { IsNetApi: true } and not + { IsSystem: true } => assembly.ReferencedBy.Any(a => a.From.IsEntryPoint) ? 20 : 25, + { IsNetApi: true } => 30, + { IsSystem: true } => 40 + }; + }).ThenBy(a => a.Name).ToList(); + + context.Assemblies = assemblies; + } + + private static IAnalyzerResult ConvertContextToResult(IContext context) + { + return context switch + { + ICoreContext coreContext => new CoreAnalyzerResult(context.Assemblies, coreContext.Config, + coreContext.Runtime, coreContext.EnvironmentInfo), + IMonoContext monoContext => new MonoAnalyzerResult(context.Assemblies, + monoContext.MachineConfig, monoContext.Config, monoContext.Runtime, + context.EnvironmentInfo), + IFrameworkContext frameworkContext => new FrameworkAnalyzerResult(context.Assemblies, + frameworkContext.MachineConfig, frameworkContext.Config, frameworkContext.Runtime, + context.EnvironmentInfo), + _ => throw new NotSupportedException($"Cannot convert {context.GetType().Name} to AnalyzerResult") + }; + } + + private void CleanUp() + { + if (_extractionDirectory == null) + { + return; + } + + try + { + Directory.Delete(_extractionDirectory, true); + } + catch (Exception e) + { + Logger.Error(e, $"Could not delete single file extraction directory: {_extractionDirectory}"); + } + } + + private string ResolveCoreEntryPoint(string fileName) + { + var baseDirectory = Path.GetDirectoryName(fileName)!; + + var startupInfo = TryReadStartupInfo(fileName); + if (startupInfo != null) + { + var extractionDirectory = TryExtractCoreSingleFile(fileName); + if (extractionDirectory != null) + { + baseDirectory = extractionDirectory; + _extractionDirectory = extractionDirectory; + } + } + + if (startupInfo != null && startupInfo.EntryPoint != null) + { + return Path.Combine(baseDirectory, startupInfo.EntryPoint); + } + + var originalFileName = GetOriginalFileName(fileName); + return originalFileName != null ? Path.Combine(baseDirectory, originalFileName) : fileName; + } + + private static StartupInfo? TryReadStartupInfo(string fileName) + { + try + { + return new ExecutableReader().ReadStartupInfo(fileName); + } + catch (Exception e) + { + Logger.Info("Cannot extract .NET Core startup info: " + e.Message); + } + + return null; + } + + private string? TryExtractCoreSingleFile(string fileName) + { + try + { + var extractionDirectory = Path.Join(_fileSystem.Path.GetTempPath(), "RefScout", + Path.GetFileNameWithoutExtension(fileName)); + _ = BundleExtractor.Extract(fileName, extractionDirectory); + return extractionDirectory; + } + catch (Exception e) + { + Logger.Info("Cannot extract single file application: " + e.Message); + } + + return null; + } + + private static string? GetOriginalFileName(string fileName) + { + try + { + var fileInfo = FileVersionInfo.GetVersionInfo(fileName); + if (fileInfo.OriginalFilename != null && Path.GetFileName(fileName) != fileInfo.OriginalFilename && + Path.GetExtension(fileInfo.OriginalFilename) == ".dll") + { + return fileInfo.OriginalFilename; + } + } + catch (Exception e) + { + Logger.Error(e, "Error while trying to resolve entry assembly filename using FileVersionInfo."); + } + + return null; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/CoreResolver.cs b/src/RefScout.Analyzer/Resolvers/CoreResolver.cs new file mode 100644 index 0000000..89efc32 --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/CoreResolver.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using RefScout.Analyzer.Analyzers.Environment.Core; +using RefScout.Analyzer.Config.Core; +using RefScout.Analyzer.Helpers; +using RefScout.Analyzer.Resolvers.Strategies; +using RefScout.Analyzer.Resolvers.Strategies.Core; +using RefScout.Analyzer.Resolvers.Strategies.Shared; +using RefScout.Core.Logging; + +namespace RefScout.Analyzer.Resolvers; + +internal class CoreResolver : Resolver +{ + private readonly CoreConfig _config; + private readonly CoreRuntime? _runtime; + private readonly IEnvironment _environment; + private readonly IFileSystem _fileSystem; + private readonly TargetFramework _targetFramework; + + public CoreResolver( + IEnvironment environment, + IFileSystem fileSystem, + TargetFramework targetFramework, + CoreConfig config, + CoreRuntime? runtime) : base(targetFramework) + { + _environment = environment ?? throw new ArgumentNullException(nameof(environment)); + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _targetFramework = targetFramework ?? throw new ArgumentNullException(nameof(targetFramework)); + _config = config ?? throw new ArgumentNullException(nameof(config)); + _runtime = runtime; + } + + protected override IReadOnlyList GetResolverStrategies() + { + if (_runtime == null || _config.SelfContained) + { + if (_config.SelfContained) + { + Logger.Info("Application is a single file application, will not search in shared .NET Core folders."); + } + else + { + Logger.Info("No .NET Core runtime was found, will not search in shared .NET Core folders."); + } + + return new List + { + new DirectoryResolverStrategy(_fileSystem, SearchDirectories), + new CoreNuGetPackageResolverStrategy(_environment, _fileSystem, _targetFramework, _config.DepsFile) + }; + } + + return new List + { + new DirectoryResolverStrategy(_fileSystem, SearchDirectories), + new CoreNuGetPackageResolverStrategy(_environment, _fileSystem, _targetFramework, _config.DepsFile), + new DirectoryResolverStrategy(_fileSystem, SearchDirectories) + }; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/FrameworkResolver.cs b/src/RefScout.Analyzer/Resolvers/FrameworkResolver.cs new file mode 100644 index 0000000..62e6d6d --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/FrameworkResolver.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using RefScout.Analyzer.Helpers; +using RefScout.Analyzer.Resolvers.Strategies; +using RefScout.Analyzer.Resolvers.Strategies.Framework; +using RefScout.Analyzer.Resolvers.Strategies.Shared; + +namespace RefScout.Analyzer.Resolvers; + +internal class FrameworkResolver : Resolver +{ + private readonly IEnvironment _environment; + private readonly IFileSystem _fileSystem; + private readonly TargetFramework _targetFramework; + private readonly bool _is64Bit; + + public FrameworkResolver( + IEnvironment environment, + IFileSystem fileSystem, + TargetFramework targetFramework, + bool is64Bit) : base(targetFramework) + { + _environment = environment ?? throw new ArgumentNullException(nameof(environment)); + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _targetFramework = targetFramework ?? throw new ArgumentNullException(nameof(targetFramework)); + _is64Bit = is64Bit; + } + + protected override IReadOnlyList GetResolverStrategies() => + new List + { + new WindowsMetadataResolverStrategy(_environment, _fileSystem), + new SilverlightResolverStrategy(_environment, _fileSystem, _targetFramework), + new CorLibResolverStrategy(_environment, _fileSystem), + new FusionGacResolverStrategy(_environment, _fileSystem, _is64Bit), + new FileSystemGacResolverStrategy(_environment, _fileSystem), + new DirectoryResolverStrategy(_fileSystem, new List(SearchDirectories)), + new FrameworkProxyGacResolverStrategy(_environment, _fileSystem) + }; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/IResolver.cs b/src/RefScout.Analyzer/Resolvers/IResolver.cs new file mode 100644 index 0000000..0437452 --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/IResolver.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +namespace RefScout.Analyzer.Resolvers; + +internal interface IResolver : IDisposable +{ + public IReadOnlyList SearchDirectories { get; } + + public AssemblyResolverResult ResolvePath(AssemblyIdentity identity); + + void AddSearchDirectory(string directory); +} + +public record AssemblyResolverResult(AssemblySource Source, string? Path = null) +{ + public bool Unification { get; init; } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/IResolverFactory.cs b/src/RefScout.Analyzer/Resolvers/IResolverFactory.cs new file mode 100644 index 0000000..07235db --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/IResolverFactory.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using RefScout.Analyzer.Analyzers.Environment.Core; +using RefScout.Analyzer.Config.Core; + +namespace RefScout.Analyzer.Resolvers; + +internal interface IResolverFactory +{ + IResolver CreateCoreResolver( + string mainAssemblyFileName, + Assembly assembly, + CoreConfig config, + CoreRuntime? runtime); + + IResolver CreateFrameworkResolver( + string mainAssemblyFileName, + Assembly assembly, + IEnumerable probeFolders); + + IResolver CreateMonoResolver( + string mainAssemblyFileName, + Assembly assembly, + IEnumerable probeFolders); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/MonoResolver.cs b/src/RefScout.Analyzer/Resolvers/MonoResolver.cs new file mode 100644 index 0000000..adc8f35 --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/MonoResolver.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using RefScout.Analyzer.Analyzers.Environment.Mono; +using RefScout.Analyzer.Resolvers.Strategies; +using RefScout.Analyzer.Resolvers.Strategies.Mono; +using RefScout.Analyzer.Resolvers.Strategies.Shared; + +namespace RefScout.Analyzer.Resolvers; + +internal class MonoResolver : Resolver +{ + private readonly IFileSystem _fileSystem; + private readonly IMonoRuntimeAnalyzer _monoRuntimeAnalyzer; + + public MonoResolver( + IFileSystem fileSystem, + IMonoRuntimeAnalyzer monoRuntimeAnalyzer, + TargetFramework targetFramework) : base(targetFramework) + { + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _monoRuntimeAnalyzer = monoRuntimeAnalyzer ?? throw new ArgumentNullException(nameof(monoRuntimeAnalyzer)); + } + + protected override IReadOnlyList GetResolverStrategies() => + new List + { + new DirectoryResolverStrategy(_fileSystem, new List(SearchDirectories)), + new MonoCorLibResolverStrategy(_fileSystem, _monoRuntimeAnalyzer), + new MonoRuntimeResolverStrategy(_fileSystem, _monoRuntimeAnalyzer), + new MonoGacResolverStrategy(_fileSystem, _monoRuntimeAnalyzer) + }; +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/Resolver.cs b/src/RefScout.Analyzer/Resolvers/Resolver.cs new file mode 100644 index 0000000..0213d65 --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/Resolver.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using RefScout.Analyzer.Resolvers.Strategies; +using RefScout.Core.Logging; + +namespace RefScout.Analyzer.Resolvers; + +// A universal way of resolving assemblies with support for .NET Core and Framework +internal abstract class Resolver : IResolver +{ + private readonly List _directories; + private readonly TargetFramework _targetFramework; + + private IReadOnlyList? _strategies; + + protected Resolver(TargetFramework targetFramework) + { + _targetFramework = targetFramework; + _directories = new List(); + } + + public IReadOnlyList SearchDirectories => _directories; + + public AssemblyResolverResult ResolvePath(AssemblyIdentity identity) + { + _ = identity ?? throw new ArgumentNullException(nameof(identity)); + + + _strategies ??= GetResolverStrategies(); + + foreach (var strategy in _strategies) + { + if (!strategy.Test(identity, _targetFramework)) + { + continue; + } + + var result = TryResolveWithMethod(strategy, identity); + if (result != null) + { + return result; + } + } + + return new AssemblyResolverResult(AssemblySource.NotFound); + } + + public void AddSearchDirectory(string directory) + { + if (_directories.Contains(directory)) + { + return; + } + + _directories.Add(directory); + } + + private static AssemblyResolverResult? TryResolveWithMethod(IResolverStrategy method, AssemblyIdentity identity) + { + try + { + return method.Resolve(identity); + } + catch (Exception e) + { + Logger.Warn(e, $"Error occurred while running assembly resolver method {method.GetType().Name}"); + } + + return null; + } + + protected abstract IReadOnlyList GetResolverStrategies(); + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!disposing || _strategies == null) + { + return; + } + + foreach (var method in _strategies) + { + method.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/ResolverFactory.cs b/src/RefScout.Analyzer/Resolvers/ResolverFactory.cs new file mode 100644 index 0000000..aba13a7 --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/ResolverFactory.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using RefScout.Analyzer.Analyzers.Environment; +using RefScout.Analyzer.Analyzers.Environment.Core; +using RefScout.Analyzer.Config.Core; +using RefScout.Analyzer.Helpers; + +namespace RefScout.Analyzer.Resolvers; + +internal class ResolverFactory : IResolverFactory +{ + private readonly IEnvironment _environment; + private readonly IFileSystem _fileSystem; + private readonly IEnvironmentAnalyzer _environmentAnalyzer; + + public ResolverFactory( + IEnvironment environment, + IFileSystem fileSystem, + IEnvironmentAnalyzer environmentAnalyzer) + { + _environment = environment ?? throw new ArgumentNullException(nameof(environment)); + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _environmentAnalyzer = environmentAnalyzer ?? throw new ArgumentNullException(nameof(environmentAnalyzer)); + } + + public IResolver CreateCoreResolver( + string mainAssemblyFileName, + Assembly assembly, + CoreConfig config, + CoreRuntime? runtime) + { + if (assembly.TargetFramework == null) + { + throw new Exception("Resolvers require TargetFramework for entry point assembly to be specified."); + } + + var resolver = new CoreResolver(_environment, _fileSystem, assembly.TargetFramework, config, + runtime); + + var workingDirectory = Path.GetDirectoryName(mainAssemblyFileName); + if (workingDirectory != null) + { + resolver.AddSearchDirectory(workingDirectory); + } + + return resolver; + } + + public IResolver CreateFrameworkResolver( + string mainAssemblyFileName, + Assembly assembly, + IEnumerable probeFolders) + { + if (assembly.TargetFramework == null) + { + throw new Exception("Resolvers require TargetFramework for entry point assembly to be specified."); + } + + var resolver = new FrameworkResolver(_environment, _fileSystem, assembly.TargetFramework, assembly.Is64Bit); + return AddDirectories(mainAssemblyFileName, probeFolders, resolver); + } + + public IResolver CreateMonoResolver( + string mainAssemblyFileName, + Assembly assembly, + IEnumerable probeFolders) + { + if (assembly.TargetFramework == null) + { + throw new Exception("Resolvers require TargetFramework for entry point assembly to be specified."); + } + + var resolver = new MonoResolver(_fileSystem, _environmentAnalyzer.MonoRuntimeAnalyzer, + assembly.TargetFramework); + return AddDirectories(mainAssemblyFileName, probeFolders, resolver); + } + + private static IResolver AddDirectories( + string mainAssemblyFileName, + IEnumerable directories, + IResolver resolver) + { + var workingDirectory = Path.GetDirectoryName(mainAssemblyFileName); + if (workingDirectory == null) + { + return resolver; + } + + resolver.AddSearchDirectory(workingDirectory); + foreach (var directory in directories) + { + resolver.AddSearchDirectory(Path.Combine(workingDirectory, directory)); + } + + return resolver; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/Strategies/Core/CoreNuGetPackageResolverStrategy.cs b/src/RefScout.Analyzer/Resolvers/Strategies/Core/CoreNuGetPackageResolverStrategy.cs new file mode 100644 index 0000000..bab2c3f --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/Strategies/Core/CoreNuGetPackageResolverStrategy.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using RefScout.Analyzer.Config.Core; +using RefScout.Analyzer.Helpers; + +namespace RefScout.Analyzer.Resolvers.Strategies.Core; + +// This code is not pretty and should be simplified, wrapping the deps file information in +// another object is very pointless +internal class CoreNuGetPackageResolverStrategy : IResolverStrategy +{ + private readonly IEnvironment _environment; + private readonly IFileSystem _fileSystem; + + private readonly List _searchDirectories; + private readonly TargetFramework _targetFramework; + private readonly DepsFile _depsFile; + + private bool _initialized; + + public CoreNuGetPackageResolverStrategy( + IEnvironment environment, + IFileSystem fileSystem, + TargetFramework targetFramework, + DepsFile depsFile) + { + _environment = environment; + _fileSystem = fileSystem; + _targetFramework = targetFramework; + _depsFile = depsFile; + _searchDirectories = new List(); + } + + public bool Test(AssemblyIdentity identity, TargetFramework? targetFramework) => + targetFramework?.Runtime is NetRuntime.Core or NetRuntime.Standard; + + public AssemblyResolverResult? Resolve(AssemblyIdentity identity) + { + // Initializing is a 'heavy' task, so only do when necessary + if (!_initialized) + { + Initialize(); + } + + foreach (var basePath in _searchDirectories) + { + if (_fileSystem.File.Exists(Path.Combine(basePath, identity.Name + ".dll"))) + { + return new AssemblyResolverResult(AssemblySource.Local, + Path.Combine(basePath, identity.Name + ".dll")); + } + + if (_fileSystem.File.Exists(Path.Combine(basePath, identity.Name + ".exe"))) + { + return new AssemblyResolverResult(AssemblySource.Local, + Path.Combine(basePath, identity.Name + ".exe")); + } + } + + return null; + } + + private void Initialize() + { + _initialized = true; + + var packages = LoadPackageInfos(_targetFramework.Id).ToArray(); + foreach (var path in GetLookupPaths()) + { + foreach (var p in packages) + { + foreach (var item in p.RuntimeComponents) + { + var itemPath = _fileSystem.Path.GetDirectoryName(item); + + if (itemPath == null) + { + continue; + } + + var fullPath = Path.Combine(path, p.Name, p.Version, itemPath).ToLowerInvariant(); + if (_fileSystem.Directory.Exists(fullPath)) + { + _searchDirectories.Add(fullPath); + } + } + } + } + } + + private IEnumerable GetLookupPaths() + { + yield return Path.Combine(_environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", + "packages"); + } + + private IEnumerable LoadPackageInfos( + string targetFramework) + { + if (_depsFile.Libraries == null) + { + yield break; + } + + var runtimeInfos = _depsFile.Targets?.FirstOrDefault(target => target.Key + .StartsWith(targetFramework, StringComparison.OrdinalIgnoreCase) && target.Value.Count > 0).Value; + if (runtimeInfos == null) + { + yield break; + } + + foreach (var (key, _) in _depsFile.Libraries) + { + if (!runtimeInfos.ContainsKey(key)) + { + continue; + } + + var runtimeInfo = runtimeInfos[key].Runtime; + var components = new string[runtimeInfo?.Count ?? 0]; + if (runtimeInfo != null) + { + var i = 0; + foreach (var component in runtimeInfo) + { + components[i] = component.Key; + i++; + } + } + + yield return new DotNetCorePackageInfo(key, components); + } + } + + private class DotNetCorePackageInfo + { + public DotNetCorePackageInfo(string fullName, string[] runtimeComponents) + { + var parts = fullName.Split('/'); + Name = parts[0]; + Version = parts.Length > 1 ? parts[1] : ""; + + RuntimeComponents = runtimeComponents; + } + + public string Name { get; } + public string Version { get; } + public string[] RuntimeComponents { get; } + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/Strategies/Core/CoreSharedResolverStrategy.cs b/src/RefScout.Analyzer/Resolvers/Strategies/Core/CoreSharedResolverStrategy.cs new file mode 100644 index 0000000..dc5b211 --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/Strategies/Core/CoreSharedResolverStrategy.cs @@ -0,0 +1,56 @@ +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using RefScout.Analyzer.Analyzers.Environment.Core; + +namespace RefScout.Analyzer.Resolvers.Strategies.Core; + +public class CoreSharedResolverStrategy : IResolverStrategy +{ + private readonly IFileSystem _fileSystem; + private readonly CoreRuntime _runtime; + private readonly string _preferredRuntimePack; + + public CoreSharedResolverStrategy( + IFileSystem fileSystem, + CoreRuntime runtime, + RuntimePack runtimePack) + { + _fileSystem = fileSystem; + _runtime = runtime; + _preferredRuntimePack = CoreRuntimeAnalyzer.PackToString[runtimePack]; + } + + public bool Test(AssemblyIdentity identity, TargetFramework? targetFramework) => + targetFramework?.Runtime is NetRuntime.Core or NetRuntime.Standard; + + public AssemblyResolverResult? Resolve(AssemblyIdentity identity) + { + var path = ResolveFromSharedRuntime(identity); + return path != null + ? new AssemblyResolverResult(AssemblySource.Shared, path) + : null; + } + + private string? ResolveFromSharedRuntime(AssemblyIdentity identity) + { + var packs = CoreRuntimeAnalyzer.Packs.ToList(); + packs.Insert(0, _preferredRuntimePack); + foreach (var pack in packs) + { + var packDirectory = Path.Combine(_runtime.Path, pack, _runtime.VersionName); + if (!_fileSystem.Directory.Exists(packDirectory)) + { + continue; + } + + var pathToAssembly = Path.Combine(packDirectory, identity.Name + ".dll"); + if (_fileSystem.File.Exists(pathToAssembly)) + { + return pathToAssembly; + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/Strategies/Framework/CorLibResolverStrategy.cs b/src/RefScout.Analyzer/Resolvers/Strategies/Framework/CorLibResolverStrategy.cs new file mode 100644 index 0000000..f401e37 --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/Strategies/Framework/CorLibResolverStrategy.cs @@ -0,0 +1,86 @@ +using System; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using RefScout.Analyzer.Helpers; + +namespace RefScout.Analyzer.Resolvers.Strategies.Framework; + +internal class CorLibResolverStrategy : IResolverStrategy +{ + private const string CorLibName = "mscorlib"; + private static readonly PublicKeyToken CompactPublicKeyToken = PublicKeyToken.Parse("969db8053d3322ac"); + private readonly IEnvironment _environment; + private readonly IFileSystem _fileSystem; + + public CorLibResolverStrategy(IEnvironment environment, IFileSystem fileSystem) + { + _environment = environment; + _fileSystem = fileSystem; + } + + public bool Test(AssemblyIdentity identity, TargetFramework? targetFramework) => + identity.Name == CorLibName && targetFramework?.Runtime is NetRuntime.Framework && + _environment.OSVersion.Platform == PlatformID.Win32NT && + identity.IsStrongNamed; + + public AssemblyResolverResult? Resolve(AssemblyIdentity identity) + { + var path = GetCorLib(identity); + return path != null ? new AssemblyResolverResult(AssemblySource.Gac, path) : null; + } + + private string? GetCorLib(AssemblyIdentity identity) + { + var path = GetMscorlibBasePath(identity.Version, identity.PublicKeyToken); + if (path == null) + { + return null; + } + + var file = Path.Combine(path, "mscorlib.dll"); + return _fileSystem.File.Exists(file) ? file : null; + } + + private string? GetMscorlibBasePath(Version version, PublicKeyToken publicKeyToken) + { + if (publicKeyToken == CompactPublicKeyToken) + { + var programFiles = _environment.Is64BitOperatingSystem + ? _environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + : _environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + var cfPath = $@"Microsoft.NET\SDK\CompactFramework\v{version.Major}.{version.Minor}\WindowsCE\"; + var cfBasePath = Path.Combine(programFiles, cfPath); + if (_fileSystem.Directory.Exists(cfBasePath)) + { + return cfBasePath; + } + } + else + { + var rootPath = Path.Combine(_environment.GetFolderPath(Environment.SpecialFolder.Windows), + "Microsoft.NET"); + string[] frameworkPaths = + { + Path.Combine(rootPath, "Framework64"), + Path.Combine(rootPath, "Framework") + }; + + var folder = version.Major switch + { + 1 => version.MajorRevision == 3300 ? "v1.0.3705" : "v1.1.4322", + 2 => "v2.0.50727", + 4 => "v4.0.30319", + _ => null + }; + if (folder != null) + { + return frameworkPaths + .Select(path => Path.Combine(path, folder)) + .FirstOrDefault(_fileSystem.Directory.Exists); + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/Strategies/Framework/FileSystemGacResolverStrategy.cs b/src/RefScout.Analyzer/Resolvers/Strategies/Framework/FileSystemGacResolverStrategy.cs new file mode 100644 index 0000000..01c61d4 --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/Strategies/Framework/FileSystemGacResolverStrategy.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using System.Text; +using RefScout.Analyzer.Helpers; + +namespace RefScout.Analyzer.Resolvers.Strategies.Framework; + +internal class FileSystemGacResolverStrategy : IResolverStrategy +{ + private readonly IEnvironment _environment; + private readonly IFileSystem _fileSystem; + + public FileSystemGacResolverStrategy(IEnvironment environment, IFileSystem fileSystem) + { + _environment = environment; + _fileSystem = fileSystem; + } + + public bool Test(AssemblyIdentity identity, TargetFramework? targetFramework) => + targetFramework?.Runtime is NetRuntime.Framework && _environment.OSVersion.Platform is PlatformID.Win32NT && + identity.IsStrongNamed; + + public AssemblyResolverResult? Resolve(AssemblyIdentity identity) + { + var path = GetAssemblyInGac(identity); + return path != null ? new AssemblyResolverResult(AssemblySource.Gac, path) : null; + } + + private IReadOnlyList GetGacPaths() + { + var paths = new List(); + var windir = _environment.GetFolderPath(Environment.SpecialFolder.Windows); + paths.Add(Path.Combine(windir, "assembly")); + paths.Add(Path.Combine(windir, "Microsoft.NET", "assembly")); + return paths; + } + + private string? GetAssemblyInGac(AssemblyIdentity identity) + { + var gacPaths = GetGacPaths(); + var gacs = new[] { "GAC_MSIL", "GAC_32", "GAC_64", "GAC" }; + var prefixes = new[] { string.Empty, "v4.0_" }; + + for (var i = 0; i < gacPaths.Count; i++) + { + foreach (var gac in gacs) + { + var pathToGac = Path.Combine(gacPaths.ElementAt(i), gac); + var file = GetAssemblyFile(identity, prefixes[i], pathToGac); + if (_fileSystem.Directory.Exists(pathToGac) && _fileSystem.File.Exists(file)) + { + return file; + } + } + } + + return null; + } + + private static string GetAssemblyFile(AssemblyIdentity identity, string? prefix, string gac) + { + var gacFolder = new StringBuilder() + .Append(prefix) + .Append(identity.Version); + gacFolder.Append("__"); + gacFolder.Append(identity.PublicKeyToken); + + return Path.Combine(gac, identity.Name, gacFolder.ToString(), identity.Name + ".dll"); + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/Strategies/Framework/FrameworkProxyGacResolverStrategy.cs b/src/RefScout.Analyzer/Resolvers/Strategies/Framework/FrameworkProxyGacResolverStrategy.cs new file mode 100644 index 0000000..771c3d6 --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/Strategies/Framework/FrameworkProxyGacResolverStrategy.cs @@ -0,0 +1,130 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Abstractions; +using RefScout.Analyzer.Helpers; +using RefScout.Core.Logging; +using RefScout.IPC.Client; +#if !DEBUG +using RefScout.Core.Helpers; +#endif + +namespace RefScout.Analyzer.Resolvers.Strategies.Framework +{ + internal class FrameworkProxyGacResolverStrategy : IResolverStrategy + { + private const string ProcessName = "RefScout.Ipc.FrameworkRuntime.exe"; + private const string ErrorResponse = "error"; + + private readonly IEnvironment _environment; + private readonly IFileSystem _fileSystem; + private readonly IIpcClient _ipcClient; + + private string? _ipcPath; + + [ExcludeFromCodeCoverage] + public FrameworkProxyGacResolverStrategy(IEnvironment environment, IFileSystem fileSystem) : this(environment, + fileSystem, new PipeIpcClient()) { } + + public FrameworkProxyGacResolverStrategy(IEnvironment environment, IFileSystem fileSystem, IIpcClient ipcClient) + { + _environment = environment; + _fileSystem = fileSystem; + _ipcClient = ipcClient; + } + + public bool Test(AssemblyIdentity identity, TargetFramework? targetFramework) => + targetFramework?.Runtime is NetRuntime.Framework && + _environment.OSVersion.Platform is PlatformID.Win32NT && + identity.IsStrongNamed; + + public AssemblyResolverResult? Resolve(AssemblyIdentity identity) + { + var (version, path) = ResolveFromProxy(identity.FullName); + if (path == null || version == null) + { + return null; + } + + var unification = identity.Version != version; + if (unification) + { + Logger.Info( + $"Assembly \"{identity.FullName}\" resolved from GAC using unification from {identity.Version} to {version}."); + } + + return new AssemblyResolverResult(AssemblySource.Gac, path) + { + Unification = unification + }; + } + + + private void StartProxy() + { +#if DEBUG + // Use local variant for debug builds + _ipcPath = Path.Combine(@"..\..\..\..\RefScout.IPC.FrameworkRuntime\bin\Debug\net472", + ProcessName); +#else + // Use embedded resource for release builds + _ipcPath = Path.Combine(_environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "RefScout", ProcessName); + if (!_fileSystem.File.Exists(_ipcPath)) + { + if (!ResourceHelper.ExtractResourceToFile(_fileSystem, ProcessName, + _ipcPath)) + { + throw new Exception("Could not extract framework runtime proxy from assembly resources"); + } + } +#endif + + _ipcClient.Start(_ipcPath); + } + + private (Version? version, string? path) ResolveFromProxy(string assemblyName) + { + if (!_ipcClient.Started) + { + StartProxy(); + } + + try + { + var result = _ipcClient.Send(assemblyName); + if (result == ErrorResponse) + { + return (null, null); + } + + var split = result.Split('|'); + return (new Version(split[0]), split[1]); + } + catch (Exception ex) + { + Logger.Error(ex, "Failed to resolve assembly using the FrameworkRuntime process"); + return (null, null); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + if (_ipcClient.Started) + { + _ipcClient.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/Strategies/Framework/FusionGacResolverStrategy.cs b/src/RefScout.Analyzer/Resolvers/Strategies/Framework/FusionGacResolverStrategy.cs new file mode 100644 index 0000000..f7e7d88 --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/Strategies/Framework/FusionGacResolverStrategy.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using System.Runtime.InteropServices; +using RefScout.Analyzer.Helpers; + +namespace RefScout.Analyzer.Resolvers.Strategies.Framework; + +internal class FusionGacResolverStrategy : IResolverStrategy +{ + private static readonly string[] FrameworkVersions = { "v4.0.30319", "v2.0.50727", "v1.1.4322", "v1.0.3705" }; + + private static readonly string[] ArchitectureDependentAssemblies = + { + "mscorlib", + "PresentationCore", + "System.Data", + "System.Data.OracleClient", + "System.EnterpriseServices", + "System.Printing", + "System.Transactions", + "System.Web" + }; + + private readonly IEnvironment _environment; + private readonly IFileSystem _fileSystem; + private readonly IFusionWrapper _fusionWrapper; + private readonly bool _is64Bit; + + [ExcludeFromCodeCoverage] + public FusionGacResolverStrategy(IEnvironment environment, IFileSystem fileSystem, bool is64Bit) : this(environment, + fileSystem, new FusionWrapper(), is64Bit) { } + + public FusionGacResolverStrategy( + IEnvironment environment, + IFileSystem fileSystem, + IFusionWrapper fusionWrapper, + bool is64Bit) + { + _environment = environment; + _fileSystem = fileSystem; + _fusionWrapper = fusionWrapper; + _is64Bit = is64Bit; + + _fusionWrapper.LoadFusion(FindFusionPaths()); + } + + public bool Test(AssemblyIdentity identity, TargetFramework? targetFramework) => + targetFramework?.Runtime is NetRuntime.Framework && + _environment.OSVersion.Platform is PlatformID.Win32NT && + identity.IsStrongNamed; + + public AssemblyResolverResult? Resolve(AssemblyIdentity identity) + { + var architecture = "MSIL"; + if (ArchitectureDependentAssemblies.Contains(identity.Name)) + { + architecture = _is64Bit ? "AMD64" : "X86"; + } + + var assemblyName = identity.FullName + ", processorArchitecture=" + architecture; + + // .NET Core only works with absolute path, so first load fusion.dll manually + var path = _fusionWrapper.QueryAssemblyPath(assemblyName); + return path != null ? new AssemblyResolverResult(AssemblySource.Gac, path) : null; + } + + public IEnumerable FindFusionPaths() + { + var basePath = Path.Combine(_environment.GetFolderPath(Environment.SpecialFolder.Windows), "Microsoft.NET"); + var frameworkDirectory = _environment.Is64BitOperatingSystem ? "Framework64" : "Framework"; + foreach (var version in FrameworkVersions) + { + var fusionPath = Path.Combine(basePath, frameworkDirectory, version, "fusion.dll"); + if (_fileSystem.File.Exists(fusionPath)) + { + yield return fusionPath; + } + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + _fusionWrapper.Dispose(); + } +} + +internal interface IFusionWrapper : IDisposable +{ + void LoadFusion(IEnumerable fusionPaths); + string? QueryAssemblyPath(string assemblyName); +} + +[ExcludeFromCodeCoverage] +internal class FusionWrapper : IFusionWrapper +{ + private IntPtr _fusionLibrary; + + public string? QueryAssemblyPath(string assemblyName) + { + if (_fusionLibrary == IntPtr.Zero) + { + throw new FileLoadException("Call IFusionWrapper.Load(paths) before querying for assemblies."); + } + + const int bufferSize = 1024; + var assemblyInfo = new AssemblyInfo + { + cchBuf = bufferSize, + pszCurrentAssemblyPathBuf = new string('\0', bufferSize) + }; + + var hr = CreateAssemblyCache(out var assemblyCache, 0); + if (hr >= 0) + { + hr = assemblyCache.QueryAssemblyInfo(0, assemblyName, ref assemblyInfo); + } + + if (hr == -2147024894) + { + return null; + } + + if (hr < 0) + { + Marshal.ThrowExceptionForHR(hr); + } + + return assemblyInfo.pszCurrentAssemblyPathBuf; + } + + public void LoadFusion(IEnumerable fusionPaths) + { + if (_fusionLibrary != IntPtr.Zero) + { + return; + } + + foreach (var path in fusionPaths) + { + try + { + _fusionLibrary = NativeLibrary.Load(path); + break; + } + catch + { + throw new FileLoadException($"Could not load fusion.dll ({path})"); + } + } + + if (_fusionLibrary != IntPtr.Zero) + { + return; + } + + throw new FileNotFoundException("Could not find fusion.dll"); + } + + + [DllImport("fusion.dll")] + private static extern int CreateAssemblyCache(out IAssemblyCache ppAsmCache, int reserved); + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!disposing || _fusionLibrary == IntPtr.Zero) + { + return; + } + + NativeLibrary.Free(_fusionLibrary); + _fusionLibrary = IntPtr.Zero; + } + + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("e707dcde-d1cd-11d2-bab9-00c04f8eceae")] + private interface IAssemblyCache + { + void UninstallAssembly(); + + [PreserveSig] + int QueryAssemblyInfo( + int flags, + [MarshalAs(UnmanagedType.LPWStr)] string assemblyName, + ref AssemblyInfo assemblyInfo); + } + + [StructLayout(LayoutKind.Sequential)] + private struct AssemblyInfo + { + private readonly int cbAssemblyInfo; + private readonly int dwAssemblyFlags; + private readonly long uliAssemblySizeInKB; + + [MarshalAs(UnmanagedType.LPWStr)] + public string pszCurrentAssemblyPathBuf; + + public int cchBuf; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/Strategies/Framework/SilverlightResolverStrategy.cs b/src/RefScout.Analyzer/Resolvers/Strategies/Framework/SilverlightResolverStrategy.cs new file mode 100644 index 0000000..199a9d5 --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/Strategies/Framework/SilverlightResolverStrategy.cs @@ -0,0 +1,60 @@ +using System; +using System.IO; +using System.IO.Abstractions; +using RefScout.Analyzer.Helpers; + +namespace RefScout.Analyzer.Resolvers.Strategies.Framework; + +internal class SilverlightResolverStrategy : IResolverStrategy +{ + private readonly IEnvironment _environment; + private readonly IFileSystem _fileSystem; + private readonly TargetFramework _targetFramework; + + public SilverlightResolverStrategy( + IEnvironment environment, + IFileSystem fileSystem, + TargetFramework targetFramework) + { + _environment = environment; + _fileSystem = fileSystem; + _targetFramework = targetFramework; + } + + public bool Test(AssemblyIdentity identity, TargetFramework? targetFramework) => + targetFramework?.Runtime is NetRuntime.Silverlight && _environment.OSVersion.Platform is PlatformID.Win32NT; + + public AssemblyResolverResult? Resolve(AssemblyIdentity identity) + { + var path = ResolveSilverlight(identity, _targetFramework.Version); + return path != null ? new AssemblyResolverResult(AssemblySource.Gac, path) : null; + } + + private string? ResolveSilverlight(AssemblyIdentity identity, Version version) + { + string[] targetFrameworkSearchPaths = + { + _environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), + _environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86) + }; + + foreach (var baseDirectory in targetFrameworkSearchPaths) + { + var directory = Path.Combine(baseDirectory, "Microsoft Silverlight"); + if (!_fileSystem.Directory.Exists(directory)) + { + continue; + } + + var versionDirectory = Path.Combine(directory, + ResolverHelper.FindClosestVersionDirectory(_fileSystem, directory, version)); + var file = ResolverHelper.SearchInDirectory(_fileSystem, identity, versionDirectory); + if (file != null) + { + return file; + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/Strategies/Framework/WindowsMetadataResolverStrategy.cs b/src/RefScout.Analyzer/Resolvers/Strategies/Framework/WindowsMetadataResolverStrategy.cs new file mode 100644 index 0000000..c2aaceb --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/Strategies/Framework/WindowsMetadataResolverStrategy.cs @@ -0,0 +1,78 @@ +using System; +using System.IO; +using System.IO.Abstractions; +using RefScout.Analyzer.Helpers; + +namespace RefScout.Analyzer.Resolvers.Strategies.Framework; + +internal class WindowsMetadataResolverStrategy : IResolverStrategy +{ + private readonly IEnvironment _environment; + private readonly IFileSystem _fileSystem; + + public WindowsMetadataResolverStrategy(IEnvironment environment, IFileSystem fileSystem) + { + _environment = environment; + _fileSystem = fileSystem; + } + + public bool Test(AssemblyIdentity identity, TargetFramework? targetFramework) => + identity.IsWindowsRuntime && _environment.OSVersion.Platform is PlatformID.Win32NT; + + public AssemblyResolverResult? Resolve(AssemblyIdentity identity) + { + var path = FindWindowsMetadataFile(identity); + return path != null ? new AssemblyResolverResult(AssemblySource.Gac, path) : null; + } + + private string? FindWindowsMetadataFile(AssemblyIdentity identity) + { + // TODO: this could technically be read from the registry: "HKLM\SOFTWARE\Wow6432Node\Microsoft\Windows Kits\Installed Roots" + var basePath = Path.Combine(_environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), + "Windows Kits", "10", "References"); + + if (!_fileSystem.Directory.Exists(basePath)) + { + return FindWindowsMetadataInSystemDirectory(identity); + } + + // TODO : Find a way to detect the required Windows SDK version. + var di = _fileSystem.DirectoryInfo.FromDirectoryName(basePath); + basePath = null; + foreach (var versionFolder in di.GetDirectories()) + { + basePath = versionFolder.FullName; + } + + if (basePath == null) + { + return FindWindowsMetadataInSystemDirectory(identity); + } + + basePath = Path.Combine(basePath, identity.Name); + if (!_fileSystem.Directory.Exists(basePath)) + { + return FindWindowsMetadataInSystemDirectory(identity); + } + + basePath = Path.Combine(basePath, + ResolverHelper.FindClosestVersionDirectory(_fileSystem, basePath, identity.Version)); + if (!_fileSystem.Directory.Exists(basePath)) + { + return FindWindowsMetadataInSystemDirectory(identity); + } + + var file = Path.Combine(basePath, identity.Name + ".winmd"); + return _fileSystem.File.Exists(file) + ? file + : FindWindowsMetadataInSystemDirectory(identity); + } + + private string? FindWindowsMetadataInSystemDirectory(AssemblyIdentity identity) + { + var file = Path.Combine(Environment.SystemDirectory, "WinMetadata", identity.Name + ".winmd"); + return _fileSystem.File.Exists(file) + ? file + : null; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/Strategies/IResolverStrategy.cs b/src/RefScout.Analyzer/Resolvers/Strategies/IResolverStrategy.cs new file mode 100644 index 0000000..fd7f6e5 --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/Strategies/IResolverStrategy.cs @@ -0,0 +1,12 @@ +using System; + +namespace RefScout.Analyzer.Resolvers.Strategies; + +public interface IResolverStrategy : IDisposable +{ + bool Test(AssemblyIdentity identity, TargetFramework? targetFramework); + + AssemblyResolverResult? Resolve(AssemblyIdentity identity); + + void IDisposable.Dispose() { } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/Strategies/Mono/MonoCorLibResolverStrategy.cs b/src/RefScout.Analyzer/Resolvers/Strategies/Mono/MonoCorLibResolverStrategy.cs new file mode 100644 index 0000000..e5dc7ab --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/Strategies/Mono/MonoCorLibResolverStrategy.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using RefScout.Analyzer.Analyzers.Environment.Mono; + +namespace RefScout.Analyzer.Resolvers.Strategies.Mono; + +internal class MonoCorLibResolverStrategy : IResolverStrategy +{ + private const string CorLibName = "mscorlib"; + private readonly IFileSystem _fileSystem; + private readonly IMonoRuntimeAnalyzer _monoRuntimeAnalyzer; + + public MonoCorLibResolverStrategy(IFileSystem fileSystem, IMonoRuntimeAnalyzer monoRuntimeAnalyzer) + { + _fileSystem = fileSystem; + _monoRuntimeAnalyzer = monoRuntimeAnalyzer; + } + + public bool Test(AssemblyIdentity identity, TargetFramework? targetFramework) => + identity.Name == CorLibName && targetFramework?.Runtime is NetRuntime.Framework && + identity.IsStrongNamed; + + public AssemblyResolverResult? Resolve(AssemblyIdentity identity) + { + var possibleVersions = GetPossibleMonoVersions(identity.Version).ToList(); + foreach (var baseDirectory in _monoRuntimeAnalyzer.GetRuntimePrefixDirectories()) + { + foreach (var version in possibleVersions) + { + var fileName = Path.Combine(baseDirectory, "lib", "mono", version, "mscorlib.dll"); + if (_fileSystem.File.Exists(fileName)) + { + return new AssemblyResolverResult(AssemblySource.Gac, fileName); + } + } + } + + return null; + } + + private static IEnumerable GetPossibleMonoVersions(Version version) + { + switch (version.Major) + { + case 1: + yield return "1.0"; + break; + case 2 when version.MajorRevision == 5: + yield return "2.1"; + break; + case 2: + yield return "2.0"; + break; + case 4: + yield return "4.5"; + yield return "4.0"; + break; + } + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/Strategies/Mono/MonoGacResolverStrategy.cs b/src/RefScout.Analyzer/Resolvers/Strategies/Mono/MonoGacResolverStrategy.cs new file mode 100644 index 0000000..e1b0aaf --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/Strategies/Mono/MonoGacResolverStrategy.cs @@ -0,0 +1,41 @@ +using System.IO; +using System.IO.Abstractions; +using RefScout.Analyzer.Analyzers.Environment.Mono; + +namespace RefScout.Analyzer.Resolvers.Strategies.Mono; + +internal class MonoGacResolverStrategy : IResolverStrategy +{ + private readonly IFileSystem _fileSystem; + private readonly IMonoRuntimeAnalyzer _monoRuntimeAnalyzer; + + public MonoGacResolverStrategy(IFileSystem fileSystem, IMonoRuntimeAnalyzer monoRuntimeAnalyzer) + { + _fileSystem = fileSystem; + _monoRuntimeAnalyzer = monoRuntimeAnalyzer; + } + + public bool Test(AssemblyIdentity identity, TargetFramework? targetFramework) => + targetFramework?.Runtime is NetRuntime.Framework && + identity.IsStrongNamed; + + public AssemblyResolverResult? Resolve(AssemblyIdentity identity) + { + foreach (var baseDirectory in _monoRuntimeAnalyzer.GetGacPrefixDirectories()) + { + var gacDirectory = Path.Combine(baseDirectory, "lib", "mono", "gac"); + var fileName = GetAssemblyFile(identity, gacDirectory); + + if (_fileSystem.File.Exists(fileName)) + { + return new AssemblyResolverResult(AssemblySource.Gac, fileName); + } + } + + return null; + } + + private static string GetAssemblyFile(AssemblyIdentity identity, string gacDirectory) => + Path.Combine(gacDirectory, identity.Name, $"{identity.Version}__{identity.PublicKeyToken}", + identity.Name + ".dll"); +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/Strategies/Mono/MonoRuntimeResolverStrategy.cs b/src/RefScout.Analyzer/Resolvers/Strategies/Mono/MonoRuntimeResolverStrategy.cs new file mode 100644 index 0000000..c5ac20d --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/Strategies/Mono/MonoRuntimeResolverStrategy.cs @@ -0,0 +1,44 @@ +using System.IO; +using System.IO.Abstractions; +using RefScout.Analyzer.Analyzers.Environment.Mono; + +namespace RefScout.Analyzer.Resolvers.Strategies.Mono; + +internal class MonoRuntimeResolverStrategy : IResolverStrategy +{ + private static readonly string[] SubDirectories = { "", "Facades" }; + + private readonly IFileSystem _fileSystem; + private readonly IMonoRuntimeAnalyzer _monoRuntimeAnalyzer; + + public MonoRuntimeResolverStrategy(IFileSystem fileSystem, IMonoRuntimeAnalyzer monoRuntimeAnalyzer) + { + _fileSystem = fileSystem; + _monoRuntimeAnalyzer = monoRuntimeAnalyzer; + } + + public bool Test(AssemblyIdentity identity, TargetFramework? targetFramework) => + targetFramework?.Runtime is NetRuntime.Framework && + identity.IsStrongNamed; + + public AssemblyResolverResult? Resolve(AssemblyIdentity identity) + { + foreach (var baseDirectory in _monoRuntimeAnalyzer.GetRuntimePrefixDirectories()) + { + foreach (var version in MonoRuntimeAnalyzer.PossibleVersionNames) + { + foreach (var subDirectory in SubDirectories) + { + var fileName = Path.Combine(baseDirectory, "lib", "mono", version, subDirectory, + identity.Name + ".dll"); + if (_fileSystem.File.Exists(fileName)) + { + return new AssemblyResolverResult(AssemblySource.Gac, fileName); + } + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/Strategies/ResolverHelper.cs b/src/RefScout.Analyzer/Resolvers/Strategies/ResolverHelper.cs new file mode 100644 index 0000000..f70e625 --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/Strategies/ResolverHelper.cs @@ -0,0 +1,76 @@ +using System; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using RefScout.Core.Logging; + +namespace RefScout.Analyzer.Resolvers.Strategies; + +public static class ResolverHelper +{ + private static (Version? version, string? directoryName) ConvertNameToVersion(string name) + { + try + { + var shortName = name; + var dashIndex = shortName.IndexOf('-'); + if (dashIndex > 0) + { + shortName = shortName.Remove(dashIndex); + } + + if (!Version.TryParse(shortName, out var parsedVersion)) + { + Logger.Warn($"Could not convert directory version name to version: {name}"); + } + + return (parsedVersion, name); + } + catch + { + Logger.Warn($"Could not convert directory version name to version: {name}"); + return (null, null); + } + } + + public static string FindClosestVersionDirectory(IFileSystem fileSystem, string? basePath, Version? version) + { + if (basePath == null) + { + return "."; + } + + string? path = null; + foreach (var (directoryVersion, directoryName) in fileSystem.DirectoryInfo.FromDirectoryName(basePath) + .GetDirectories() + .Select(d => ConvertNameToVersion(d.Name)) + .Where(v => v.version != null) + .OrderByDescending(v => v.version)) + { + if (path == null || version == null || directoryVersion >= version) + { + path = directoryName; + } + } + + return path ?? version?.ToString() ?? "."; + } + + public static string? SearchInDirectory(IFileSystem fileSystem, AssemblyIdentity identity, string directory) + { + var extensions = identity.IsWindowsRuntime ? new[] { ".winmd", ".dll" } : new[] { ".dll", ".exe" }; + + // As per documentation, only look in culture specific directories if culture is specified + // https://docs.microsoft.com/en-us/dotnet/framework/deployment/how-the-runtime-locates-assemblies#probing-the-application-base-and-culture-directories + if (identity.Culture != AssemblyIdentity.CultureNeutral) + { + return extensions + .Select(extension => Path.Combine(directory, identity.Culture, identity.Name + extension)) + .FirstOrDefault(fileSystem.File.Exists); + } + + return extensions + .Select(extension => Path.Combine(directory, identity.Name + extension)) + .FirstOrDefault(fileSystem.File.Exists); + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/Resolvers/Strategies/Shared/DirectoryResolverStrategy.cs b/src/RefScout.Analyzer/Resolvers/Strategies/Shared/DirectoryResolverStrategy.cs new file mode 100644 index 0000000..666b3c7 --- /dev/null +++ b/src/RefScout.Analyzer/Resolvers/Strategies/Shared/DirectoryResolverStrategy.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Linq; + +namespace RefScout.Analyzer.Resolvers.Strategies.Shared; + +public class DirectoryResolverStrategy : IResolverStrategy +{ + private readonly IFileSystem _fileSystem; + private readonly IReadOnlyList _directories; + + public DirectoryResolverStrategy(IFileSystem fileSystem, IEnumerable directories) + { + _fileSystem = fileSystem; + _directories = directories.ToList(); + } + + public bool Test(AssemblyIdentity identity, TargetFramework? targetFramework) => true; + + public AssemblyResolverResult? Resolve(AssemblyIdentity identity) + { + var path = _directories + .Select(directory => ResolverHelper.SearchInDirectory(_fileSystem, identity, directory)) + .FirstOrDefault(file => file != null); + + return path != null ? new AssemblyResolverResult(AssemblySource.Local, path) : null; + } +} \ No newline at end of file diff --git a/src/RefScout.Analyzer/TargetFramework.cs b/src/RefScout.Analyzer/TargetFramework.cs new file mode 100644 index 0000000..e2851d1 --- /dev/null +++ b/src/RefScout.Analyzer/TargetFramework.cs @@ -0,0 +1,108 @@ +using System; +using RefScout.Analyzer.Helpers; + +namespace RefScout.Analyzer; + +public class TargetFramework +{ + private const string NetFramework = ".NETFramework"; + private const string NetPortable = ".NETPortable"; + private const string NetCoreApp = ".NETCoreApp"; + private const string NetCore = ".NETCore"; + private const string Netstandard = ".NETStandard"; + private const string Silverlight = "Silverlight"; + + public TargetFramework(NetRuntime runtime, Version version) + { + // Trim last zero if predecessor is also a zero + // 5.0.0 -> 5.0 + if (version.Minor == 0 && version.Build == 0) + { + version = version.ToMajorMinor(); + } + + Runtime = runtime; + Version = version; + } + + public NetRuntime Runtime { get; } + public Version Version { get; } + + // .NET 5.0 is still called .NETCoreApp too + public string Id => Runtime == NetRuntime.Core + ? $"{NetCoreApp},Version=v{Version}" + : $".NET{Runtime},Version=v{Version}"; + + public string ShortName => Runtime is NetRuntime.Core or NetRuntime.Standard && Version.Major >= 5 + ? Version.ToString() + : $"{Runtime} {Version}"; + + public override string ToString() => ".NET " + ShortName; + + public static TargetFramework Parse(ReadOnlySpan identifier) + { + const string versionProperty = ",Version="; + var separator = identifier.IndexOf(','); + var runtime = identifier[..separator]; + var properties = identifier[(separator + 1)..]; + var version = properties[(properties.IndexOf(versionProperty) + versionProperty.Length)..]; + + var nextPropertyStart = version.IndexOf(','); + if (nextPropertyStart != -1) + { + version = version[..nextPropertyStart]; + } + + if (version[0] == 'v') + { + version = version[1..]; + } + + var parsedVersion = Version.Parse(version); + NetRuntime netRuntime; + if (runtime.Equals(NetFramework, StringComparison.OrdinalIgnoreCase) || + runtime.Equals(NetPortable, StringComparison.OrdinalIgnoreCase)) + { + netRuntime = parsedVersion.Major >= 5 ? NetRuntime.Silverlight : NetRuntime.Framework; + } + else if (runtime.Equals(NetCoreApp, StringComparison.OrdinalIgnoreCase) || + runtime.Equals(NetCore, StringComparison.OrdinalIgnoreCase)) + { + netRuntime = NetRuntime.Core; + } + else if (runtime.Equals(Netstandard, StringComparison.OrdinalIgnoreCase)) + { + netRuntime = NetRuntime.Standard; + } + else if (runtime.Equals(Silverlight, StringComparison.OrdinalIgnoreCase)) + { + netRuntime = NetRuntime.Silverlight; + } + else + { + throw new Exception($"Unsupported runtime in target framework: {runtime.ToString()}"); + } + + return new TargetFramework(netRuntime, parsedVersion); + } + + public override bool Equals(object? obj) + { + if (obj is TargetFramework other) + { + return Runtime == other.Runtime && Version.Equals(other.Version); + } + + return false; + } + + public override int GetHashCode() => HashCode.Combine((int)Runtime, Version); +} + +public enum NetRuntime +{ + Standard, + Core, + Framework, + Silverlight +} \ No newline at end of file diff --git a/src/RefScout.Cli/Commands/ConsoleConflictVisualizerCommand.cs b/src/RefScout.Cli/Commands/ConsoleConflictVisualizerCommand.cs new file mode 100644 index 0000000..a97c015 --- /dev/null +++ b/src/RefScout.Cli/Commands/ConsoleConflictVisualizerCommand.cs @@ -0,0 +1,23 @@ +using McMaster.Extensions.CommandLineUtils; +using RefScout.Visualizers.Console; + +namespace RefScout.CLI.Commands; + +internal class ConsoleConflictVisualizerCommand : VisualizerCommand +{ + private CommandOption? _showFramework; + + public override bool IsConfigured => true; + + protected override ConsoleVisualizerOptions Options => new() + { + Detailed = _showFramework?.HasValue() ?? false + }; + + public override void Register(CommandLineApplication application) + { + _showFramework = application.Option("-cd|--console-detail", + "Show a more detailed information in the console such as target framework and source language.", + CommandOptionType.NoValue); + } +} \ No newline at end of file diff --git a/src/RefScout.Cli/Commands/DotConflictVisualizerCommand.cs b/src/RefScout.Cli/Commands/DotConflictVisualizerCommand.cs new file mode 100644 index 0000000..7012f28 --- /dev/null +++ b/src/RefScout.Cli/Commands/DotConflictVisualizerCommand.cs @@ -0,0 +1,26 @@ +using McMaster.Extensions.CommandLineUtils; +using RefScout.Visualizers.Dot; + +namespace RefScout.CLI.Commands; + +internal class DotConflictVisualizerCommand : VisualizerCommand +{ + private CommandOption? _outputFile; + private CommandOption? _showFramework; + + protected override DotConflictVisualizerOptions Options => new(_outputFile?.Value() ?? string.Empty) + { + ShowTargetFramework = _showFramework?.HasValue() ?? false + }; + + public override bool IsConfigured => _outputFile?.HasValue() ?? false; + + public override void Register(CommandLineApplication application) + { + _outputFile = + application.Option("--dot ", "Export to a GraphViz file.", CommandOptionType.SingleValue); + _showFramework = application.Option("-dotf|--dot-framework", + "Include .NET versions in the GraphViz diagram.", + CommandOptionType.NoValue); + } +} \ No newline at end of file diff --git a/src/RefScout.Cli/Commands/IVisualizerCommand.cs b/src/RefScout.Cli/Commands/IVisualizerCommand.cs new file mode 100644 index 0000000..dfcc1ad --- /dev/null +++ b/src/RefScout.Cli/Commands/IVisualizerCommand.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using McMaster.Extensions.CommandLineUtils; +using RefScout.Analyzer; +using RefScout.Visualizers; + +namespace RefScout.CLI.Commands; + +internal interface IVisualizerCommand +{ + bool IsConfigured { get; } + + void Register(CommandLineApplication application); + + Task RunVisualizerAsync(IAnalyzerResult result, VisualizeMode visualizeMode); +} \ No newline at end of file diff --git a/src/RefScout.Cli/Commands/VisualizerCommand.cs b/src/RefScout.Cli/Commands/VisualizerCommand.cs new file mode 100644 index 0000000..e3fa380 --- /dev/null +++ b/src/RefScout.Cli/Commands/VisualizerCommand.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; +using McMaster.Extensions.CommandLineUtils; +using RefScout.Analyzer; +using RefScout.Core.Logging; +using RefScout.Visualizers; + +namespace RefScout.CLI.Commands; + +internal abstract class VisualizerCommand : IVisualizerCommand + where TVisualizer : Visualizer + where TOptions : IVisualizerOptions +{ + protected abstract TOptions Options { get; } + + public abstract bool IsConfigured { get; } + + public abstract void Register(CommandLineApplication application); + + public async Task RunVisualizerAsync(IAnalyzerResult result, VisualizeMode mode) + { + var visualizer = (TVisualizer?)Activator.CreateInstance(typeof(TVisualizer)); + if (visualizer == null) + { + Logger.Error($"Could not instantiate visualizer {typeof(TVisualizer).Name}"); + return; + } + + await Task.Run(() => visualizer.Visualize(result, mode, Options)); + } +} \ No newline at end of file diff --git a/src/RefScout.Cli/Program.cs b/src/RefScout.Cli/Program.cs new file mode 100644 index 0000000..6f80bda --- /dev/null +++ b/src/RefScout.Cli/Program.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using McMaster.Extensions.CommandLineUtils; +using McMaster.Extensions.CommandLineUtils.HelpText; +using RefScout.Analyzer; +using RefScout.Analyzer.Analyzers.Compatibility; +using RefScout.CLI.Commands; +using RefScout.Core.Logging; +using RefScout.Visualizers; + +namespace RefScout.CLI; + +public static class Program +{ + private static readonly List VisualizerCommands = new() + { + new ConsoleConflictVisualizerCommand(), + new DotConflictVisualizerCommand() + }; + + private static CommandArgument? _file; + private static CommandOption? _config; + private static CommandOption? _filter; + private static CommandOption? _analyzeMode; + private static CommandOption? _analyzeRuntime; + private static CommandOption? _visualizeMode; + private static CommandOption? _systemVersionsMode; + + public static int Main(string[] args) + { + Logger.AddLogger(new ConsoleLogger()); + Logger.Level = LogLevel.Info; + + var app = new CommandLineApplication + { + HelpTextGenerator = new DefaultHelpTextGenerator { SortCommandsByName = false } + }; + app.HelpOption(); + + _file = app.Argument("file", "Required. Assembly file to be analyzed (.exe, .dll)").IsRequired(); + _config = app.Option("-c|--config ", + "The config file containing binding redirects, default .dll|exe.config (.NET Framework only).", + CommandOptionType.SingleValue); + + _filter = app.Option("-f|--filter ", "Specify a query string to filter the results of the analyzer.", + CommandOptionType.SingleValue); + + _analyzeMode = + app.Option("-a|--analyze ", + "Specify to which degree assembly references should be analyzed.", CommandOptionType.SingleValue); + _analyzeRuntime = + app.Option("-r|--runtime ", + "Specify which runtime type should be used for analyzing. Useful for analyzing Mono applications on Windows.", + CommandOptionType.SingleValue); + + _systemVersionsMode = app.Option("-sv|--system-versions ", + "Specify how strict version mismatches of system assemblies should be reported.", + CommandOptionType.SingleValue); + + _visualizeMode = + app.Option("-v|--visualize ", + "Specify which type of assemblies should be visualized by the visualizer.", + CommandOptionType.SingleValue); + + VisualizerCommands.ForEach(a => a.Register(app)); + + app.OnExecuteAsync(async _ => + { + var result = await RunAnalyzerAsync(); + if (result == null) + { + await app.Error.WriteLineAsync("Analyzer did not run successfully."); + return; + } + + var visualizeMode = _visualizeMode.HasValue() ? _visualizeMode.ParsedValue : VisualizeMode.Default; + var tasks = VisualizerCommands + .Where(v => v.IsConfigured) + .ToList() + .Select(v => v.RunVisualizerAsync(result, visualizeMode)); + await Task.WhenAll(tasks); + }); + + try + { + if (args.Length != 0) + { + return app.Execute(args); + } + + app.ShowHelp(); + return 0; + } + catch (CommandParsingException cpe) + { + app.Error.WriteLine(cpe.Message); + app.ShowHelp(); + return -1; + } + catch (Exception e) + { + app.Error.WriteLine(e.Message); + return -1; + } + } + + private static async Task RunAnalyzerAsync() + { + if (_file?.Value == null) + { + return null; + } + + try + { + return await Task.Run(() => ReferenceAnalyzer.Run(_file.Value, new AnalyzerOptions + { + Config = _config?.Value(), + Filter = _filter?.Value(), + SystemVersionMode = _systemVersionsMode?.ParsedValue ?? VersionCompatibilityMode.Off, + AnalyzeMode = _analyzeMode?.ParsedValue ?? AnalyzeMode.AppDirectSystem, + AnalyzeRuntime = _analyzeRuntime?.ParsedValue ?? AnalyzeRuntime.Default + })); + } + catch (Exception e) + { + Logger.Error(e, "Analyzer failed to run"); + Console.WriteLine(e); + return null; + } + } +} \ No newline at end of file diff --git a/src/RefScout.Cli/RefScout.Cli.csproj b/src/RefScout.Cli/RefScout.Cli.csproj new file mode 100644 index 0000000..7191e80 --- /dev/null +++ b/src/RefScout.Cli/RefScout.Cli.csproj @@ -0,0 +1,39 @@ + + + + Exe + net6.0 + win-x64;linux-x64 + enable + + true + ref-analyzer + ..\..\_nupkg + + Resources\Icon.ico + + Link + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/RefScout.Cli/Resources/Icon.ico b/src/RefScout.Cli/Resources/Icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3d70c4e5d31a5fd7d7560983f59f02b52fe479eb GIT binary patch literal 381534 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 z z=>JFmFZzG@*stTpf5QHUwVr9je;@My>52b#<^QMp4}32``aiVwKRfZ@JZuA^}pAs#Ll0cVoWWX8aEDde`i^zm@L-Gaf+OSb1swMh%(Vx&Q6H4 zK>l|N2IYw483nkWGX2NHb8L_N*x>}5^c$y^Esc?k9mCiT8oNgk!xDo3< zLhHY_Qx@RH}=15|3UvB&8YrEZ~Wg%{Fih9@jt-!KllRnMNdH;pvWFLcO#c~8Z&HS zf^!GSgU>F6^goWaJpT(O|6vS><3QGsFUwvy?lp3$-(zHo?2cviAa%P`vNMj=fm}w} zAhUfg1ib$9V}iObD%HRK`ezoybx{9n0v7+fFFG5ap}$1!|0dPHcNk(EIIIEr0knYw zu>PZtuK{V_`j0w@{W<{hI^Ns6(zO5ERR8VAFr4o|^&if)*Z#6o{kI>(aJ~c8e>m4( z`^!%C-+m0k`3_Y7;aq#|FFVwKQy8!RX=*wj0Jeo>y) zJE%E%BeCAq?}YOX)!WQ3)oe|e1?#^pT(%@wpbc=j1V=8L5-pd6VAsf8(;YEaP4WxeGpP$sMc9GPI>Q?g)*WT@dDcyqu5j$S zsftS%IHEk1Bgum~mF&TdmF&AJd0;~YL%APieFeKumV4KivwPP`at|EuSzFHT=2FH$ z53su|l6(h_D0i+Y<#Goc?^s>RzRjfsjwS5&Y)Ni|<87--*tb>{bGa3cw`NHKx`5r1 zX=OLdauXbHT4`lBaw&pi5xZf9B-g|7`sL@?b+TLw$7`2Ka?P@{?3$%#nL)_1v%3sS ziw1OPW1xq3Dg+XD*1XkoXU$a*208$wnyK}GD;;3d16n%3Sr6Fh07pIGLI*sAI>12> zfK6c20d{)8K?i^y5IW#)PddOs57_Ad&;#F6>jAY6$a2;Jk{-~|0gie=(E;Ze>H$Ru zEIr$1SbAo3r)q@svK@jzuU(ganYgQlq4Y!n{wsPwZ3n=)ogR?w04*J0lD_DdPbg7if+LmYuz9SaOc`|LYWN4E{?d?YTU&*X~+| z0(t;+KrOceoOJ+<3DmZLgHOQZ#ugAh0Zm(g`vfjXK7oLY2P*7+0xBKgZam=57T|V( zqhG+qcmRC^I2LH%c;L+R;I|BVVx%LGeuCYQyr;Gjg!5lZ2RP{gwGQxRJOKVdp$8m& zgZ>&1ppVd%EuhCYsB1i+!#C)DJb?Z|n_p0KJm72#R2W7xO7s!wgbecgrRUe0Q}$eD z$y^+C0JjAspMaAN&^aF9egT*B1D<_@VmyFHPxAxz;{la#P&Gf$-tmBXjzIVY9DM>f zPhdAcpfMhBX$vfOJsvPDvKoWjJm_%|jzG%px+hciUS?_?kSvb{v}^(47j)+v6mx_D z84qmMo+I?yHwa?@oF}yR4VJ+?fo+~p*aBjX(68eGWuDO9H^}{hF6IXeOU@3C@a9Ob z7i0ud_tXzfMgGe=K+*%XU=KLh0xF+?lP%zGJOJ?mXam^h2f`~>Kw=#0t3_5Aaw4 zDPG85;{k{T;ORdjyh+mQ1rdQk`RwMjeRVZJGJHXv| zAONufmkcxids7gb20f~iAdsG4wUI?2Xw{?=vhmsUQcMZen1sF94fH_(po}XQ|QH7Lg!e4 zwi~jFQ#znY&?7q;0=@UwuLB*x(ouvRfJgQGfH%HDdHp~*#R~BC#CG!o&auPNn&RNZ z3cz~ewVkXP((85*0uS%K+_U$-`m3k|xE=uRD|#S=#{-yOz|I!XSWAepgM2-qo3(`g ziyd|yD{#9GTR;^%eD%gv)?OXd9O$u~5P^sH)nz}tub%VYNeAd04@6q5fXFT4G(Vto z9g&C^@;+9eB&!oPL3-s5M4-?9#@L7V*SAPIK+*$ZJYcg0RPjRIj0gM_DjuF4c4!7REs+8h=I*<#U_0f+UXjlO{fT?wWJ6phhegM}JImZr1 zTC9McwS<^!OvDe1m?2>c=*Tm+th2U1dhd=!Abn3`%p+g}fH0dL(C`U(@C^ntRv=m< z-=LjOARw^A&D7#_F^$lk77}9tq1r6-Q^q&xjbX5OBQzKwny*Y@d)%ge5HGz z1NBXIIzSl{XxjpQi5>POR$yxtGrsaGW;7Z#CFYgiupXN#Z91S`u>#t8hu!%G_2(Jm z`G=eDu#`u2d<&owban(DJ#gjIJ_j1O4Pd7S+{_QSixto_9?*;x@RVoltxq(0uhs|A zZ%vWs2tz&3Sjn8u-tOrALj!^Shw2{${x_fw09!!O12&(4i+RFe#tLjM=eE9@>ze0@ z$t-GP8Pk(zEV!`((*6VTTEZ)LWfpe{joPPD5CR6nqlX%1KFay8p#%Ii9%xUVvDte) z_WeR1M9(=)s11n+|X_9&qIw z^f6XIXY6p?C!2KhKjz~cyZwly`2iQ>0f#(e{*4vTj2&*cC%fpukg9wt3?Tpr^gGxv z1#|#I9bnS~UW^A+af9};0)EIdw)nVy{x3WhvU`$2oWLY%Y=l54A^@pK^q2 zvEwYh?@_Gs4eHsW*xnY<6)RBvZHx7;P_BC_8zF!Q^gncE4CnyXPu2liwt$Eeu#X$m zju)~U4>+tN(i1D-n0MH`?ld#~c^-><{Vf*z`tMjQp07LOkbm5pJYz0=gMQhgSjsDK z&-#iFsC5wn;eY__6?RAeLyZ@?4gfvipYecm?6BYWAaEBu9E3b$Zt@CTx^v^DJHw&w zsf>gGA^;xAi}K zrBbZ}ydMwP#|r4lJM4B30-dn}d>;Zgwt(Bb0`B%Gc8C>t$W4x+2R11ClUCicsp|G{ zs(UIcA%F<Y!2f{YbSWacx zgIh{(4ad5tG7|!bz`(DRJbabqMS>BIG&z@v%#{=!iGY0+tThxjO zfpA6u_YE9a&{!<#09g+>jR$nb3b-2&xQrbR+dN~XT`bmH!ny9L?1TU!0Nj6bKtUrL z!0iBgTR_YYc<&nwPON~teF<%Ql{?1@v^US#gE{AaMXiSrhy(Y1~heQ=SjC+#78seW*yCDShBET$$>x!nv4Jevy{!7u6xLMXICW|7I z<1AJeat^L5G6~MrG7&!Okh2rwERq=E2r^zre+MdLR(v0Udji_1W-s z)6n0%S^31UAyq?$JyAaR*S|lPy7AQIq+PWPbB(I=jcVo@^OtW>J67PE^!e-ty>(CD z>%0h9r*^r;I?ePa>r_)k(Nq&FnxY|-<5-cSd;-@%&Z%V*d{!iuo%>irCc-rbIXfYi zNn(T}$aoDI2iHKBFL|3iex!->-&qGVGKd%Q7%L#=2;I#OIPF28iWPY2-S=6qgjlEb zKgn=ysZfa~a*!)$%f2q$GeSetn|MWN$du`9xY!GlCb%4`&z@u-_KDUV8*x{iI zHhSc~&;?J<%hA*U?#2V|Yyl~D*p)3H%@4ph;7h}3cCFraK;P?}2*6nXFz}y+EC103 zShMI&Ht1-RTOHsoRzTzyaUKu2&NKGXnDM&#Kk|cdoc|)XKrqGwQMngh?wl=}-dZmL zVDFDW?uR@7&y0^_Kic&-CmrBq3uw#__!=va@z>G1`Tyd(AMm|N-L56nu}85k-=GvH z@OSF_^|GMze$P79yd@I&k2b*i1^>&whc|9ilH zKjazH(g7x?yaE?a7#O=oPaB}`bY29k(@njA|J?Qucl#gv|FdIbSkC;PvB8DNe{CJ$ zdOYBLtbn)t$92U*2Lv+DSX}PKp`Eu?)0^u>0Q`Q>L>m852mE~J31=PPjc?Fhtbi(Z zc$7Ex|9hjEp1cCWFPJ393_8sZco+|u@~R8;)<1o(b0bhZ-INiD{NMWNk2(L*4j9bs z0Bu`fAdCFWTW{SS5f|6T(A^IT(K0|-4JYymy%h%iP#h^x3kj2Vo}s~*z1TQ$AC zUIZeQ|7XAk_~rHjHU#-Ei|+A&7qJ4~@c(7te>&t75cvijb$~y{1M%By4(hFY`W_(= z$^73r`xQ3iShJ=MQ053Ue1l>wpgucZUHU%(H0l9_j2dYoD{ zA)pt5NajD<0KeIB$Vmsd^9|}552#}WyyZV%LnN;yRP=za;{oSbfn9p*oxVp1AOexj z|6Q{)*b~QEKqNcB-FU#4*kSMa?;`K8Kk|%iuN^?Gnh@{?fk@{++5m6m?UQvti<2!N z`2<|}2GuzQ9M=-c>xjJNKi{X=&Ax=b?n#!ovu2w&^-kX<1Q3DF!2f+SMzE)jH#_M7 zcea37L*zPEK#CXgmjCwq656jNw2cS6&oh>=r{)oA(S(4v2y_PipZO?;{bAcq_T+IK z2iOGp?@|YNh!qfd#$NTp|2GQ!e`0@9ZU=bCDz%$&2p|HTh5!3! zJjb2}8vu2{lWHB{FhAfvRzS)zrj8xxPvZe8R-iRK zzt-$)y$9=eXj7N3N*NE_Zv$ar!hcB*EKEF9wj}l5U}4n}&*^OZM;qXSP3t-TxekzR z0igptj|c2y1-#`y<{j0OXUyI=sJSQE(vD~^-afN3GvytL_sIRQ<3ElG4*z$i@g{HI zJ6yis8To%~^7q)VBla#?|bZ&sy0t(Epm1-#)u&J`m6 z6&;|m1%zMF<#<39E0DhL@&aG$J6OL%yT2kk`G`IDz3Bh#d;mco{|oyc`+t;X>%92b zU||=`=X6&7pP2kOd+zuZXC2_>c;K~3({+#kM@^au`xE+>XG}F7XiD94rF$^jh%e77 z&rTguu}awd>Z4BoUuf|kb--L-a0vC^bXNYO4KQKjN;dSft4=z=oh{%PD=>WX-&m}? z$A`%CuXT)zHL_MrY+Q-w_s}rZO=QsV`?H&Z)lH$nyU&TrUt#b)cuL#Qzxf<-~OM;;~vq2dMRcFXI7`Z`6-@#xD0eeEB{-^%*MP zA^wL-O}H-y@jr&0857NBSmv-{;0HiK{yXUaN56oBEg;qsdJ{YBxRwy@0g+dL&l8GR zfrD4Zy0691`z!H3^y);bM8toI|Nk1F!ZMCkvtcLOGL=pAjsIu^%*&p_o`?P)`LAUQ3~gtufZ`i;yC<2HXKX-0(>tNs zZakHX`0oj!(D*y?-=6>f9+${oE39JAf8neHT>1u`#sl{Aguaaj8msyrVz-AzEAhS* z5y}6Bi3Jb`XmOJHaLrNX!8J`ufa|i%g`+IRaO5%vj&>3cpB-cte0G$X@fNEmnGWA^ zlxgOINZkJu^ZqLWYyNTFzalW_kMsUEiDuaM3t5~sf%9Lj1KK+t(25lhwt(;ps`HEu zJlgn2?^zc*zlcQsV+?TV0=4vn&plbmd<~fg*VOVUeCCn>M_J~=Q5N7n<1z=1YMC9+ ziXCMZTyvC}@eKGciy4j}GaO_(e0G#+aE*%zjwV)wGSxw*z-LF99LI_rPTj4&QtGSQocHGC$HtwSi_|HcP z?1x9o*>JD{PzR{>fH%HDukwt+`eLC6z%Nkt{0Vkz=&;%Qk`Vui|6C6!+($j2!F|*R z4&2Aj9m#*x0n0MS0RNd=9pG|2pfh&ZWvsxF#y7oZTxk7*_)q)~Xa4_ZY&`qX(K0st zZ>R&9Vh6bO4Z>PtTus|Ir3mx#E2`0{JiNfFO+r z{?~i9h2AfS|HS`r=l?e!#<8CsDdIYS>j6F&bm95%Hh+9|`>bW^^q3`Qg)=I>6m{ zz@=}{MXbP6$6LQiKf!MBjcuX#E8;)#KN9$lHo%%Cf3)L2+5uX&KzqgmPakjnVd$~g z_Yx8RiT{zpf3yMKIP{G!9pG*};CdZV%fBJc|9aoq7J9!V{uBQrh5u*+tY7q&CjS*% zpdGOSPkq+4 z9&k4vI4R{7cw_J1*?-@Qvb3LVp?3%3Kk+}(_<#C?XtsIY8@l+fwgp_p3T&G7KkU@I zJ&6CA$*dgr`*M^OaE(ic@Bb(E0i^YRVjZC4+CTgpA?yFdI$*Twi7*f7{%j=;puu{D3WXcf^)|A(jkANjvy_V>N!zpw>%%y^CXFL*DGj${AO+y9&P{}E$< zJa+Q_|MUk@?EiKbc&!6|zvmb`^&YqTg$^M9AL;<&e}~us)c^a||0DnRPkWxdaI!_W z4#4jom^_U5FZz7-k@!#iSLFfg==@(e2C(z}<6MC3|Cf$Y@HvXj&7Q8C|DUd!Dsi9s zf63=h{O=IXs~<}JzwiA&^8Xa*gim4LAThRgb)39zDf{NH(Zv6(WLBZ(y*Q`-Uro!P zofH4t$N$ssgB<{}b?#5uTlt?m>47)*eg=PoI3Miu)gsOPMY8|DiN^qDrLYP+319w~ zEKC{^G-3wnLBxOm@L%YGlN0)~9kagA<>dIj?9^YuMnF+>UuFNFn;2ugup!C9Wj!1n zxA@zb6M8H z(Lt&#y;zkc*;&@WHI-DtIq=_Evf-Lafd8x_JCy|U(FH2*qaN_Y{|e9nlB8M6SM>Upng``sDSwGD{-034U!nBk`a3?;rl7 z4oKa}@&_q-%z*#PedfQi_7B$pcKG_gimJt?u-%K*oxURe6aW3j|4h&UnW?`O?d?Kl zuP(s<3VR)(v;Pkr{g0>N%@;QLiD9-sh|9hWJnX4*>sb)NPE; zLl*(!Kk+{R{AZ!t;;Ce61c?8{f8xKI5uNLt_)q*N{&z0UtKXLRPy8qTs~OR`&WZoT zf8u}V;=KB8iT}iZ;=h^^o$H+VPy8qTcP`GW-?n7G(yJaX+0Ouf09wiFCqag?3nwQdeu*Wk^`7w!D zCUcmaqQ!l@*0KC&SN2B>InbCN zV^r$_>x}qV;(y2SzjRI<@n78_!{of>ShpdqgU}@OIqm<+_y4l9`{lhrMDA}qddmM@u_A%^FB)T*9GeRtN(S!N zfk^#+`1Jq64!GR3KRQXkT{O)+kNEFw1L)cRuRZ+7{lhM-?@9a@4KWOktf0#+t;f2a zLGFh!|Jjv&(ciHA9ycT8#Z%02!2cHN|Lt`^p!r`i*96>8B>sm1??n->9_#jJ;6LYn znDd`Cu7i}=qkIZ7Y4!B<#Df|Z;paS0saX&M< z$IaI1aYtzUZ_@z*>;J(|P?;GobD#Jh>bw`_YdO|+Am@IB^#8zr*049Gzj(j$Ph#!^ zU%*lF|4BL^5d1$qI*L`SFsryv{1@#n)Q?vSyS^J~{J*l#_>LNPv;%LdC8z4rgZUcc;ob^^P&5jn4x@ZA5iv(5hl_vb_!{~3*F&7Akhed51pW#M%M?yre7{zELl8h5pT@c}qLP&7Gq;JGQL z47V}~{_a+c@OKTF0M~qxvG6+=Qk7+5E+rHGwo;NxI99HVlVk-PSHNctseo%0%T=<> z%qkoOI9?%3CD`~?*w@4Sp5VOtNd3RNRu`IoYdzl85^4Nz+!yV%S3_u`?_H^Ca?MPU zZE(IV-d?uCXOL>OY=O^P5}ajo0=wiWn|iWKj*W z`w{<%|Nd@W#D9My8o2ur|B3(pZe5Ycf0+Nb;v67KL%usP!rvvShhzO74cQIXcE{}9_)q-jdVuEtmAQYK|MOidM5z5w{lAC-r1(FI|8b)pi2u?U zfb4&W2X>78#m{!JzZeTl@xN~96n1|i{uBR|bpXVFivJDY{GYP#kNkhM{)giK^%^47 z|5N`@{lB~xh}Qpvd;VY92B7&r#Ree%ANl{>7z5P*Q~yu>KlT4S2T1tm|EK<6%=?KW z_5alWQ~&QA14!|Il>f&u2MCS-Y5eaP15EM1l>bZK1KdqF8n%C;@js3KY5Y&)e;WU5 zto=b-fck&x|Ed3X-Us{$>=H|473=V}@JV1Aq5HF2irQ?1ZD8nBcP)k_X@QQfU1zt^f54 z=~Xw0ME+C$f7KoUH2+8Qe=5E^o)Z5Z5eePT)c;fe@9)-0{P#Dafx92^pZM?Z)NAMu~~@9)+{{P#Dafx92^pZM?Z)NAMu~~@9)+{{P#Dafx92^pZM?Z z)&+5YS&_E?_mzBOroV9th5HfziU0m?hb@I&r$!q88~4Y2;%}Tn;eNz_;=jM!;nl(} zzl}Wp8-ML@oI>G##DC(yzuO`Etcx-7_#YkXZ=6Eme#C#`e<0f7)ni>h<2oQR^8XwU z1V*87PvSrEKM?J(^;oy(BZdEs`7zG~0;5p4C-I;7AE?Jy4bJL-UzLRi}`ppfe<-v= zDnrL0K>R2EcTCHpr-lLo#DC&{D6~T=L&qRM{3rf*Ov|IEh5`b_f8u{Av_mRG#~?ub zC;oR#%cG};0s_Q;;(sW#Ln=eZAVB;l{&!5vqo;-f0>ppfe<-v=DnrL00Q_%Lbig(< ztJxNBFI(X=NVQsM|3822|JT+rwN6hZ1SAB2`&Gn$*#-#y{@)eU4heydM4)z?`3vHI z$MC;wW!mQ*S@-m8LZBwkJe&C6G5jyjOr1e3kPzrd1gf{kKSBKOApWmN8{Cm~PtPU< z7&BZ0{6E2U0Qvt|mC6TD3FnooQ=BCmuBoH~&MUH0S;Z<1$%1RLlxL+eE}3x5Ogo0y zQwt;nIvRoMT+?9S|J54ef3Wy}wKOYrU`N+GJ)aN|2-M`6-Xs3QI3OtffBK&V?sOys zLIDA^MXR@&KBoD9n+^zw{hygSf$aWJXz^4AEd;;@_&!L5q6aAc$3Nr$%+#{-tkhSv zFsEyTKqw(l340z?Z!`ZHbin5r`$zGA4n9DY?+^WeZv203;QRZmGAs4%N=sz#BN0mN zm&)Ql1TOFBbze + + + + \ No newline at end of file diff --git a/src/RefScout.Core/Constants.cs b/src/RefScout.Core/Constants.cs new file mode 100644 index 0000000..adaa675 --- /dev/null +++ b/src/RefScout.Core/Constants.cs @@ -0,0 +1,6 @@ +namespace RefScout.Core; + +public class Constants +{ + public const string ApplicationName = "RefScout"; +} \ No newline at end of file diff --git a/src/RefScout.Core/Extensions/ListExtensions.cs b/src/RefScout.Core/Extensions/ListExtensions.cs new file mode 100644 index 0000000..a9f6817 --- /dev/null +++ b/src/RefScout.Core/Extensions/ListExtensions.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; + +namespace RefScout.Core.Extensions; + +public static class Extensions +{ + public static void Deconstruct(this IList list, out T? first, out IList rest) + { + first = list.Count > 0 ? list[0] : default; + rest = list.Skip(1).ToList(); + } + + public static void Deconstruct(this IList list, out T? first, out T? second, out IList rest) + { + first = list.Count > 0 ? list[0] : default; + second = list.Count > 1 ? list[1] : default; + rest = list.Skip(2).ToList(); + } +} \ No newline at end of file diff --git a/src/RefScout.Core/Helpers/ConsoleHelper.cs b/src/RefScout.Core/Helpers/ConsoleHelper.cs new file mode 100644 index 0000000..b085ed3 --- /dev/null +++ b/src/RefScout.Core/Helpers/ConsoleHelper.cs @@ -0,0 +1,28 @@ +using System; + +namespace RefScout.Core.Helpers; + +public static class ConsoleHelper +{ + public static void WriteLine(object message, ConsoleColor? color = null) + { + if (color.HasValue) + { + Console.ForegroundColor = color.Value; + } + + Console.WriteLine(message); + Console.ResetColor(); + } + + public static void Write(object message, ConsoleColor? color = null) + { + if (color.HasValue) + { + Console.ForegroundColor = color.Value; + } + + Console.Write(message); + Console.ResetColor(); + } +} \ No newline at end of file diff --git a/src/RefScout.Core/Helpers/ResourceHelper.cs b/src/RefScout.Core/Helpers/ResourceHelper.cs new file mode 100644 index 0000000..3eac3a3 --- /dev/null +++ b/src/RefScout.Core/Helpers/ResourceHelper.cs @@ -0,0 +1,33 @@ +using System; +using System.IO; +using System.IO.Abstractions; +using System.Linq; + +namespace RefScout.Core.Helpers; + +public static class ResourceHelper +{ + public static bool ExtractResourceToFile( + IFileSystem fileSystem, + string embeddedFileName, + string destinationFileName) + { + var resourceName = GetResourceName(embeddedFileName); + if (resourceName == null) + { + return false; + } + + using var resourceToSave = typeof(T).Assembly.GetManifestResourceStream(resourceName); + + var destinationPath = Path.GetDirectoryName(destinationFileName); + fileSystem.Directory.CreateDirectory(destinationPath!); + using var output = fileSystem.File.Open(destinationFileName, FileMode.Create); + resourceToSave?.CopyTo(output); + return true; + } + + private static string? GetResourceName(string fileName) => + typeof(T).Assembly.GetManifestResourceNames().FirstOrDefault(resourceName => + resourceName.EndsWith(fileName, StringComparison.OrdinalIgnoreCase)); +} \ No newline at end of file diff --git a/src/RefScout.Core/Logging/ConsoleLogger.cs b/src/RefScout.Core/Logging/ConsoleLogger.cs new file mode 100644 index 0000000..41ca5d3 --- /dev/null +++ b/src/RefScout.Core/Logging/ConsoleLogger.cs @@ -0,0 +1,27 @@ +using System; +using RefScout.Core.Helpers; + +namespace RefScout.Core.Logging; + +public class ConsoleLogger : ILogger +{ + public void LogDebug(LogEntry entry) + { + ConsoleHelper.WriteLine(entry.FormattedMessage, ConsoleColor.Cyan); + } + + public void LogInfo(LogEntry entry) + { + Console.WriteLine(entry.FormattedMessage); + } + + public void LogWarn(LogEntry entry) + { + ConsoleHelper.WriteLine(entry.FormattedMessage, ConsoleColor.Yellow); + } + + public void LogError(LogEntry entry) + { + ConsoleHelper.WriteLine(entry.FormattedMessage, ConsoleColor.Red); + } +} \ No newline at end of file diff --git a/src/RefScout.Core/Logging/ILogger.cs b/src/RefScout.Core/Logging/ILogger.cs new file mode 100644 index 0000000..14c63a9 --- /dev/null +++ b/src/RefScout.Core/Logging/ILogger.cs @@ -0,0 +1,14 @@ +namespace RefScout.Core.Logging; + +public interface ILogger +{ + void Log(LogEntry entry) { } + + void LogDebug(LogEntry entry) { } + + void LogInfo(LogEntry entry) { } + + void LogWarn(LogEntry entry) { } + + void LogError(LogEntry entry) { } +} \ No newline at end of file diff --git a/src/RefScout.Core/Logging/LogEntry.cs b/src/RefScout.Core/Logging/LogEntry.cs new file mode 100644 index 0000000..2fd91a6 --- /dev/null +++ b/src/RefScout.Core/Logging/LogEntry.cs @@ -0,0 +1,5 @@ +using System; + +namespace RefScout.Core.Logging; + +public record LogEntry(LogLevel Level, string Message, string FormattedMessage, Exception? Exception = null); \ No newline at end of file diff --git a/src/RefScout.Core/Logging/Logger.cs b/src/RefScout.Core/Logging/Logger.cs new file mode 100644 index 0000000..f67879c --- /dev/null +++ b/src/RefScout.Core/Logging/Logger.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace RefScout.Core.Logging; + +public enum LogLevel +{ + Debug, + Info, + Warn, + Error +} + +public abstract class Logger +{ + private static readonly List Loggers = new(); + + public static LogLevel Level { get; set; } = LogLevel.Warn; + + public static void AddLogger(ILogger logger) + { + Loggers.Add(logger); + } + + private static void Log(LogLevel level, string message, Exception? exception = null) + { + if (Level > level || Loggers.Count == 0) + { + return; + } + + var formattedMessage = FormatMessage(level, message, exception); + var entry = new LogEntry(level, message, formattedMessage, exception); + foreach (var logger in Loggers) + { + logger.Log(entry); + switch (level) + { + case LogLevel.Debug: + logger.LogDebug(entry); + break; + case LogLevel.Info: + logger.LogInfo(entry); + break; + case LogLevel.Warn: + logger.LogWarn(entry); + break; + case LogLevel.Error: + logger.LogError(entry); + break; + } + } + } + + public static void Info(string message) => Log(LogLevel.Info, message); + + public static void Warn(string message) => Log(LogLevel.Warn, message); + + public static void Warn(Exception exception, string message) => Log(LogLevel.Warn, message, exception); + + public static void Error(string message) => Log(LogLevel.Error, message); + + public static void Error(Exception exception, string message) => Log(LogLevel.Error, message, exception); + + private static string FormatMessage(LogLevel level, string message, Exception? exception) + { + var sb = new StringBuilder(); + sb.Append('['); + sb.Append(level.ToString().ToUpper().PadRight(5, ' ')); + sb.Append("] "); + sb.Append(message); + + if (exception != null) + { + sb.Append(": ").Append(exception.Message); + } + + return sb.ToString(); + } +} \ No newline at end of file diff --git a/src/RefScout.Core/RefScout.Core.csproj b/src/RefScout.Core/RefScout.Core.csproj new file mode 100644 index 0000000..7a7b42f --- /dev/null +++ b/src/RefScout.Core/RefScout.Core.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + win-x64;linux-x64 + enable + + + + + + + \ No newline at end of file diff --git a/src/RefScout.Ipc.FrameworkRuntime/Program.cs b/src/RefScout.Ipc.FrameworkRuntime/Program.cs new file mode 100644 index 0000000..8a34226 --- /dev/null +++ b/src/RefScout.Ipc.FrameworkRuntime/Program.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using RefScout.IPC.Server; + +namespace RefScout.IPC.FrameworkRuntime +{ + public class Program + { + public static async Task Main(string[] args) + { + var port = int.Parse(args[0]); + + var server = new PipeIpcServer(port); + server.Start(); + + Console.WriteLine($"Listening on port {port}"); + + await foreach (var request in server.Listen()) + { + if (request.Message == "exit") + { + await request.Reply("goodbye"); + break; + } + + try + { + var context = AppDomain.CreateDomain("fake domain"); + var assembly = context.Load(request.Message); + AppDomain.Unload(context); + await request.Reply($"{assembly.GetName().Version}|{assembly.Location}"); + } + catch + { + await request.Reply("error"); + } + } + } + } +} \ No newline at end of file diff --git a/src/RefScout.Ipc.FrameworkRuntime/RefScout.Ipc.FrameworkRuntime.csproj b/src/RefScout.Ipc.FrameworkRuntime/RefScout.Ipc.FrameworkRuntime.csproj new file mode 100644 index 0000000..d918a7d --- /dev/null +++ b/src/RefScout.Ipc.FrameworkRuntime/RefScout.Ipc.FrameworkRuntime.csproj @@ -0,0 +1,26 @@ + + + + Exe + net472 + 8.0 + + + + AnyCPU + + + + AnyCPU + + + + + + + + + + + + \ No newline at end of file diff --git a/src/RefScout.Ipc/Client/IIpcClient.cs b/src/RefScout.Ipc/Client/IIpcClient.cs new file mode 100644 index 0000000..6808614 --- /dev/null +++ b/src/RefScout.Ipc/Client/IIpcClient.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading.Tasks; + +namespace RefScout.IPC.Client +{ + public interface IIpcClient : IDisposable + { + bool Started { get; } + + void Start(string executableFileName, int? port = null); + + string Send(string message); + + Task SendAsync(string message); + } +} \ No newline at end of file diff --git a/src/RefScout.Ipc/Client/IpcClient.cs b/src/RefScout.Ipc/Client/IpcClient.cs new file mode 100644 index 0000000..a91618e --- /dev/null +++ b/src/RefScout.Ipc/Client/IpcClient.cs @@ -0,0 +1,76 @@ +using System; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace RefScout.IPC.Client +{ + public abstract class IpcClient : IIpcClient + { + private Process? _process; + public int Port { get; set; } + + public bool Started { get; set; } + + public void Start(string executableFileName, int? port = null) + { + Port = port ?? FreeTcpPort(); + _process = new Process + { + StartInfo = new ProcessStartInfo(executableFileName, Port.ToString()) + { + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = false + } + }; + _process.Exited += (_, _) => _process = null; + _process.Start(); + + Started = true; + } + + public abstract string Send(string message); + + public abstract Task SendAsync(string message); + + private static int FreeTcpPort() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return port; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + if (_process == null) + { + return; + } + + try + { + Send("exit"); + _process.WaitForExit(); + } + finally + { + _process.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/src/RefScout.Ipc/Client/PipeIpcClient.cs b/src/RefScout.Ipc/Client/PipeIpcClient.cs new file mode 100644 index 0000000..8a96827 --- /dev/null +++ b/src/RefScout.Ipc/Client/PipeIpcClient.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using System.IO.Pipes; +using System.Threading.Tasks; + +namespace RefScout.IPC.Client +{ + public class PipeIpcClient : IpcClient + { + private NamedPipeClientStream? _client; + private StreamReader? _reader; + private StreamWriter? _writer; + + public override string Send(string message) + { + if (!Started) + { + throw new Exception("IPC client has not been started, call Start() first."); + } + + if (_client == null || _reader == null || _writer == null) + { + _client = new NamedPipeClientStream($"PipeIpc-{Port}"); + _client.Connect(3000); + _reader = new StreamReader(_client); + _writer = new StreamWriter(_client); + } + + _writer.WriteLine(message); + _writer.Flush(); + return _reader.ReadLine()!; + } + + public override async Task SendAsync(string message) + { + if (!Started) + { + throw new Exception("IPC client has not been started, call Start() first."); + } + + if (_client == null) + { + _client = new NamedPipeClientStream($"PipeIpc-{Port}"); + await _client.ConnectAsync().ConfigureAwait(false); + } + + using var reader = new StreamReader(_client); + using var writer = new StreamWriter(_client); + + await writer.WriteLineAsync(message).ConfigureAwait(false); + await writer.FlushAsync().ConfigureAwait(false); + return await reader.ReadLineAsync().ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/RefScout.Ipc/Client/TcpIpcClient.cs b/src/RefScout.Ipc/Client/TcpIpcClient.cs new file mode 100644 index 0000000..ae637b5 --- /dev/null +++ b/src/RefScout.Ipc/Client/TcpIpcClient.cs @@ -0,0 +1,60 @@ +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace RefScout.IPC.Client +{ + public class TcpIpcClient : IpcClient + { + private TcpClient? _client; + private NetworkStream? _stream; + + public override string Send(string message) + { + if (_client == null || _stream == null) + { + _client = new TcpClient(); + _client.Connect("127.0.0.1", Port); + _stream = _client.GetStream(); + } + + var buffer = Encoding.ASCII.GetBytes(message); + _stream.Write(buffer, 0, buffer.Length); + + var responseBuffer = new byte[4096]; + var read = _stream.Read(responseBuffer, 0, responseBuffer.Length); + + return Encoding.ASCII.GetString(responseBuffer, 0, read); + } + + public override async Task SendAsync(string message) + { + if (_client == null || _stream == null) + { + _client = new TcpClient(); + await _client.ConnectAsync("127.0.0.1", Port).ConfigureAwait(false); + _stream = _client.GetStream(); + } + + var buffer = Encoding.ASCII.GetBytes(message); + await _stream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + + var responseBuffer = new byte[4096]; + var read = await _stream.ReadAsync(responseBuffer, 0, responseBuffer.Length).ConfigureAwait(false); + + return Encoding.ASCII.GetString(responseBuffer, 0, read); + } + + protected override void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + base.Dispose(disposing); + _stream?.Dispose(); + _client?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/RefScout.Ipc/IsExternalInit.cs b/src/RefScout.Ipc/IsExternalInit.cs new file mode 100644 index 0000000..9cdf925 --- /dev/null +++ b/src/RefScout.Ipc/IsExternalInit.cs @@ -0,0 +1,6 @@ +// ReSharper disable once CheckNamespace + +namespace System.Runtime.CompilerServices +{ + internal static class IsExternalInit { } +} \ No newline at end of file diff --git a/src/RefScout.Ipc/RefScout.Ipc.csproj b/src/RefScout.Ipc/RefScout.Ipc.csproj new file mode 100644 index 0000000..b8ffe20 --- /dev/null +++ b/src/RefScout.Ipc/RefScout.Ipc.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.0 + win-x64;linux-x64 + 9.0 + enable + + + + + + + \ No newline at end of file diff --git a/src/RefScout.Ipc/Server/IIpcRequest.cs b/src/RefScout.Ipc/Server/IIpcRequest.cs new file mode 100644 index 0000000..45a2feb --- /dev/null +++ b/src/RefScout.Ipc/Server/IIpcRequest.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace RefScout.IPC.Server +{ + public interface IIpcRequest + { + string Message { get; } + Task Reply(string message); + } +} \ No newline at end of file diff --git a/src/RefScout.Ipc/Server/IIpcServer.cs b/src/RefScout.Ipc/Server/IIpcServer.cs new file mode 100644 index 0000000..175b297 --- /dev/null +++ b/src/RefScout.Ipc/Server/IIpcServer.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace RefScout.IPC.Server +{ + public interface IIpcServer + { + void Start(); + + IAsyncEnumerable Listen(); + } +} \ No newline at end of file diff --git a/src/RefScout.Ipc/Server/PipeIpcRequest.cs b/src/RefScout.Ipc/Server/PipeIpcRequest.cs new file mode 100644 index 0000000..e3ac1e5 --- /dev/null +++ b/src/RefScout.Ipc/Server/PipeIpcRequest.cs @@ -0,0 +1,13 @@ +using System.IO; +using System.Threading.Tasks; + +namespace RefScout.IPC.Server +{ + public record PipeIpcRequest(string Message, StreamWriter Writer) : IIpcRequest + { + public async Task Reply(string message) + { + await Writer.WriteLineAsync(message).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/RefScout.Ipc/Server/PipeIpcServer.cs b/src/RefScout.Ipc/Server/PipeIpcServer.cs new file mode 100644 index 0000000..5058313 --- /dev/null +++ b/src/RefScout.Ipc/Server/PipeIpcServer.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.IO; +using System.IO.Pipes; + +namespace RefScout.IPC.Server +{ + public class PipeIpcServer : IIpcServer + { + private readonly int _port; + + public PipeIpcServer(int port) + { + _port = port; + } + + public void Start() + { + // started for each client in listen + } + + public async IAsyncEnumerable Listen() + { + while (true) + { + using var server = new NamedPipeServerStream($"PipeIpc-{_port}"); + await server.WaitForConnectionAsync().ConfigureAwait(false); + + var reader = new StreamReader(server); + var writer = new StreamWriter(server); + while (server.IsConnected) + { + var message = await reader.ReadLineAsync().ConfigureAwait(false); + if (string.IsNullOrEmpty(message)) + { + break; + } + + yield return new PipeIpcRequest(message, writer); + + await writer.FlushAsync().ConfigureAwait(false); + server.WaitForPipeDrain(); + } + + try + { + reader.Dispose(); + writer.Dispose(); + } + catch + { + // pass + } + } + // ReSharper disable once IteratorNeverReturns + } + } +} \ No newline at end of file diff --git a/src/RefScout.Ipc/Server/TcpIpcRequest.cs b/src/RefScout.Ipc/Server/TcpIpcRequest.cs new file mode 100644 index 0000000..0bf2ad0 --- /dev/null +++ b/src/RefScout.Ipc/Server/TcpIpcRequest.cs @@ -0,0 +1,15 @@ +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace RefScout.IPC.Server +{ + public record TcpIpcRequest(string Message, NetworkStream Stream) : IIpcRequest + { + public async Task Reply(string message) + { + var response = Encoding.UTF8.GetBytes(message); + await Stream.WriteAsync(response, 0, response.Length); + } + } +} \ No newline at end of file diff --git a/src/RefScout.Ipc/Server/TcpIpcServer.cs b/src/RefScout.Ipc/Server/TcpIpcServer.cs new file mode 100644 index 0000000..88b7fe0 --- /dev/null +++ b/src/RefScout.Ipc/Server/TcpIpcServer.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace RefScout.IPC.Server +{ + public class TcpIpcServer : IIpcServer + { + private readonly int _port; + private TcpListener? _listener; + + public TcpIpcServer(int port) + { + _port = port; + } + + public void Start() + { + _listener = new TcpListener(IPAddress.Loopback, _port); + _listener.Start(); + } + + public async IAsyncEnumerable Listen() + { + if (_listener == null) + { + throw new InvalidOperationException("Call Start() before listening for requests."); + } + + while (true) + { + using var tcpClient = await _listener.AcceptTcpClientAsync(); + using var networkStream = tcpClient.GetStream(); + + while (true) + { + string? message; + try + { + var buffer = new byte[4096]; + var byteCount = await networkStream.ReadAsync(buffer, 0, buffer.Length); + message = Encoding.ASCII.GetString(buffer, 0, byteCount); + } + catch + { + break; + } + + yield return new TcpIpcRequest(message, networkStream); + } + } + } + } +} \ No newline at end of file diff --git a/src/RefScout.Visualizers/AssemblyTreeFilter.cs b/src/RefScout.Visualizers/AssemblyTreeFilter.cs new file mode 100644 index 0000000..d473f67 --- /dev/null +++ b/src/RefScout.Visualizers/AssemblyTreeFilter.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using RefScout.Analyzer; +using RefScout.Analyzer.Helpers; +using RefScout.Analyzer.Notes; + +namespace RefScout.Visualizers; + +internal static class AssemblyTreeFilter +{ + public static IReadOnlyList OnlyApplicationAssemblies(IEnumerable assemblies) + { + return AssemblyHelper.Clone(assemblies, + assembly => !assembly.IsSystem && !assembly.IsNetApi || assembly.IsLocalSystem || + assembly.IsEntryPoint); + } + + public static IReadOnlyList OnlyConflictAssemblies( + IEnumerable assemblies, + NoteLevel minLevel) + { + var resultAssemblies = new HashSet(); + var conflictAssemblies = new List(); + + var assemblyList = AssemblyHelper.Clone(assemblies); + foreach (var assembly in assemblyList.Where(assembly => assembly.Level >= minLevel)) + { + if (assembly.IsArchitectureMismatch) + { + conflictAssemblies.Add(assembly); + } + + if (assembly.IsArchitectureMismatch && (!assembly.IsArchitectureMismatch || assembly.Notes.Count < 2)) + { + continue; + } + + conflictAssemblies.AddRange(assembly.ReferencedBy.Select(r => r.From)); + resultAssemblies.Add(assembly); + } + + var entryPoint = assemblyList.SingleOrDefault(a => a.IsEntryPoint); + if (entryPoint == null) + { + return assemblyList; + } + + resultAssemblies.Add(entryPoint); + + // Add assemblies in path of conflicting assemblies + var shortestPath = new BfsShortestPath(entryPoint); + shortestPath.Prepare(); + foreach (var result in conflictAssemblies + .Where(assembly => !resultAssemblies.Contains(assembly)) + .SelectMany(assembly => shortestPath.GetPath(assembly))) + { + resultAssemblies.Add(result); + } + + // Remove now unused references + foreach (var assembly in resultAssemblies) + { + assembly.References.RemoveAll(r => resultAssemblies.All(a => a.FullName != r.To.FullName)); + assembly.ReferencedBy.RemoveAll(r => resultAssemblies.All(a => a.FullName != r.From.FullName)); + } + + return resultAssemblies.ToList(); + } + + private class BfsShortestPath + { + private readonly Assembly _start; + private readonly Dictionary _previous; + + public BfsShortestPath(Assembly start) + { + _start = start ?? throw new ArgumentNullException(nameof(start)); + _previous = new Dictionary(); + } + + public void Prepare() + { + var queue = new Queue(); + queue.Enqueue(_start); + + while (queue.Count > 0) + { + var assembly = queue.Dequeue(); + foreach (var reference in assembly.References) + { + var to = reference.To; + if (_previous.ContainsKey(to) || to.Name == "netstandard") + { + continue; + } + + _previous[to] = assembly; + queue.Enqueue(to); + } + } + } + + public IReadOnlyList GetPath(Assembly assembly) + { + var path = new List(); + var current = assembly; + while (!current.Equals(_start)) + { + path.Add(current); + if (!_previous.ContainsKey(current)) + { + break; + } + + current = _previous[current]; + } + + path.Add(_start); + path.Reverse(); + return path; + } + } + + //private static Func> ShortestPathFunction(Assembly start) + //{ + // // Simple BFS for shortest paths to conflicting assemblies + // var previous = new Dictionary(); + // var queue = new Queue(); + // queue.Enqueue(start); + // while (queue.Count > 0) + // { + // var assembly = queue.Dequeue(); + // foreach (var reference in assembly.References) + // { + // var to = reference.To; + // if (previous.ContainsKey(to) || to.Name == "netstandard") + // { + // continue; + // } + + // previous[to] = assembly; + // queue.Enqueue(to); + // } + // } + + // return assembly => + // { + // var path = new List(); + // var current = assembly; + // while (!current.Equals(start)) + // { + // path.Add(current); + // if (!previous.ContainsKey(current)) + // { + // break; + // } + + // current = previous[current]; + // } + + // path.Add(start); + // path.Reverse(); + // return path; + // }; + //} +} \ No newline at end of file diff --git a/src/RefScout.Visualizers/Console/ConsoleVisualizer.cs b/src/RefScout.Visualizers/Console/ConsoleVisualizer.cs new file mode 100644 index 0000000..5dfafc9 --- /dev/null +++ b/src/RefScout.Visualizers/Console/ConsoleVisualizer.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using RefScout.Analyzer; +using RefScout.Analyzer.Config.Framework; +using RefScout.Analyzer.Context; +using RefScout.Analyzer.Notes; +using RefScout.Core.Helpers; +using SysConsole = System.Console; + +namespace RefScout.Visualizers.Console; + +public class ConsoleVisualizerOptions : IVisualizerOptions +{ + public bool Detailed { get; init; } = true; +} + +public class ConsoleVisualizer : Visualizer +{ + private const string Tab = " "; + + private const ConsoleColor ColorText = ConsoleColor.White; + private const ConsoleColor ColorGac = ConsoleColor.DarkMagenta; + private const ConsoleColor ColorInfo = ConsoleColor.Cyan; + private const ConsoleColor ColorWarning = ConsoleColor.Yellow; + private const ConsoleColor ColorFatal = ConsoleColor.Red; + private const ConsoleColor ColorSuccess = ConsoleColor.Green; + + protected override bool IsTreeVisualization => false; + protected override VisualizeMode DefaultMode => VisualizeMode.Conflicts; + + protected override void RunVisualizer( + IAnalyzerResult result, + IReadOnlyList assemblies, + VisualizeMode mode, + ConsoleVisualizerOptions options) + { + WriteEnvironmentInfo(result); + + WriteLegend(); + + var conflictsWereFound = false; + foreach (var assembly in assemblies.Where(a => a.Level != NoteLevel.Message)) + { + var nonSystemIfApp = mode == VisualizeMode.App && !(assembly.IsSystem || assembly.IsNetApi); + var displayAssembly = mode == VisualizeMode.All || IsConflictAssembly(assembly) || nonSystemIfApp; + if (assembly.IsEntryPoint) + { + WriteEntryPoint(assembly); + SysConsole.WriteLine(); + } + else if (displayAssembly) + { + conflictsWereFound = true; + WriteAssembly(assembly, options.Detailed); + SysConsole.WriteLine(); + } + } + + var frameworkConfig = result is FrameworkAnalyzerResult analyzerResult ? analyzerResult.Config : null; + if (frameworkConfig != null) + { + WriteBindingConfigErrors(frameworkConfig); + } + + if (mode == VisualizeMode.All || conflictsWereFound || frameworkConfig?.ErrorReport.HasErrors == true) + { + return; + } + + ConsoleHelper.WriteLine("No conflicts were found.", + ColorSuccess); + SysConsole.WriteLine(); + } + + private static void WriteLegend() + { + SysConsole.WriteLine(); + ConsoleHelper.Write("Legend: "); + ConsoleHelper.Write("Success", ColorSuccess); + ConsoleHelper.Write(", "); + ConsoleHelper.Write("Info", ColorInfo); + ConsoleHelper.Write(", "); + ConsoleHelper.Write("Warning", ColorWarning); + ConsoleHelper.Write(", "); + ConsoleHelper.WriteLine("Fatal", ColorFatal); + SysConsole.WriteLine(); + } + + private static void WriteEnvironmentInfo(IAnalyzerResult result) + { + SysConsole.WriteLine(); + + ConsoleHelper.WriteLine("Environment information:", ConsoleColor.White); + ConsoleHelper.WriteLine(" .NET Core: ", ConsoleColor.White); + + if (result is CoreAnalyzerResult coreAnalyzerResult) + { + ConsoleHelper.Write(" Used runtime version: ", ConsoleColor.White); + if (coreAnalyzerResult.Config.SelfContained) + { + ConsoleHelper.WriteLine("self-contained application"); + } + else if (coreAnalyzerResult.Runtime != null) + { + ConsoleHelper.Write(coreAnalyzerResult.Runtime.VersionName); + ConsoleHelper.WriteLine($" (policy: {coreAnalyzerResult.Config.RollForward})"); + } + else + { + ConsoleHelper.WriteLine("Could not find appropriate runtime version."); + } + } + + if (result.EnvironmentInfo.Core != null) + { + var x64Runtimes = result.EnvironmentInfo.Core.Runtimes.Where(x => x.Is64Bit).ToList(); + var x86Runtimes = result.EnvironmentInfo.Core.Runtimes.Where(x => !x.Is64Bit).ToList(); + ConsoleHelper.Write(" Installed versions: ", ConsoleColor.White); + ConsoleHelper.WriteLine(x64Runtimes.Count > 0 + ? string.Join(", ", x64Runtimes.Select(s => s.VersionName)) + : "None installed."); + + ConsoleHelper.Write(" Installed versions (x86): ", ConsoleColor.White); + ConsoleHelper.WriteLine(x86Runtimes.Count > 0 + ? string.Join(", ", x86Runtimes.Select(s => s.VersionName)) + : "None installed."); + } + + if (result.EnvironmentInfo.Framework?.Runtimes.Count > 0) + { + ConsoleHelper.WriteLine(" .NET Framework: ", ConsoleColor.White); + foreach (var (framework, version, servicePack) in result.EnvironmentInfo.Framework.Runtimes) + { + ConsoleHelper.WriteLine( + $" {framework}: {version} {(servicePack > 0 ? $"(service pack {servicePack})" : "")}"); + } + } + } + + private static void WriteEntryPoint(Assembly assembly) + { + ConsoleHelper.Write("Entry assembly: "); + ConsoleHelper.WriteLine(assembly.FullName, ColorText); + + foreach (var (_, to, version) in assembly.References) + { + ConsoleHelper.WriteLine($"{Tab}{to.Name} -> {version}", ColorText); + } + } + + private static void WriteAssembly(Assembly assembly, bool detailed) + { + ConsoleHelper.Write("Reference: "); + var assemblyColor = GetNoteLevelColor(assembly.Level); + ConsoleHelper.WriteLine(assembly.FullName, assemblyColor); + + if (detailed) + { + if (assembly.TargetFramework != null) + { + SysConsole.Write($"Framework: {assembly.TargetFramework}"); + } + + if (assembly.SourceLanguage != AssemblySourceLanguage.Unknown) + { + SysConsole.Write($", Language: {assembly.SourceLanguage}"); + } + + SysConsole.WriteLine(); + } + + if (assembly.Path != null) + { + SysConsole.WriteLine($" Path: {assembly.Path}"); + } + + if (assembly.Source is AssemblySource.Gac or AssemblySource.Shared) + { + ConsoleHelper.Write(" Note: "); + if (assembly.Source == AssemblySource.Gac) + { + ConsoleHelper.Write("Assembly was found in the ", ColorText); + ConsoleHelper.WriteLine("GAC", + ColorGac); + } + else + { + ConsoleHelper.Write("Assembly was found in the ", ColorText); + ConsoleHelper.Write("Shared", + ColorGac); + ConsoleHelper.WriteLine(" runtime folder.", ColorText); + } + } + + foreach (var note in assembly.Notes) + { + ConsoleHelper.Write(" Note: "); + ConsoleHelper.WriteLine(note.Message, ColorText); + } + + foreach (var reference in assembly.ReferencedBy) + { + WriteAssemblyReference(reference); + } + } + + private static void WriteAssemblyReference(AssemblyRef reference) + { + ConsoleHelper.Write(Tab); + var referenceColor = GetNoteLevelColor(reference.Level); + ConsoleHelper.Write(reference.Version, + referenceColor == ColorSuccess ? ColorWarning : referenceColor); + + if (reference.BindingRedirect != null && + reference.BindingRedirectStatus != BindingRedirectStatus.Default) + { + ConsoleHelper.Write(" -> "); + ConsoleHelper.Write(reference.ActualVersion, referenceColor); + } + + ConsoleHelper.Write(" by "); + ConsoleHelper.WriteLine(reference.From.Name, ColorText); + } + + private static void WriteBindingConfigErrors(FrameworkConfig config) + { + if (config.ErrorReport.HasErrors) + { + return; + } + + SysConsole.WriteLine(); + ConsoleHelper.WriteLine("The applications config file contains invalid configurations:", + ColorWarning); + SysConsole.WriteLine(config.ErrorReport.ToString()); + } + + private static bool IsConflictAssembly(Assembly assembly) => + assembly.ReferencedBy.Any(a => a.Level != NoteLevel.Default); + + private static ConsoleColor GetNoteLevelColor(NoteLevel level) + => level switch + { + NoteLevel.Info => ColorInfo, + NoteLevel.Warning => ColorWarning, + NoteLevel.Fatal => ColorFatal, + NoteLevel.Success => ColorSuccess, + _ => ColorText + }; +} \ No newline at end of file diff --git a/src/RefScout.Visualizers/Dgml/DgmlConflictVisualizer.cs b/src/RefScout.Visualizers/Dgml/DgmlConflictVisualizer.cs new file mode 100644 index 0000000..6ddace3 --- /dev/null +++ b/src/RefScout.Visualizers/Dgml/DgmlConflictVisualizer.cs @@ -0,0 +1,177 @@ +using System.Collections.Generic; +using System.Linq; +using OpenSoftware.DgmlTools; +using OpenSoftware.DgmlTools.Builders; +using OpenSoftware.DgmlTools.Model; +using RefScout.Analyzer; +using RefScout.Analyzer.Notes; + +namespace RefScout.Visualizers.Dgml; + +public record DgmlConflictVisualizerOptions(string OutputFile) : IVisualizerOptions +{ + public bool ShowTargetFramework { get; init; } +} + +public class DgmlConflictVisualizer : Visualizer +{ + protected override void RunVisualizer( + IAnalyzerResult result, + IReadOnlyList assemblies, + VisualizeMode mode, + DgmlConflictVisualizerOptions options) + { + var dgmlBuilder = new DgmlBuilder + { + NodeBuilders = new NodeBuilder[] + { + new NodeBuilder(CreateAssemblyNode), + new NodeBuilder(a => new Node + { + Id = a.FullName + "_note", + Label = GetCommentForAssembly(a), + Category = "Note" + }, a => a.Notes.Count > 0), + new NodeBuilder(f => new Node + { + Id = f.ToString(), + Label = f.ToString(), + Category = "TargetFramework" + }, _ => options.ShowTargetFramework) + }, + LinkBuilders = new LinkBuilder[] + { + new LinkBuilder(a => new Link + { + Category = "Note", + Source = a.FullName, + Target = a.FullName + "_note" + }, a => a.Notes.Count > 0), + new LinksBuilder(a => CreateReferenceLinks(a, options.ShowTargetFramework)) + } + }; + + var nodes = new List(); + nodes.AddRange(assemblies.Where(a => !a.IsUnreferenced)); + nodes.AddRange(GetTargetFrameworks(assemblies)); + + var graph = dgmlBuilder.Build(nodes); + graph.Styles = DgmlStyles.ConflictStyles.ToList(); + + AddUnreferencedAssemblies(graph, assemblies); + WriteToFile(options.OutputFile, XmlHelper.SerializeObjectUtf8(graph)); + } + + private static IEnumerable GetTargetFrameworks(IEnumerable assemblies) + { + return assemblies + .Where(a => !a.IsUnreferenced) + .Select(a => a.TargetFramework) + .Where(t => t != null) + .GroupBy(p => p!.Id) + .Select(g => g.FirstOrDefault()) + .Cast() + .ToList(); + } + + private static string GetCommentForAssembly(Assembly assembly) + { + var result = string.Empty; + foreach (var comment in assembly.Notes) + { + result += comment.Message + "\n"; + } + + return result.Trim(); + } + + private static void AddUnreferencedAssemblies(DirectedGraph graph, IEnumerable assemblies) + { + var unreferencedAssemblies = + assemblies.Where(a => a.IsUnreferenced).ToList(); + if (unreferencedAssemblies.Count == 0) + { + return; + } + + graph.Nodes.Add(new Node + { + Id = "UnreferencedAssemblies", + Category = "UnreferencedAssembliesGroup", + Label = "Unreferenced Assemblies", + Group = "Expanded" + }); + + graph.Nodes.Add(new Node + { + Id = "UnreferencedAssemblies_comment", + Label = unreferencedAssemblies.Count == 1 + ? "There was 1 unreferenced assembly found, this assembly was found in the directory of the used assemblies." + : $"There were {unreferencedAssemblies.Count} unreferenced assemblies found, these assemblies were found in the directory of the used assemblies.", + Category = "Comment" + }); + graph.Links.Add(new Link + { + Category = "Comment", + Source = "UnreferencedAssemblies", + Target = "UnreferencedAssemblies_comment" + }); + + foreach (var assembly in unreferencedAssemblies) + { + graph.Nodes.Add(new Node + { + Id = assembly.FullName, + Label = assembly.Name, + Category = "UnreferencedAssemblies" + }); + graph.Links.Add(new Link + { + Source = "UnreferencedAssemblies", + Target = assembly.FullName, + Category = "Contains" + }); + } + } + + private static Node CreateAssemblyNode(Assembly assembly) => + new() + { + Id = assembly.FullName, + Label = $"{assembly.Name}\n{assembly.Version}", + Category = assembly.Level <= NoteLevel.Default && + assembly.Source is AssemblySource.Gac or AssemblySource.Shared + ? "Assembly_GAC" + : $"Assembly_{assembly.Level}" + }; + + private static IEnumerable CreateReferenceLinks(Assembly assembly, bool showTargetFramework) + { + var referencedBy = assembly.ReferencedBy.ToList(); + var showVersion = referencedBy.Any(a => a.Level == NoteLevel.Warning); + foreach (var reference in referencedBy) + { + var versionString = reference.BindingRedirect != null + ? $"{reference.BindingRedirect.NewVersion} ({reference.Version})" + : reference.Version.ToString(); + + yield return new Link + { + Source = reference.From.FullName, + Target = assembly.FullName, + Label = showVersion ? versionString : string.Empty, + Category = $"Assembly_{reference.Level}" + }; + } + + if (showTargetFramework && assembly.TargetFramework != null) + { + yield return new Link + { + Source = assembly.FullName, + Target = assembly.TargetFramework.ToString(), + Category = "TargetFramework" + }; + } + } +} \ No newline at end of file diff --git a/src/RefScout.Visualizers/Dgml/DgmlStyles.cs b/src/RefScout.Visualizers/Dgml/DgmlStyles.cs new file mode 100644 index 0000000..fa88dbe --- /dev/null +++ b/src/RefScout.Visualizers/Dgml/DgmlStyles.cs @@ -0,0 +1,190 @@ +using System.Collections.Generic; +using OpenSoftware.DgmlTools.Model; + +namespace RefScout.Visualizers.Dgml; + +internal static class DgmlStyles +{ + public static IEnumerable + \ No newline at end of file diff --git a/src/RefScout.Wpf/Styles/Brushes.Dark.xaml b/src/RefScout.Wpf/Styles/Brushes.Dark.xaml new file mode 100644 index 0000000..43a63c8 --- /dev/null +++ b/src/RefScout.Wpf/Styles/Brushes.Dark.xaml @@ -0,0 +1,191 @@ + + + #242424 + #313131 + #444 + #222 + + #FFF + #ccc + #AAA + + #2B2B2B + #313131 + #444 + #555 + + #383838 + #777 + #444 + + #555 + #666 + #777 + + #00A2E8 + #2ade81 + #F2A100 + #EF5858 + + #8de38d + #3094e3 + #cc94db + #b680b3 + + #ba8ced + + #EA7BF7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/RefScout.Wpf/Styles/Brushes.Light.xaml b/src/RefScout.Wpf/Styles/Brushes.Light.xaml new file mode 100644 index 0000000..77d38d5 --- /dev/null +++ b/src/RefScout.Wpf/Styles/Brushes.Light.xaml @@ -0,0 +1,186 @@ + + + #fff + #edebe9 + #d2d0ce + #ccc + + #000 + #333 + #555 + + #fff + #333 + #e0e0e0 + #ccc + + #ccc + #B2B2B2 + #444 + + #d2d0ce + #bebbb8 + #a19f9d + + #00A2E8 + #5FAD61 + #D89000 + #E05353 + + #54AD54 + #185C92 + #9568CF + #6295CB + + #6A4097 + #D872E5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/RefScout.Wpf/Styles/Buttons.xaml b/src/RefScout.Wpf/Styles/Buttons.xaml new file mode 100644 index 0000000..785394a --- /dev/null +++ b/src/RefScout.Wpf/Styles/Buttons.xaml @@ -0,0 +1,251 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/RefScout.Wpf/Styles/ComboBox.xaml b/src/RefScout.Wpf/Styles/ComboBox.xaml new file mode 100644 index 0000000..517b290 --- /dev/null +++ b/src/RefScout.Wpf/Styles/ComboBox.xaml @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/RefScout.Wpf/Styles/Icons.xaml b/src/RefScout.Wpf/Styles/Icons.xaml new file mode 100644 index 0000000..77d7ccb --- /dev/null +++ b/src/RefScout.Wpf/Styles/Icons.xaml @@ -0,0 +1,111 @@ + + M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z + + M8.59,16.58L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.58Z + M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z + + M15.5,14L20.5,19L19,20.5L14,15.5V14.71L13.73,14.43C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.43,13.73L14.71,14H15.5M9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14M12,10H10V12H9V10H7V9H9V7H10V9H12V10Z + M15.5,14H14.71L14.43,13.73C15.41,12.59 16,11.11 16,9.5A6.5,6.5 0 0,0 9.5,3A6.5,6.5 0 0,0 3,9.5A6.5,6.5 0 0,0 9.5,16C11.11,16 12.59,15.41 13.73,14.43L14,14.71V15.5L19,20.5L20.5,19L15.5,14M9.5,14C7,14 5,12 5,9.5C5,7 7,5 9.5,5C12,5 14,7 14,9.5C14,12 12,14 9.5,14M7,9H12V10H7V9Z + M20,2H4C2.89,2 2,2.89 2,4V20C2,21.11 2.89,22 4,22H20C21.11,22 22,21.11 22,20V4C22,2.89 21.11,2 20,2M20,20H4V4H20M13,8V10H11V8H9L12,5L15,8M16,15V13H14V11H16V9L19,12M10,13H8V15L5,12L8,9V11H10M15,16L12,19L9,16H11V14H13V16 + + M 2.1818182,6.5454544 A 2.1818182,2.1818182 0 0 0 0,8.7272727 v 6.5454553 a 2.1818182,2.1818182 0 0 0 2.1818182,2.181818 H 4.3636364 A 2.1818182,2.1818182 0 0 0 6.5454546,15.272728 V 10.909091 H 4.3636364 v 4.363637 H 2.1818182 V 8.7272727 H 6.5454546 V 6.5454544 Z m 8.7272728,0 A 2.1818182,2.1818182 0 0 0 8.7272728,8.7272727 V 17.454546 H 10.909091 V 13.09091 h 2.181818 v 4.363636 h 2.181818 V 8.7272727 A 2.1818182,2.1818182 0 0 0 13.090909,6.5454544 Z m 8.727273,0 a 2.1818182,2.1818182 0 0 0 -2.181818,2.1818183 v 6.5454553 a 2.1818182,2.1818182 0 0 0 2.181818,2.181818 h 2.181818 A 2.1818182,2.1818182 0 0 0 24,15.272728 v -1.090909 h -2.181818 v 1.090909 H 19.636364 V 8.7272727 h 2.181818 V 9.8181816 H 24 V 8.7272727 A 2.1818182,2.1818182 0 0 0 21.818182,6.5454544 Z m -8.727273,2.1818183 h 2.181818 v 2.1818183 h -2.181818 z + + M 9 0 L 9 3 L 5 3 L 5 1 L 0 1 L 0 6 L 5 6 L 5 4 L 9 4 L 9 7 L 16 7 L 16 0 L 9 0 z M 10 1 L 15 1 L 15 6 L 10 6 L 10 1 z M 1 2 L 4 2 L 4 5 L 1 5 L 1 2 z + + M11.5,15.97L11.91,18.41C11.65,18.55 11.23,18.68 10.67,18.8C10.1,18.93 9.43,19 8.66,19C6.45,18.96 4.79,18.3 3.68,17.04C2.56,15.77 2,14.16 2,12.21C2.05,9.9 2.72,8.13 4,6.89C5.32,5.64 6.96,5 8.94,5C9.69,5 10.34,5.07 10.88,5.19C11.42,5.31 11.82,5.44 12.08,5.59L11.5,8.08L10.44,7.74C10.04,7.64 9.58,7.59 9.05,7.59C7.89,7.58 6.93,7.95 6.18,8.69C5.42,9.42 5.03,10.54 5,12.03C5,13.39 5.37,14.45 6.08,15.23C6.79,16 7.79,16.4 9.07,16.41L10.4,16.29C10.83,16.21 11.19,16.1 11.5,15.97M13.89,19L14.5,15H13L13.34,13H14.84L15.16,11H13.66L14,9H15.5L16.11,5H18.11L17.5,9H18.5L19.11,5H21.11L20.5,9H22L21.66,11H20.16L19.84,13H21.34L21,15H19.5L18.89,19H16.89L17.5,15H16.5L15.89,19H13.89M16.84,13H17.84L18.16,11H17.16L16.84,13Z + M 16.24542,5 C 15.075191,5 14.011832,5.0855 13.194275,5.272519 V 18.855725 C 13.803436,18.935875 14.669084,19 15.695038,19 c 2.31374,0 3.708398,-0.496946 4.531298,-1.394656 0.657252,-0.678626 1.025954,-1.560305 1.025954,-2.709161 0,-1.517557 -0.860305,-2.858779 -2.458015,-3.307634 v -0.08015 c 1.373282,-0.59313 2.00916,-1.784734 2.00916,-3.019085 0,-0.961832 -0.309923,-1.886259 -1.047328,-2.500764 C 18.794275,5.165649 17.538549,5 16.24542,5 Z M 2.74771,5.06411 5.8416032,18.893118 H 8.8606868 L 12.018702,5.064117 H 9.0637409 l -0.940458,5.909924 C 7.8721375,12.534347 7.6477108,14.196178 7.4660307,15.836636 H 7.4232827 C 7.2416039,14.212214 6.9530536,12.512978 6.7072527,11.032824 L 5.7187021,5.064122 Z m 13.829007,1.945038 c 1.106108,0 1.576336,0.785497 1.576336,1.790077 0,1.127481 -0.737403,1.950381 -1.699236,1.950381 H 15.882062 V 7.094645 c 0.160304,-0.05878 0.406106,-0.0855 0.694655,-0.0855 z m -0.694655,5.728245 h 0.571755 c 1.068703,0.02138 2.009161,0.737405 2.009161,2.11603 0,1.41603 -0.903055,2.073283 -1.993131,2.073283 -0.245801,0 -0.427481,-0.0053 -0.587785,-0.04275 z + M11,18H13V16H11V18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,6A4,4 0 0,0 8,10H10A2,2 0 0,1 12,8A2,2 0 0,1 14,10C14,12 11,11.75 11,15H13C13,12.75 16,12.5 16,10A4,4 0 0,0 12,6Z + M 6.6826173,6 C 4.9854743,6 3.5796709,6.547712 2.4482423,7.6191406 1.3510994,8.6819978 0.7762556,10.199688 0.7333985,12.179688 c 0,1.671428 0.4794531,3.052052 1.4394531,4.140624 0.9514285,1.08 2.3752455,1.645403 4.2695313,1.679688 0.66,0 1.2340849,-0.06045 1.7226562,-0.171875 0.48,-0.102857 0.839644,-0.213984 1.0625,-0.333984 L 8.8759766,15.402344 c -0.2657142,0.111428 -0.5747879,0.20682 -0.9433593,0.27539 L 6.7939454,15.779297 C 5.6968026,15.770725 4.8400169,15.429531 4.2314454,14.769531 3.6228741,14.100959 3.3056641,13.191106 3.3056641,12.025391 3.3313784,10.748248 3.664001,9.7878237 4.3154297,9.1621094 4.9582868,8.5278237 5.7820816,8.2121331 6.7763673,8.2207031 c 0.4542855,0 0.848549,0.041239 1.1914062,0.1269531 L 8.8759766,8.640625 9.3740235,6.5058594 C 9.1511665,6.377288 8.8075841,6.2649666 8.3447266,6.1621094 7.8818696,6.0592523 7.3254743,6 6.6826173,6 Z M 11.143555,9.0513005 V 11.232941 H 8.9384766 v 2.095703 h 2.2050784 v 2.232422 h 2.072266 v -2.207031 h 2.257811 V 11.232941 H 13.215821 V 9.0513005 Z m 7.792968,0 v 2.1816405 h -2.205078 v 2.095703 h 2.205078 v 2.232422 h 2.072266 v -2.207031 h 2.257812 V 11.232941 H 21.008789 V 9.0513005 Z + M2,15A1,1 0 0,1 3,16A1,1 0 0,1 2,17A1,1 0 0,1 1,16A1,1 0 0,1 2,15M21,17H19V9H17V7H23V9H21V17M16,7V9H14V11H16V13H14V15H16V17H12V7H16M11,7V17H9L6,11V17H4V7H6L9,13V7H11Z + + M17,18C17.56,18 18,18.44 18,19C18,19.56 17.56,20 17,20C16.44,20 16,19.56 16,19C16,18.44 16.44,18 17,18M17,15C14.27,15 11.94,16.66 11,19C11.94,21.34 14.27,23 17,23C19.73,23 22.06,21.34 23,19C22.06,16.66 19.73,15 17,15M17,21.5A2.5,2.5 0 0,1 14.5,19A2.5,2.5 0 0,1 17,16.5A2.5,2.5 0 0,1 19.5,19A2.5,2.5 0 0,1 17,21.5M9.27,20H6V4H13V9H18V13.07C18.7,13.15 19.36,13.32 20,13.56V8L14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H10.5C10,21.41 9.59,20.73 9.27,20Z + M17 3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H19C20.1 21 21 20.1 21 19V7L17 3M19 19H5V5H16.17L19 7.83V19M12 12C10.34 12 9 13.34 9 15S10.34 18 12 18 15 16.66 15 15 13.66 12 12 12M6 6H15V10H6V6Z + + M19.5 17C19.36 17 19.24 17 19.11 17.04L17.5 13.8C17.95 13.35 18.25 12.71 18.25 12C18.25 10.62 17.13 9.5 15.75 9.5C15.61 9.5 15.5 9.5 15.35 9.54L13.74 6.3C14.21 5.84 14.5 5.21 14.5 4.5C14.5 3.12 13.38 2 12 2S9.5 3.12 9.5 4.5C9.5 5.2 9.79 5.84 10.26 6.29L8.65 9.54C8.5 9.5 8.39 9.5 8.25 9.5C6.87 9.5 5.75 10.62 5.75 12C5.75 12.71 6.04 13.34 6.5 13.79L4.89 17.04C4.76 17 4.64 17 4.5 17C3.12 17 2 18.12 2 19.5C2 20.88 3.12 22 4.5 22S7 20.88 7 19.5C7 18.8 6.71 18.16 6.24 17.71L7.86 14.46C8 14.5 8.12 14.5 8.25 14.5C8.38 14.5 8.5 14.5 8.63 14.46L10.26 17.71C9.79 18.16 9.5 18.8 9.5 19.5C9.5 20.88 10.62 22 12 22S14.5 20.88 14.5 19.5C14.5 18.12 13.38 17 12 17C11.87 17 11.74 17 11.61 17.04L10 13.8C10.45 13.35 10.75 12.71 10.75 12C10.75 11.3 10.46 10.67 10 10.21L11.61 6.96C11.74 7 11.87 7 12 7C12.13 7 12.26 7 12.39 6.96L14 10.21C13.54 10.66 13.25 11.3 13.25 12C13.25 13.38 14.37 14.5 15.75 14.5C15.88 14.5 16 14.5 16.13 14.46L17.76 17.71C17.29 18.16 17 18.8 17 19.5C17 20.88 18.12 22 19.5 22S22 20.88 22 19.5C22 18.12 20.88 17 19.5 17M4.5 20.5C3.95 20.5 3.5 20.05 3.5 19.5S3.95 18.5 4.5 18.5 5.5 18.95 5.5 19.5 5.05 20.5 4.5 20.5M13 19.5C13 20.05 12.55 20.5 12 20.5S11 20.05 11 19.5 11.45 18.5 12 18.5 13 18.95 13 19.5M7.25 12C7.25 11.45 7.7 11 8.25 11S9.25 11.45 9.25 12 8.8 13 8.25 13 7.25 12.55 7.25 12M11 4.5C11 3.95 11.45 3.5 12 3.5S13 3.95 13 4.5 12.55 5.5 12 5.5 11 5.05 11 4.5M14.75 12C14.75 11.45 15.2 11 15.75 11S16.75 11.45 16.75 12 16.3 13 15.75 13 14.75 12.55 14.75 12M19.5 20.5C18.95 20.5 18.5 20.05 18.5 19.5S18.95 18.5 19.5 18.5 20.5 18.95 20.5 19.5 20.05 20.5 19.5 20.5Z + m 17,4.5 c 0,0.14 0,0.26 0.04,0.39 L 13.8,6.5 C 13.35,6.05 12.71,5.75 12,5.75 c -1.38,0 -2.5,1.12 -2.5,2.5 0,0.14 0,0.25 0.04,0.4 L 6.3,10.26 C 5.84,9.79 5.21,9.5 4.5,9.5 3.12,9.5 2,10.62 2,12 c 0,1.38 1.12,2.5 2.5,2.5 0.7,0 1.34,-0.29 1.79,-0.76 l 3.25,1.61 C 9.5,15.5 9.5,15.61 9.5,15.75 c 0,1.38 1.12,2.5 2.5,2.5 0.71,0 1.34,-0.29 1.79,-0.75 l 3.25,1.61 C 17,19.24 17,19.36 17,19.5 17,20.88 18.12,22 19.5,22 20.88,22 22,20.88 22,19.5 22,18.12 20.88,17 19.5,17 c -0.7,0 -1.34,0.29 -1.79,0.76 L 14.46,16.14 C 14.5,16 14.5,15.88 14.5,15.75 c 0,-0.13 0,-0.25 -0.04,-0.38 l 3.25,-1.63 c 0.45,0.47 1.09,0.76 1.79,0.76 1.38,0 2.5,-1.12 2.5,-2.5 0,-1.38 -1.12,-2.5 -2.5,-2.5 -1.38,0 -2.5,1.12 -2.5,2.5 0,0.13 0,0.26 0.04,0.39 L 13.8,14 c -0.45,-0.45 -1.09,-0.75 -1.8,-0.75 -0.7,0 -1.33,0.29 -1.79,0.75 L 6.96,12.39 C 7,12.26 7,12.13 7,12 7,11.87 7,11.74 6.96,11.61 L 10.21,10 c 0.45,0.46 1.09,0.75 1.79,0.75 1.38,0 2.5,-1.12 2.5,-2.5 0,-0.13 0,-0.25 -0.04,-0.38 L 17.71,6.24 C 18.16,6.71 18.8,7 19.5,7 20.88,7 22,5.88 22,4.5 22,3.12 20.88,2 19.5,2 18.12,2 17,3.12 17,4.5 m 3.5,15 c 0,0.55 -0.45,1 -1,1 -0.55,0 -1,-0.45 -1,-1 0,-0.55 0.45,-1 1,-1 0.55,0 1,0.45 1,1 m -1,-8.5 c 0.55,0 1,0.45 1,1 0,0.55 -0.45,1 -1,1 -0.55,0 -1,-0.45 -1,-1 0,-0.55 0.45,-1 1,-1 M 12,16.75 c -0.55,0 -1,-0.45 -1,-1 0,-0.55 0.45,-1 1,-1 0.55,0 1,0.45 1,1 0,0.55 -0.45,1 -1,1 M 4.5,13 c -0.55,0 -1,-0.45 -1,-1 0,-0.55 0.45,-1 1,-1 0.55,0 1,0.45 1,1 0,0.55 -0.45,1 -1,1 M 12,9.25 c -0.55,0 -1,-0.45 -1,-1 0,-0.55 0.45,-1 1,-1 0.55,0 1,0.45 1,1 0,0.55 -0.45,1 -1,1 M 20.5,4.5 c 0,0.55 -0.45,1 -1,1 -0.55,0 -1,-0.45 -1,-1 0,-0.55 0.45,-1 1,-1 0.55,0 1,0.45 1,1 z + + M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z + M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z + + M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10M10,22C9.75,22 9.54,21.82 9.5,21.58L9.13,18.93C8.5,18.68 7.96,18.34 7.44,17.94L4.95,18.95C4.73,19.03 4.46,18.95 4.34,18.73L2.34,15.27C2.21,15.05 2.27,14.78 2.46,14.63L4.57,12.97L4.5,12L4.57,11L2.46,9.37C2.27,9.22 2.21,8.95 2.34,8.73L4.34,5.27C4.46,5.05 4.73,4.96 4.95,5.05L7.44,6.05C7.96,5.66 8.5,5.32 9.13,5.07L9.5,2.42C9.54,2.18 9.75,2 10,2H14C14.25,2 14.46,2.18 14.5,2.42L14.87,5.07C15.5,5.32 16.04,5.66 16.56,6.05L19.05,5.05C19.27,4.96 19.54,5.05 19.66,5.27L21.66,8.73C21.79,8.95 21.73,9.22 21.54,9.37L19.43,11L19.5,12L19.43,13L21.54,14.63C21.73,14.78 21.79,15.05 21.66,15.27L19.66,18.73C19.54,18.95 19.27,19.04 19.05,18.95L16.56,17.95C16.04,18.34 15.5,18.68 14.87,18.93L14.5,21.58C14.46,21.82 14.25,22 14,22H10M11.25,4L10.88,6.61C9.68,6.86 8.62,7.5 7.85,8.39L5.44,7.35L4.69,8.65L6.8,10.2C6.4,11.37 6.4,12.64 6.8,13.8L4.68,15.36L5.43,16.66L7.86,15.62C8.63,16.5 9.68,17.14 10.87,17.38L11.24,20H12.76L13.13,17.39C14.32,17.14 15.37,16.5 16.14,15.62L18.57,16.66L19.32,15.36L17.2,13.81C17.6,12.64 17.6,11.37 17.2,10.2L19.31,8.65L18.56,7.35L16.15,8.39C15.38,7.5 14.32,6.86 13.12,6.62L12.75,4H11.25Z + + M13,9V3.5L18.5,9M6,2C4.89,2 4,2.89 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6Z + M 8.4915254,2.3389831 V 5 H 7 C 5.8954305,5 5,5.8954305 5,7 V 8.4915254 H 2.3389831 V 11 H 5 v 2 H 2.3389831 v 2.508475 H 5 V 17 c 0,0 0.8954305,2 2,2 h 1.4915254 v 2.661017 H 11 V 19 h 2 v 2.661017 h 2.508475 V 19 H 17 c 1.104569,0 2,-0.895431 2,-2 v -1.491525 h 2.661017 V 13 H 19 v -2 h 2.661017 V 8.4915254 H 19 V 7 C 19,5.8954305 18.104569,5 17,5 H 15.508475 V 2.3389831 H 13 V 5 H 11 V 2.3389831 + M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5M12,2A7,7 0 0,0 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9A7,7 0 0,0 12,2Z + M11,18H13V16H11V18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,6A4,4 0 0,0 8,10H10A2,2 0 0,1 12,8A2,2 0 0,1 14,10C14,12 11,11.75 11,15H13C13,12.75 16,12.5 16,10A4,4 0 0,0 12,6Z + M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3M11 8H9V10C9 11.1 8.1 12 7 12C8.1 12 9 12.9 9 14V16H11V18H9C7.9 18 7 17.1 7 16V15C7 13.9 6.1 13 5 13V11C6.1 11 7 10.1 7 9V8C7 6.9 7.9 6 9 6H11V8M19 13C17.9 13 17 13.9 17 15V16C17 17.1 16.1 18 15 18H13V16H15V14C15 12.9 15.9 12 17 12C15.9 12 15 11.1 15 10V8H13V6H15C16.1 6 17 6.9 17 8V9C17 10.1 17.9 11 19 11V13Z + + M 9 0 L 9 1 L 8 1 L 8 2 L 9 2 L 9 4 L 8 4 L 8 5 L 9 5 L 9 6 L 10 6 L 10 5 L 12 5 L 12 6 L 13 6 L 13 5 L 14 5 L 14 4 L 13 4 L 13 2 L 14 2 L 14 1 L 13 1 L 13 0 L 12 0 L 12 1 L 10 1 L 10 0 L 9 0 z M 4.3613281 0.025390625 C 4.0149781 0.02004021 3.6411452 0.038301173 3.2617188 0.095703125 C 2.5028657 0.21050704 1.6813227 0.49403876 1.0371094 1.1328125 C 0.39289602 1.7715862 -0.0078125 2.7468091 -0.0078125 4 C -0.0078125 5.2531909 0.3928994 6.2284076 1.0371094 6.8671875 C 1.6813193 7.5059674 2.5028664 7.7894818 3.2617188 7.9042969 C 4.7794234 8.1339269 6.1972656 7.7324219 6.1972656 7.7324219 L 5.8027344 6.2675781 C 5.8027344 6.2675781 4.5955767 6.5718317 3.4882812 6.4042969 C 2.9346336 6.3205295 2.4436807 6.1283134 2.1035156 5.7910156 C 1.7633506 5.4537178 1.5078125 4.9525183 1.5078125 4 C 1.5078125 3.0474818 1.763354 2.5462738 2.1035156 2.2089844 C 2.4436773 1.8716949 2.9346343 1.6794622 3.4882812 1.5957031 C 4.5955754 1.4281849 5.8027344 1.7324219 5.8027344 1.7324219 L 6.1972656 0.26757812 C 6.1972656 0.26757812 5.4003781 0.04144187 4.3613281 0.025390625 z M 10 2 L 12 2 L 12 4 L 10 4 L 10 2 z + M 1.578125 -0.02734375 L 0.421875 0.44921875 L 3.421875 7.7382812 A 0.6250625 0.6250625 0 0 0 4.578125 7.7382812 L 7.578125 0.44921875 L 6.421875 -0.02734375 L 4 5.8574219 L 1.578125 -0.02734375 z M 10.076172 -0.013671875 C 9.4806262 0.015462465 9.0058594 0.009765625 9.0058594 0.009765625 L 8.375 0.00390625 L 8.375 3.7089844 L 8.375 4.1972656 L 8.375 8 L 9.0136719 7.9863281 C 9.0136719 7.9863281 9.6116269 7.9724126 10.339844 7.9941406 C 10.970698 8.0129596 11.708633 7.9738636 12.376953 7.6660156 C 13.045273 7.3581707 13.625 6.6496723 13.625 5.7109375 C 13.625 4.7900191 13.054235 4.1206047 12.402344 3.7988281 C 12.807287 3.4278966 13.125 2.8963096 13.125 2.2109375 C 13.125 1.2739719 12.589649 0.56325226 11.953125 0.265625 C 11.316601 -0.03200226 10.634394 -0.040980275 10.076172 -0.013671875 z M 10.138672 1.234375 C 10.60584 1.211521 11.12041 1.256565 11.423828 1.3984375 C 11.727246 1.5403104 11.875 1.6557413 11.875 2.2109375 C 11.875 2.6550945 11.758941 2.8133591 11.572266 2.9570312 C 11.38559 3.1007035 11.062567 3.2002851 10.695312 3.2324219 C 10.185172 3.2770622 9.9105404 3.1971776 9.625 3.1445312 L 9.625 1.2460938 C 9.8247124 1.2430438 9.8537112 1.2483138 10.138672 1.234375 z M 10.292969 4.6816406 C 10.82941 4.6586786 11.440596 4.7122577 11.824219 4.8789062 C 12.207841 5.0455547 12.375 5.1830105 12.375 5.7109375 C 12.375 6.2249031 12.215588 6.3625161 11.853516 6.5292969 C 11.491444 6.6960776 10.90772 6.7599154 10.378906 6.7441406 C 9.9651949 6.7317953 9.8808621 6.7389659 9.625 6.7402344 L 9.625 4.7617188 C 9.8361655 4.7271637 9.9215738 4.6975388 10.292969 4.6816406 z + M 4.3613281 0.025390625 C 4.0149781 0.020040195 3.6411452 0.038301175 3.2617188 0.095703125 C 2.5028655 0.21050705 1.6813227 0.49403876 1.0371094 1.1328125 C 0.39289596 1.7715862 -0.0078125 2.7468091 -0.0078125 4 C -0.0078125 5.2531909 0.39289936 6.2284076 1.0371094 6.8671875 C 1.6813193 7.5059674 2.5028664 7.7894818 3.2617188 7.9042969 C 4.7794234 8.1339269 6.1972656 7.7324219 6.1972656 7.7324219 L 5.8027344 6.2675781 C 5.8027344 6.2675781 4.5955767 6.5718317 3.4882812 6.4042969 C 2.9346336 6.3205295 2.4436807 6.1283134 2.1035156 5.7910156 C 1.7633506 5.4537178 1.5078125 4.9525183 1.5078125 4 C 1.5078125 3.0474818 1.763354 2.5462738 2.1035156 2.2089844 C 2.4436773 1.8716949 2.9346343 1.6794622 3.4882812 1.5957031 C 4.5955755 1.4281849 5.8027344 1.7324219 5.8027344 1.7324219 L 6.1972656 0.26757812 C 6.1972656 0.26757812 5.4003781 0.041441865 4.3613281 0.025390625 z M 7 2.5 L 7 4 L 5.5 4 L 5.5 5 L 7 5 L 7 6.5 L 8 6.5 L 8 5 L 9.5 5 L 9.5 4 L 8 4 L 8 2.5 L 7 2.5 z M 12 2.5 L 12 4 L 10.5 4 L 10.5 5 L 12 5 L 12 6.5 L 13 6.5 L 13 5 L 14.5 5 L 14.5 4 L 13 4 L 13 2.5 L 12 2.5 z + m 0,0 v 1.5 2 1 V 8 H 1.5 V 4.5 h 3 v -1 h -3 v -2 h 3 V 0 h -3 z M 7,0 V 1 H 6 V 2 H 7 V 4 H 6 V 5 H 7 V 6 H 8 V 5 h 2 v 1 h 1 V 5 h 1 V 4 H 11 V 2 h 1 V 1 H 11 V 0 H 10 V 1 H 8 V 0 Z m 1,2 h 2 V 4 H 8 Z + M 2.96875,0.00816248 C 2.6358475,0.02485997 2.3746148,0.29984171 2.375,0.63316248 V 8.026 h 1.25 V 2.8558188 L 6.6338079,7.7093344 C 6.9517736,8.2666112 7.8025776,8.040394 7.8017767,7.3987875 V -2.2888184e-8 h -1.25 V 5.1448812 L 3.5429688,0.3226156 C 3.4261508,0.11792601 3.2041318,-0.00365577 2.96875,0.00816248 Z M 8.875,-2.2888184e-8 V 8.026 L 13.5,8.0257406 v -1.25 H 10.125 V 4.62535 H 13 v -1.25 h -2.875 v -2.125 H 13 V 3.4997711e-4 H 10.125 9.5 Z M 14,0.0257406 v 1.25 h 1.875 V 8.00035 h 1.25 V 1.2757406 H 19 v -1.25 z M 0,6.52465 v 1.5 h 1.5 v -1.5 z + + M 6.5,0 C 2.9101491,0 0,2.9101491 0,6.5 0,10.089851 2.9101491,13 6.5,13 10.089851,13 13,10.089851 13,6.5 13,2.9101491 10.089851,0 6.5,0 Z M 9.4931641,3.7929688 10.200195,4.5 5.2001955,9.5 2.7998049,7.1621094 3.5068361,6.4550781 5.2001955,8.0859375 Z + M 6.5 0 A 6.5 6.5 0 0 0 0 6.5 A 6.5 6.5 0 0 0 6.5 13 A 6.5 6.5 0 0 0 13 6.5 A 6.5 6.5 0 0 0 6.5 0 z M 6 3 L 7 3 L 7 4 L 6 4 L 6 3 z M 6 5 L 7 5 L 7 10 L 6 10 L 6 5 z + M 6.5 0 L 0 13 L 13 13 L 6.5 0 z M 6 4 L 7 4 L 7 8.5 L 6 8.5 L 6 4 z M 6 10 L 7 10 L 7 11 L 6 11 L 6 10 z + M 6.5 0 A 6.5 6.5 0 0 0 0 6.5 A 6.5 6.5 0 0 0 6.5 13 A 6.5 6.5 0 0 0 13 6.5 A 6.5 6.5 0 0 0 6.5 0 z M 4.3535156 3.6464844 L 6.5 5.7929688 L 8.6464844 3.6464844 L 9.3535156 4.3535156 L 7.2070312 6.5 L 9.3535156 8.6464844 L 8.6464844 9.3535156 L 6.5 7.2070312 L 4.3535156 9.3535156 L 3.6464844 8.6464844 L 5.7929688 6.5 L 3.6464844 4.3535156 L 4.3535156 3.6464844 z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/RefScout.Wpf/Styles/Misc.xaml b/src/RefScout.Wpf/Styles/Misc.xaml new file mode 100644 index 0000000..f00fd48 --- /dev/null +++ b/src/RefScout.Wpf/Styles/Misc.xaml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/RefScout.Wpf/Styles/ScrollBar.xaml b/src/RefScout.Wpf/Styles/ScrollBar.xaml new file mode 100644 index 0000000..47a4388 --- /dev/null +++ b/src/RefScout.Wpf/Styles/ScrollBar.xaml @@ -0,0 +1,117 @@ + + + + + + + \ No newline at end of file diff --git a/src/RefScout.Wpf/Styles/TabControl.xaml b/src/RefScout.Wpf/Styles/TabControl.xaml new file mode 100644 index 0000000..4f73ef0 --- /dev/null +++ b/src/RefScout.Wpf/Styles/TabControl.xaml @@ -0,0 +1,286 @@ + + + + + + \ No newline at end of file diff --git a/src/RefScout.Wpf/Styles/TreeView.xaml b/src/RefScout.Wpf/Styles/TreeView.xaml new file mode 100644 index 0000000..a5b4e92 --- /dev/null +++ b/src/RefScout.Wpf/Styles/TreeView.xaml @@ -0,0 +1,164 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/RefScout.Wpf/Styles/Window.xaml b/src/RefScout.Wpf/Styles/Window.xaml new file mode 100644 index 0000000..de44d8e --- /dev/null +++ b/src/RefScout.Wpf/Styles/Window.xaml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/src/RefScout.Wpf/Themes/Dark.xaml b/src/RefScout.Wpf/Themes/Dark.xaml new file mode 100644 index 0000000..aa0e68f --- /dev/null +++ b/src/RefScout.Wpf/Themes/Dark.xaml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/src/RefScout.Wpf/Themes/Generic.xaml b/src/RefScout.Wpf/Themes/Generic.xaml new file mode 100644 index 0000000..42f1cd1 --- /dev/null +++ b/src/RefScout.Wpf/Themes/Generic.xaml @@ -0,0 +1,3 @@ + + \ No newline at end of file diff --git a/src/RefScout.Wpf/Themes/Light.xaml b/src/RefScout.Wpf/Themes/Light.xaml new file mode 100644 index 0000000..861bb9f --- /dev/null +++ b/src/RefScout.Wpf/Themes/Light.xaml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/src/RefScout.Wpf/ViewModels/AssemblyListTabViewModel.cs b/src/RefScout.Wpf/ViewModels/AssemblyListTabViewModel.cs new file mode 100644 index 0000000..25521f3 --- /dev/null +++ b/src/RefScout.Wpf/ViewModels/AssemblyListTabViewModel.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.Toolkit.Mvvm.Input; +using RefScout.Analyzer; +using RefScout.Analyzer.Filter; +using RefScout.Wpf.Services; + +namespace RefScout.Wpf.ViewModels; + +internal class AssemblyListTabViewModel : ObservableObject +{ + private readonly IContextService _context; + private IReadOnlyList _assemblies; + + private IAnalyzerResult? _analyzerResult; + private string _searchFilter = string.Empty; + private bool _showReferencedBy; + + public AssemblyListTabViewModel(IContextService context, ISettingsService settings) + { + _context = context; + _assemblies = Array.Empty(); + ViewDetails = new RelayCommand(DoViewDetails); + SearchAssemblies = new RelayCommand(DoSearchAssemblies); + + _showReferencedBy = settings.Settings.ShowReferencedBy; + settings.Settings.PropertyChanged += (_, args) => + { + if (args.PropertyName == nameof(settings.Settings.ShowReferencedBy)) + { + ShowReferencedBy = settings.Settings.ShowReferencedBy; + } + }; + } + + public RelayCommand ViewDetails { get; } + public RelayCommand SearchAssemblies { get; } + + public IAnalyzerResult? AnalyzerResult + { + get => _analyzerResult; + set => SetProperty(ref _analyzerResult, value); + } + + public IReadOnlyList Assemblies + { + get => _assemblies; + set => SetProperty(ref _assemblies, value); + } + + public string SearchFilter + { + get => _searchFilter; + set => SetProperty(ref _searchFilter, value); + } + + public bool ShowReferencedBy + { + get => _showReferencedBy; + set => SetProperty(ref _showReferencedBy, value); + } + + + public void OnNewAnalyzerResult(IAnalyzerResult? analyzerResult) + { + AnalyzerResult = analyzerResult; + SearchFilter = string.Empty; + Assemblies = AnalyzerResult?.Assemblies ?? Array.Empty(); + } + + private void DoViewDetails(Assembly? assembly) + { + if (assembly != null) + { + _context.ShowDetailsWindow(assembly); + } + } + + private void DoSearchAssemblies(string? filter) + { + if (AnalyzerResult == null) + { + return; + } + + if (string.IsNullOrWhiteSpace(filter)) + { + Assemblies = AnalyzerResult.Assemblies; + return; + } + + Assemblies = AnalyzerResult.Assemblies + .Where(FilterParser.Parse(filter)).ToList(); + } +} \ No newline at end of file diff --git a/src/RefScout.Wpf/ViewModels/DetailsWindowViewModel.cs b/src/RefScout.Wpf/ViewModels/DetailsWindowViewModel.cs new file mode 100644 index 0000000..dcad3a5 --- /dev/null +++ b/src/RefScout.Wpf/ViewModels/DetailsWindowViewModel.cs @@ -0,0 +1,30 @@ +using Microsoft.Toolkit.Mvvm.ComponentModel; +using RefScout.Analyzer; +using RefScout.Wpf.Services; + +namespace RefScout.Wpf.ViewModels; + +internal class DetailsWindowViewModel : ObservableObject +{ + private Assembly _assembly; + private bool _wasFound; + + public DetailsWindowViewModel(IContextService context) + { + _assembly = context.ActiveAssembly ?? + new Assembly(new AssemblyIdentity("Loading"), null, AssemblySource.NotFound); + _wasFound = Assembly.Source is not (AssemblySource.Error or AssemblySource.NotFound); + } + + public Assembly Assembly + { + get => _assembly; + set => SetProperty(ref _assembly, value); + } + + public bool WasFound + { + get => _wasFound; + set => SetProperty(ref _wasFound, value); + } +} \ No newline at end of file diff --git a/src/RefScout.Wpf/ViewModels/EnvironmentTabViewModel.cs b/src/RefScout.Wpf/ViewModels/EnvironmentTabViewModel.cs new file mode 100644 index 0000000..8d010ee --- /dev/null +++ b/src/RefScout.Wpf/ViewModels/EnvironmentTabViewModel.cs @@ -0,0 +1,128 @@ +using System.Linq; +using System.Text; +using Microsoft.Toolkit.Mvvm.ComponentModel; +using RefScout.Analyzer; +using RefScout.Analyzer.Analyzers.Environment; +using RefScout.Analyzer.Context; + +namespace RefScout.Wpf.ViewModels; + +internal class EnvironmentTabViewModel : ObservableObject +{ + private IAnalyzerResult? _analyzerResult; + private string? _configErrorMessage; + private EnvironmentInfo? _environment; + + private string? _coreEnvironmentDetails; + private string? _frameworkEnvironmentDetails; + + public IAnalyzerResult? AnalyzerResult + { + get => _analyzerResult; + set => SetProperty(ref _analyzerResult, value); + } + + public EnvironmentInfo? Environment + { + get => _environment; + set => SetProperty(ref _environment, value); + } + + public string? ConfigErrorMessage + { + get => _configErrorMessage; + set => SetProperty(ref _configErrorMessage, value); + } + + public string? CoreEnvironmentDetails + { + get => _coreEnvironmentDetails; + set => SetProperty(ref _coreEnvironmentDetails, value); + } + + public string? FrameworkEnvironmentDetails + { + get => _frameworkEnvironmentDetails; + set => SetProperty(ref _frameworkEnvironmentDetails, value); + } + + public void OnNewAnalyzerResult(IAnalyzerResult? analyzerResult) + { + AnalyzerResult = analyzerResult; + Environment = analyzerResult?.EnvironmentInfo; + + CoreEnvironmentDetails = BuildCoreEnvironmentDetails(analyzerResult); + FrameworkEnvironmentDetails = BuildFrameworkEnvironmentDetails(analyzerResult); + + if (analyzerResult is FrameworkAnalyzerResult frameworkResult && + frameworkResult.Config.ErrorReport.HasErrors) + { + ConfigErrorMessage = frameworkResult.Config.ErrorReport.ToString(); + } + else + { + ConfigErrorMessage = null; + } + } + + private static string BuildCoreEnvironmentDetails(IAnalyzerResult? result) + { + if (result is null) + { + return "Unknown"; + } + + var sb = new StringBuilder(); + if (result is CoreAnalyzerResult coreAnalyzerResult) + { + sb.Append(" Used runtime version: "); + if (coreAnalyzerResult.Config.SelfContained) + { + sb.AppendLine("self-contained application"); + } + else if (coreAnalyzerResult.Runtime != null) + { + sb.Append(coreAnalyzerResult.Runtime.VersionName); + sb.AppendLine($" (policy: {coreAnalyzerResult.Config.RollForward})"); + } + else + { + sb.AppendLine("Could not find appropriate runtime version."); + } + } + + if (result.EnvironmentInfo.Core != null) + { + var x64Runtimes = result.EnvironmentInfo.Core.Runtimes.Where(x => x.Is64Bit).ToList(); + var x86Runtimes = result.EnvironmentInfo.Core.Runtimes.Where(x => !x.Is64Bit).ToList(); + sb.Append(" Installed versions: "); + sb.AppendLine(x64Runtimes.Count > 0 + ? string.Join(", ", x64Runtimes.Select(s => s.VersionName)) + : "None installed."); + + sb.Append(" Installed versions (x86): "); + sb.AppendLine(x86Runtimes.Count > 0 + ? string.Join(", ", x86Runtimes.Select(s => s.VersionName)) + : "None installed."); + } + + return sb.ToString(); + } + + private static string BuildFrameworkEnvironmentDetails(IAnalyzerResult? result) + { + if (result?.EnvironmentInfo.Framework == null) + { + return "Unknown"; + } + + + var sb = new StringBuilder(); + foreach (var (framework, version, servicePack) in result.EnvironmentInfo.Framework.Runtimes) + { + sb.AppendLine($" {framework}: {version} {(servicePack > 0 ? $"(service pack {servicePack})" : "")}"); + } + + return sb.ToString(); + } +} \ No newline at end of file diff --git a/src/RefScout.Wpf/ViewModels/GraphContainerViewModel.cs b/src/RefScout.Wpf/ViewModels/GraphContainerViewModel.cs new file mode 100644 index 0000000..eb8fb9e --- /dev/null +++ b/src/RefScout.Wpf/ViewModels/GraphContainerViewModel.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.Toolkit.Mvvm.Input; +using Microsoft.Win32; +using RefScout.Analyzer; +using RefScout.Core.Logging; +using RefScout.Visualizers; +using RefScout.Visualizers.Dot; +using RefScout.Wpf.Helpers; +using RefScout.Wpf.Models; +using RefScout.Wpf.Services; +using SharpVectors.Converters; +using SharpVectors.Renderers.Wpf; + +namespace RefScout.Wpf.ViewModels; + +internal class GraphContainerViewModel : ObservableObject +{ + private const string DownloadUrl = "https://graphviz.org/download/#windows"; + + private readonly IContextService _context; + private readonly ISettingsService _settings; + + private IAnalyzerResult? _analyzerResult; + private string? _errorMessage; + private bool _graphVizNotInstalled; + private string? _loadingMessage; + private string? _vectorPath; + + public GraphContainerViewModel(ISettingsService settings, IContextService context) + { + _settings = settings; + _context = context; + + GraphVizNotInstalled = !GraphVizHelper.IsGraphVizInstalled(); + DownloadGraphViz = new RelayCommand(DoDownloadGraphViz); + InstallForMe = new AsyncRelayCommand(DoInstallForMeAsync); + TryAgain = new AsyncRelayCommand(DoTryAgainAsync); + ExportGraph = new RelayCommand(DoExportGraph); + ToggleGraphDirection = new AsyncRelayCommand(DoToggleGraphDirectionAsync); + ToggleShowTargetFramework = new AsyncRelayCommand(DoToggleShowDotNetFrameworkAsync); + VisualizeModeChanged = new AsyncRelayCommand(DoVisualizeModeChangedAsync); + + VisualizeModes = new List> + { + new(VisualizeMode.All, "All assemblies"), + new(VisualizeMode.Conflicts, "Only conflicts"), + new(VisualizeMode.App, "Application assemblies") + }; + } + + public AppSettings Settings => _settings.Settings; + + public RelayCommand DownloadGraphViz { get; } + public AsyncRelayCommand InstallForMe { get; } + public AsyncRelayCommand TryAgain { get; } + public RelayCommand ExportGraph { get; } + public AsyncRelayCommand ToggleGraphDirection { get; } + public AsyncRelayCommand ToggleShowTargetFramework { get; } + public AsyncRelayCommand VisualizeModeChanged { get; } + + public IAnalyzerResult? AnalyzerResult + { + get => _analyzerResult; + set => SetProperty(ref _analyzerResult, value); + } + + public string? LoadingMessage + { + get => _loadingMessage; + set => SetProperty(ref _loadingMessage, value); + } + + public string? ErrorMessage + { + get => _errorMessage; + set => SetProperty(ref _errorMessage, value); + } + + public bool GraphVizNotInstalled + { + get => _graphVizNotInstalled; + set => SetProperty(ref _graphVizNotInstalled, value); + } + + public string? VectorPath + { + get => _vectorPath; + set => SetProperty(ref _vectorPath, value); + } + + public List> VisualizeModes { get; } + + public ComboBoxEntry SelectedVisualizeMode + { + get => VisualizeModes.Single(v => v.Value == _settings.Settings.VisualizeMode); + set => _settings.Settings.VisualizeMode = value.Value; + } + + private void DoExportGraph() + { + if (VectorPath == null) + { + return; + } + + var saveFileDialog = new SaveFileDialog + { + Title = "Export graph as image", + Filter = "PNG Image|*.png|SVG Image|*.svg" + }; + + if (saveFileDialog.ShowDialog() == false) + { + return; + } + + switch (Path.GetExtension(saveFileDialog.FileName)) + { + case ".png": + // TODO: this still doesn't work, waiting for SharpVectors fix... + var settings = new WpfDrawingSettings + { + IgnoreRootViewbox = false, + PixelWidth = 936, + PixelHeight = 2760 + }; + + var converter = new ImageSvgConverterEx(settings) + { + EncoderType = ImageEncoderType.PngBitmap + }; + converter.Convert(VectorPath, saveFileDialog.FileName); + break; + case ".svg": + File.Copy(VectorPath, saveFileDialog.FileName, true); + break; + } + + MessageBox.Show("Successfully exported graph to image"); + //_toast.ShowToast(ToastType.Success, "Successfully exported graph to image"); + } + + private async Task DoTryAgainAsync() + { + GraphVizNotInstalled = !GraphVizHelper.IsGraphVizInstalled(); + await RenderAnalyzerResultAsync(); + } + + private static void DoDownloadGraphViz() + { + ProcessHelper.LaunchBrowser(DownloadUrl); + } + + private async Task DoInstallForMeAsync() + { + try + { + await GraphVizHelper.DownloadAndInstallGraphViz(new Progress(message => + LoadingMessage = $"Installing GraphViz: {message}")); + } + catch (Exception e) + { + Logger.Error(e, "Failed to automatically install GraphViz"); + GraphVizNotInstalled = true; + LoadingMessage = null; + return; + } + + await DoTryAgainAsync(); + } + + private async Task DoToggleGraphDirectionAsync() + { + Settings.GraphTopBottom = !Settings.GraphTopBottom; + await RenderAnalyzerResultAsync(); + } + + private async Task DoToggleShowDotNetFrameworkAsync() + { + Settings.GraphShowTargetFramework = !Settings.GraphShowTargetFramework; + await RenderAnalyzerResultAsync(); + } + + private async Task DoVisualizeModeChangedAsync() + { + await RenderAnalyzerResultAsync(); + } + + public async Task RenderAnalyzerResultAsync() + { + if (AnalyzerResult == null) + { + return; + } + + LoadingMessage = "Generating graph"; + + var previousVectorPath = VectorPath; + string? tempGraphPath = null; + try + { + tempGraphPath = Path.Combine(GraphVizHelper.TempGraphVizFolder, Path.GetRandomFileName()); + var visualizer = new DotConflictVisualizer(); + await Task.Run(() => visualizer.Visualize(AnalyzerResult, _settings.Settings.VisualizeMode, + new DotConflictVisualizerOptions(tempGraphPath) + { + ShowTargetFramework = _settings.Settings.GraphShowTargetFramework, + DarkTheme = _settings.Settings.DarkTheme, + Direction = Settings.GraphTopBottom ? GraphDirection.Tb : GraphDirection.Lr + })); + + VectorPath = await GraphVizHelper.ConvertGraphVizToImage(tempGraphPath); + LoadingMessage = "Rendering graph"; + } + catch (Exception e) + { + Logger.Error(e, "Could not visualize analyzer result"); + + if (e is FileNotFoundException) + { + GraphVizNotInstalled = true; + } + else + { + ErrorMessage = e.Message; + } + + LoadingMessage = null; + } + finally + { + CleanUpTemporaryFiles(tempGraphPath, previousVectorPath); + } + } + + public void OpenDetailsWindow(string id) + { + var assembly = AnalyzerResult?.Assemblies.SingleOrDefault(a => a.Id == id); + if (assembly != null) + { + _context.ShowDetailsWindow(assembly); + } + } + + private static void CleanUpTemporaryFiles(string? tempGraphPath, string? previousVectorPath) + { + if (!string.IsNullOrEmpty(tempGraphPath) && File.Exists(tempGraphPath)) + { + File.Delete(tempGraphPath); + } + + if (!string.IsNullOrEmpty(previousVectorPath) && File.Exists(previousVectorPath)) + { + File.Delete(previousVectorPath); + } + } +} \ No newline at end of file diff --git a/src/RefScout.Wpf/ViewModels/LoggingWindowViewModel.cs b/src/RefScout.Wpf/ViewModels/LoggingWindowViewModel.cs new file mode 100644 index 0000000..f698ed5 --- /dev/null +++ b/src/RefScout.Wpf/ViewModels/LoggingWindowViewModel.cs @@ -0,0 +1,27 @@ +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.Toolkit.Mvvm.Input; +using RefScout.Core.Logging; +using RefScout.Wpf.Services; + +namespace RefScout.Wpf.ViewModels; + +internal class LoggingWindowViewModel : ObservableObject +{ + private readonly ILoggingService _logging; + + public LoggingWindowViewModel(ILoggingService logging) + { + _logging = logging; + Save = new AsyncRelayCommand(DoSaveAsync); + } + + public AsyncRelayCommand Save { get; } + public ObservableCollection Entries => _logging.Entries; + + private async Task DoSaveAsync() + { + await _logging.SaveAsync(); + } +} \ No newline at end of file diff --git a/src/RefScout.Wpf/ViewModels/MainWindowViewModel.cs b/src/RefScout.Wpf/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..21e91e2 --- /dev/null +++ b/src/RefScout.Wpf/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.Toolkit.Mvvm.Input; +using Microsoft.Win32; +using RefScout.Analyzer; +using RefScout.Core.Logging; +using RefScout.Wpf.Services; +using RefScout.Wpf.Views; + +namespace RefScout.Wpf.ViewModels; + +internal class MainWindowViewModel : ObservableObject +{ + private readonly ISettingsService _settingsService; + private readonly IServiceProvider _serviceProvider; + private string? _errorMessage; + private bool _isAnalyzing; + private bool _isDragging; + private IAnalyzerResult? _result; + + private bool _showWelcomeScreen; + private IReadOnlyList _treeAssemblies; + + private CancellationTokenSource? _cancellationTokenSource; + private string? _currentPath; + + public MainWindowViewModel(ISettingsService settingsService, IServiceProvider serviceProvider) + { + _settingsService = settingsService; + _serviceProvider = serviceProvider; + + _treeAssemblies = Array.Empty(); + + AnalyzeAssembly = new AsyncRelayCommand(DoAnalyzeAssemblyAsync); + CancelAnalyzeAssembly = new RelayCommand(DoCancelAnalyzeAssembly); + + ShowSettingsDialog = new RelayCommand(DoShowSettingsDialog); + AnalyzeAgain = new AsyncRelayCommand(DoAnalyzeAgain); + + ShowFileChooser = new AsyncRelayCommand(DoShowFileChooser); + ShowWelcomeScreen = true; + } + + public bool ShowWelcomeScreen + { + get => _showWelcomeScreen; + set => SetProperty(ref _showWelcomeScreen, value); + } + + public bool IsDragging + { + get => _isDragging; + set => SetProperty(ref _isDragging, value); + } + + public string? ErrorMessage + { + get => _errorMessage; + set => SetProperty(ref _errorMessage, value); + } + + public bool IsAnalyzing + { + get => _isAnalyzing; + set => SetProperty(ref _isAnalyzing, value); + } + + public IAnalyzerResult? Result + { + get => _result; + set => SetProperty(ref _result, value); + } + + public IReadOnlyList TreeAssemblies + { + get => _treeAssemblies; + private set => SetProperty(ref _treeAssemblies, value); + } + + public AsyncRelayCommand AnalyzeAssembly { get; } + public RelayCommand CancelAnalyzeAssembly { get; } + + public RelayCommand ShowSettingsDialog { get; } + public AsyncRelayCommand AnalyzeAgain { get; } + public AsyncRelayCommand ShowFileChooser { get; } + + private void DoShowSettingsDialog() + { + var settingsDialog = _serviceProvider.GetRequiredService(); + settingsDialog.Owner = Application.Current.MainWindow; + settingsDialog.ShowDialog(); + } + + private async Task DoAnalyzeAgain() + { + await DoAnalyzeAssemblyAsync(_currentPath); + } + + private async Task DoShowFileChooser() + { + var dialog = new OpenFileDialog + { + DefaultExt = ".png", + Filter = "Assembly Files (*.dll, *.exe)|*.dll;*.exe" + }; + + var result = dialog.ShowDialog(); + if (result == true) + { + await DoAnalyzeAssemblyAsync(dialog.FileName).ConfigureAwait(false); + } + } + + private async Task DoAnalyzeAssemblyAsync(string? path) + { + if (path == null) + { + return; + } + + _currentPath = path; + IsAnalyzing = true; + _cancellationTokenSource = new CancellationTokenSource(); + + try + { + Result = await Task.Run(() => ReferenceAnalyzer.Run(path, new AnalyzerOptions + { + AnalyzeMode = _settingsService.Settings.AnalyzeMode, + SystemVersionMode = _settingsService.Settings.SystemVersionMode + }, _cancellationTokenSource.Token)); + + TreeAssemblies = Result.Assemblies.Take(1).ToList(); + + ShowWelcomeScreen = false; + } + catch (Exception e) + { + Logger.Error(e, "Could not analyze assembly"); + ShowWelcomeScreen = true; + ErrorMessage = e.Message; + } + finally + { + IsAnalyzing = false; + } + } + + private void DoCancelAnalyzeAssembly() + { + _cancellationTokenSource?.Cancel(); + } +} \ No newline at end of file diff --git a/src/RefScout.Wpf/ViewModels/SettingsWindowViewModel.cs b/src/RefScout.Wpf/ViewModels/SettingsWindowViewModel.cs new file mode 100644 index 0000000..81c6ed5 --- /dev/null +++ b/src/RefScout.Wpf/ViewModels/SettingsWindowViewModel.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.Toolkit.Mvvm.Input; +using RefScout.Analyzer; +using RefScout.Analyzer.Analyzers.Compatibility; +using RefScout.Wpf.Helpers; +using RefScout.Wpf.Models; +using RefScout.Wpf.Services; + +namespace RefScout.Wpf.ViewModels; + +internal class SettingsWindowViewModel : ObservableObject +{ + private readonly ISettingsService _settings; + private readonly ILoggingService _logging; + + public SettingsWindowViewModel(ISettingsService settings, ILoggingService logging) + { + _settings = settings; + _logging = logging; + + AnalyzerModes = new List> + { + new(AnalyzeMode.AppDirectSystem, "Application and direct system assemblies"), + new(AnalyzeMode.App, "Application assemblies only"), + new(AnalyzeMode.All, "All assemblies") + }; + + SystemVersionModes = new List> + { + new(VersionCompatibilityMode.Off, "Disabled"), + new(VersionCompatibilityMode.Loose, "Loose"), + new(VersionCompatibilityMode.Strict, "Strict") + }; + + _settings.Settings.PropertyChanged += (_, args) => + { + if (args.PropertyName != nameof(_settings.Settings.DarkTheme)) + { + return; + } + + var app = (App)Application.Current; + app.UpdateTheme(); + }; + + SaveAsync = new AsyncRelayCommand(DoSaveAsync); + ViewLogs = new RelayCommand(DoViewLogs); + } + + public RelayCommand ViewLogs { get; } + public AsyncRelayCommand SaveAsync { get; } + public AppSettings Settings => _settings.Settings; + + public List> AnalyzerModes { get; } + + public ComboBoxEntry SelectedAnalyzeMode + { + get => AnalyzerModes.Single(a => a.Value == _settings.Settings.AnalyzeMode); + set => _settings.Settings.AnalyzeMode = value.Value; + } + + public List> SystemVersionModes { get; } + + public ComboBoxEntry SelectedSystemVersionMode + { + get => SystemVersionModes.Single(a => a.Value == _settings.Settings.SystemVersionMode); + set => _settings.Settings.SystemVersionMode = value.Value; + } + + private void DoViewLogs() + { + _logging.OpenLoggingWindow(); + } + + private async Task DoSaveAsync(ICloseable? window) + { + await _settings.SaveAsync(); + window?.Close(); + } +} \ No newline at end of file diff --git a/src/RefScout.Wpf/ViewModels/TechnologiesTabViewModel.cs b/src/RefScout.Wpf/ViewModels/TechnologiesTabViewModel.cs new file mode 100644 index 0000000..7d7b4fc --- /dev/null +++ b/src/RefScout.Wpf/ViewModels/TechnologiesTabViewModel.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Toolkit.Mvvm.ComponentModel; +using RefScout.Analyzer; +using RefScout.Wpf.Models; + +namespace RefScout.Wpf.ViewModels; + +internal class TechnologiesTabViewModel : ObservableObject +{ + private IAnalyzerResult? _analyzerResult; + private IReadOnlyList _frameworkGroups; + private IReadOnlyList _languageGroups; + + public TechnologiesTabViewModel() + { + _frameworkGroups = Array.Empty(); + _languageGroups = Array.Empty(); + } + + public IAnalyzerResult? AnalyzerResult + { + get => _analyzerResult; + set => SetProperty(ref _analyzerResult, value); + } + + public IReadOnlyList FrameworkGroups + { + get => _frameworkGroups; + private set => SetProperty(ref _frameworkGroups, value); + } + + public IReadOnlyList LanguageGroups + { + get => _languageGroups; + private set => SetProperty(ref _languageGroups, value); + } + + public void OnNewAnalyzerResult(IAnalyzerResult? analyzerResult) + { + AnalyzerResult = analyzerResult; + + if (analyzerResult == null) + { + return; + } + + static bool FilterPredicate(Assembly a) => + a.Source is not (AssemblySource.NotFound or AssemblySource.Error) && + !a.IsUnreferenced && !a.IsSystem && !a.IsNetApi; + + FrameworkGroups = analyzerResult.Assemblies + .Where(FilterPredicate) + .OrderBy(a => a.Name) + .GroupBy(u => u.TargetFramework?.Id) + .Select(l => new FrameworkGroup(l.First().TargetFramework!, l)) + .OrderByDescending(g => g.TargetFramework.Version) + .ToList(); + + LanguageGroups = analyzerResult.Assemblies + .Where(FilterPredicate) + .OrderBy(a => a.Name) + .GroupBy(u => u.SourceLanguage) + .Select(l => new LanguageGroup(l.First().SourceLanguage, l)) + .OrderBy(a => (int)a.Language) + .ToList(); + } +} \ No newline at end of file diff --git a/src/RefScout.Wpf/ViewModels/ViewModelLocator.cs b/src/RefScout.Wpf/ViewModels/ViewModelLocator.cs new file mode 100644 index 0000000..429f8a7 --- /dev/null +++ b/src/RefScout.Wpf/ViewModels/ViewModelLocator.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace RefScout.Wpf.ViewModels; + +internal class ViewModelLocator +{ + public static MainWindowViewModel MainWindowViewModel => + App.ServiceProvider!.GetRequiredService(); + + public static SettingsWindowViewModel SettingsWindowViewModel => + App.ServiceProvider!.GetRequiredService(); + + public static DetailsWindowViewModel DetailsWindowViewModel => + App.ServiceProvider!.GetRequiredService(); + + public static LoggingWindowViewModel LoggingWindowViewModel => + App.ServiceProvider!.GetRequiredService(); + + public static AssemblyListTabViewModel AssemblyListTabViewModel => + App.ServiceProvider!.GetRequiredService(); + + public static TechnologiesTabViewModel TechnologiesTabViewModel => + App.ServiceProvider!.GetRequiredService(); + + public static GraphContainerViewModel GraphContainerViewModel => + App.ServiceProvider!.GetRequiredService(); + + public static EnvironmentTabViewModel EnvironmentTabViewModel => + App.ServiceProvider!.GetRequiredService(); +} \ No newline at end of file diff --git a/src/RefScout.Wpf/Views/AssemblyListTab.xaml b/src/RefScout.Wpf/Views/AssemblyListTab.xaml new file mode 100644 index 0000000..fcb8eae --- /dev/null +++ b/src/RefScout.Wpf/Views/AssemblyListTab.xaml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/RefScout.Wpf/Views/AssemblyListTab.xaml.cs b/src/RefScout.Wpf/Views/AssemblyListTab.xaml.cs new file mode 100644 index 0000000..714f73e --- /dev/null +++ b/src/RefScout.Wpf/Views/AssemblyListTab.xaml.cs @@ -0,0 +1,36 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using RefScout.Analyzer; +using RefScout.Wpf.ViewModels; + +namespace RefScout.Wpf.Views; + +public partial class AssemblyListTab : UserControl +{ + public static readonly DependencyProperty AnalyzerResultProperty = + DependencyProperty.Register("AnalyzerResult", + typeof(IAnalyzerResult), + typeof(AssemblyListTab), + new FrameworkPropertyMetadata(null, OnAnalyzerResultChanged)); + + public AssemblyListTab() + { + InitializeComponent(); + } + + public IAnalyzerResult AnalyzerResult + { + get => (IAnalyzerResult)GetValue(AnalyzerResultProperty); + set => SetValue(AnalyzerResultProperty, value); + } + + private static void OnAnalyzerResultChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var container = (AssemblyListTab)d; + var context = (AssemblyListTabViewModel)container.DataContext; + context.OnNewAnalyzerResult(e.NewValue as IAnalyzerResult); + } + + private void OnTargetUpdated(object sender, DataTransferEventArgs e) { } +} \ No newline at end of file diff --git a/src/RefScout.Wpf/Views/Controls/AssemblyView.xaml b/src/RefScout.Wpf/Views/Controls/AssemblyView.xaml new file mode 100644 index 0000000..8397e62 --- /dev/null +++ b/src/RefScout.Wpf/Views/Controls/AssemblyView.xaml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/RefScout.Wpf/Views/Controls/AssemblyView.xaml.cs b/src/RefScout.Wpf/Views/Controls/AssemblyView.xaml.cs new file mode 100644 index 0000000..69a8d90 --- /dev/null +++ b/src/RefScout.Wpf/Views/Controls/AssemblyView.xaml.cs @@ -0,0 +1,35 @@ +using System.Windows; +using System.Windows.Controls; +using RefScout.Analyzer; + +namespace RefScout.Wpf.Views.Controls; + +public partial class AssemblyView : UserControl +{ + public static readonly DependencyProperty InputAssemblyProperty = + DependencyProperty.Register("InputAssembly", + typeof(Assembly), + typeof(AssemblyView)); + + public static readonly DependencyProperty ReferencedByProperty = + DependencyProperty.Register("ReferencedBy", + typeof(bool), + typeof(AssemblyView)); + + public AssemblyView() + { + InitializeComponent(); + } + + public Assembly InputAssembly + { + get => (Assembly)GetValue(InputAssemblyProperty); + set => SetValue(InputAssemblyProperty, value); + } + + public bool ReferencedBy + { + get => (bool)GetValue(ReferencedByProperty); + set => SetValue(ReferencedByProperty, value); + } +} \ No newline at end of file diff --git a/src/RefScout.Wpf/Views/Controls/GraphViewer.xaml b/src/RefScout.Wpf/Views/Controls/GraphViewer.xaml new file mode 100644 index 0000000..8d8d33b --- /dev/null +++ b/src/RefScout.Wpf/Views/Controls/GraphViewer.xaml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/RefScout.Wpf/Views/Controls/GraphViewer.xaml.cs b/src/RefScout.Wpf/Views/Controls/GraphViewer.xaml.cs new file mode 100644 index 0000000..ac790a5 --- /dev/null +++ b/src/RefScout.Wpf/Views/Controls/GraphViewer.xaml.cs @@ -0,0 +1,285 @@ +using System; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using RefScout.Core.Logging; +using SharpVectors.Converters; +using SharpVectors.Renderers.Wpf; + +namespace RefScout.Wpf.Views.Controls; + +public partial class GraphViewer : UserControl +{ + private const double ZoomChange = 0.1; + + public static readonly DependencyProperty VectorPathProperty = + DependencyProperty.Register("VectorPath", + typeof(string), + typeof(GraphViewer), + new FrameworkPropertyMetadata(string.Empty, OnVectorPathChanged)); + + private WpfDrawingDocument? _drawingDocument; + private MouseButton _mouseButtonDown; + private MouseHandlingMode _mouseHandlingMode; + private bool _mouseMoved; + private Point _origContentMouseDownPoint; + + public GraphViewer() + { + InitializeComponent(); + } + + public string VectorPath + { + get => (string)GetValue(VectorPathProperty); + set => SetValue(VectorPathProperty, value); + } + + public double FitZoomValue + { + get + { + if (ZoomPanControl == null) + { + return 1; + } + + var content = ZoomPanControl.ContentElement; + return FitZoom(Container.ActualWidth - SystemParameters.VerticalScrollBarWidth - 2, + Container.ActualHeight - SystemParameters.HorizontalScrollBarHeight - 2, + content?.ActualWidth, content?.ActualHeight); + } + } + + public event EventHandler? HitNodeClicked; + public event EventHandler? GraphRendered; + public event EventHandler? GraphRenderFailed; + + private static async void OnVectorPathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var viewer = (GraphViewer)d; + await viewer.RenderSync((string)e.NewValue); + } + + private double FitZoom( + double actualWidth, + double actualHeight, + double? contentWidth, + double? contentHeight) + { + if (!contentWidth.HasValue || !contentHeight.HasValue) + { + return 1; + } + + return Math.Min(Math.Min(actualWidth / contentWidth.Value, actualHeight / contentHeight.Value), + Math.Min(Container.ActualWidth, Container.ActualHeight) / 300); + } + + private async Task RenderSync(string vectorPath) + { + try + { + await LoadFileAsync(vectorPath); + GraphRendered?.Invoke(this, EventArgs.Empty); + } + catch (Exception e) + { + GraphRenderFailed?.Invoke(this, e); + } + } + + private void OnZoomPanMouseDown(object sender, MouseButtonEventArgs e) + { + _mouseMoved = false; + + ZoomPanControl.Focus(); + Keyboard.Focus(ZoomPanControl); + + _mouseButtonDown = e.ChangedButton; + _origContentMouseDownPoint = e.GetPosition(SvgViewer); + + if ((Keyboard.Modifiers & ModifierKeys.Shift) != 0 && + e.ChangedButton is MouseButton.Left or MouseButton.Right) + { + _mouseHandlingMode = MouseHandlingMode.Zooming; + } + else if (_mouseButtonDown == MouseButton.Left) + { + _mouseHandlingMode = MouseHandlingMode.Panning; + } + else if (_mouseHandlingMode != MouseHandlingMode.None) + { + ZoomPanControl.CaptureMouse(); + e.Handled = true; + } + } + + private void OnZoomPanMouseUp(object sender, MouseButtonEventArgs e) + { + if (_mouseHandlingMode == MouseHandlingMode.Panning && !_mouseMoved && + _mouseButtonDown == MouseButton.Left && _drawingDocument != null) + { + var point = e.GetPosition(SvgViewer); + _drawingDocument.DisplayTransform = SvgViewer.DisplayTransform; + var hitResult = _drawingDocument.HitTest(point); + + var element = hitResult.Element; + if (element?.Id?.StartsWith("hit-", StringComparison.OrdinalIgnoreCase) == true) + { + var id = element.Id[4..]; + HitNodeClicked?.Invoke(this, id); + } + } + + switch (_mouseHandlingMode) + { + case MouseHandlingMode.None: + return; + case MouseHandlingMode.Zooming when _mouseButtonDown == MouseButton.Left: + // Shift + left-click zooms in on the content. + ZoomIn(_origContentMouseDownPoint); + break; + case MouseHandlingMode.Zooming when _mouseButtonDown == MouseButton.Right: + ZoomOut(_origContentMouseDownPoint); + break; + } + + ZoomPanControl.ReleaseMouseCapture(); + _mouseHandlingMode = MouseHandlingMode.None; + e.Handled = true; + } + + private void OnZoomPanMouseMove(object sender, MouseEventArgs e) + { + _mouseMoved = true; + if (_mouseHandlingMode != MouseHandlingMode.Panning) + { + return; + } + + e.Handled = true; + + var curContentMousePoint = e.GetPosition(SvgViewer); + var dragOffset = curContentMousePoint - _origContentMouseDownPoint; + + ZoomPanControl.ContentOffsetX -= dragOffset.X; + ZoomPanControl.ContentOffsetY -= dragOffset.Y; + } + + private void OnZoomPanMouseWheel(object sender, MouseWheelEventArgs e) + { + e.Handled = true; + + var curContentMousePoint = e.GetPosition(SvgViewer); + Zoom(curContentMousePoint, e.Delta); + + if (SvgViewer.IsKeyboardFocusWithin) + { + Keyboard.Focus(ZoomPanControl); + } + } + + private void Zoom(Point contentZoomCenter, int wheelMouseDelta) + { + var zoomFactor = ZoomPanControl.ContentScale + ZoomChange * wheelMouseDelta / (120 * 3); + ZoomPanControl.ZoomAboutPoint(zoomFactor, contentZoomCenter); + UpdateCurrentZoom(zoomFactor); + } + + private void OnZoomIn(object sender, RoutedEventArgs e) + { + ZoomIn(new Point(ZoomPanControl.ContentZoomFocusX, ZoomPanControl.ContentZoomFocusY)); + } + + private void OnZoomOut(object sender, RoutedEventArgs e) + { + ZoomOut(new Point(ZoomPanControl.ContentZoomFocusX, ZoomPanControl.ContentZoomFocusY)); + } + + private void OnZoomFit(object sender, RoutedEventArgs e) + { + ZoomPanControl.AnimatedZoomTo(FitZoomValue); + UpdateCurrentZoom(FitZoomValue); + } + + private void ZoomOut(Point contentZoomCenter) + { + var zoom = ZoomPanControl.ContentScale - ZoomChange; + ZoomPanControl.ZoomAboutPoint(zoom, contentZoomCenter); + UpdateCurrentZoom(zoom); + } + + private void ZoomIn(Point contentZoomCenter) + { + var zoom = ZoomPanControl.ContentScale + ZoomChange; + ZoomPanControl.ZoomAboutPoint(zoom, contentZoomCenter); + UpdateCurrentZoom(zoom); + } + + private void UpdateCurrentZoom(double zoom, double? fitScale = null) + { + var startZoom = fitScale ?? FitZoomValue; + var percentage = Math.Round(zoom / startZoom * 100); + CurrentZoom.Text = $"{percentage}%"; + } + + private async Task LoadFileAsync(string fileName) + { + if (string.IsNullOrEmpty(fileName)) + { + return; + } + + _drawingDocument = null; + using var fileReader = new FileSvgReader(new WpfDrawingSettings()) { SaveXaml = false, SaveZaml = false }; + DrawingGroup drawing; + try + { + drawing = await Task.Run(() => + { + var d = fileReader.Read(new Uri(fileName)); + d.Freeze(); + return d; + }); + } + catch (Exception e) + { + Logger.Error(e, "Could not render SVG output from GraphViz"); + SvgViewer.UnloadDiagrams(); + GraphRenderFailed?.Invoke(this, e); + return; + } + finally + { + fileReader.Dispose(); + } + + _drawingDocument = fileReader.DrawingDocument; + SvgViewer.UnloadDiagrams(); + SvgViewer.RenderDiagrams(drawing); + + ZoomPanControl.InvalidateMeasure(); + + var fitZoom = FitZoom(Container.ActualWidth - SystemParameters.VerticalScrollBarWidth - 2, + Container.ActualHeight - SystemParameters.HorizontalScrollBarHeight - 2, + drawing.Bounds.Width, drawing.Bounds.Height); + ZoomPanControl.ZoomTo(fitZoom); + UpdateCurrentZoom(fitZoom, fitZoom); + } + + private void OnSizeChanged(object sender, SizeChangedEventArgs e) + { + UpdateCurrentZoom(ZoomPanControl.ContentScale); + } +} + +public enum MouseHandlingMode +{ + None, + DraggingRectangles, + Panning, + Zooming +} \ No newline at end of file diff --git a/src/RefScout.Wpf/Views/Controls/Icon.xaml b/src/RefScout.Wpf/Views/Controls/Icon.xaml new file mode 100644 index 0000000..ad01063 --- /dev/null +++ b/src/RefScout.Wpf/Views/Controls/Icon.xaml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/RefScout.Wpf/Views/Controls/Icon.xaml.cs b/src/RefScout.Wpf/Views/Controls/Icon.xaml.cs new file mode 100644 index 0000000..68852d2 --- /dev/null +++ b/src/RefScout.Wpf/Views/Controls/Icon.xaml.cs @@ -0,0 +1,36 @@ +using System.Windows; +using System.Windows.Controls; + +namespace RefScout.Wpf.Views.Controls; + +public partial class Icon : UserControl +{ + public static readonly DependencyProperty IconNameProperty = + DependencyProperty.Register("IconName", + typeof(string), + typeof(Icon), + new FrameworkPropertyMetadata(string.Empty)); + + public static readonly DependencyProperty FitProperty = + DependencyProperty.Register("Fit", + typeof(bool), + typeof(Icon), + new FrameworkPropertyMetadata(false)); + + public Icon() + { + InitializeComponent(); + } + + public string IconName + { + get => (string)GetValue(IconNameProperty); + set => SetValue(IconNameProperty, value); + } + + public bool Fit + { + get => (bool)GetValue(FitProperty); + set => SetValue(FitProperty, value); + } +} \ No newline at end of file diff --git a/src/RefScout.Wpf/Views/Controls/IconButton.xaml b/src/RefScout.Wpf/Views/Controls/IconButton.xaml new file mode 100644 index 0000000..d2d3d19 --- /dev/null +++ b/src/RefScout.Wpf/Views/Controls/IconButton.xaml @@ -0,0 +1,22 @@ + + + \ No newline at end of file diff --git a/src/RefScout.Wpf/Views/Controls/IconButton.xaml.cs b/src/RefScout.Wpf/Views/Controls/IconButton.xaml.cs new file mode 100644 index 0000000..3282dad --- /dev/null +++ b/src/RefScout.Wpf/Views/Controls/IconButton.xaml.cs @@ -0,0 +1,77 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace RefScout.Wpf.Views.Controls; + +public partial class IconButton : UserControl +{ + public static readonly RoutedEvent ClickEvent = + EventManager.RegisterRoutedEvent("Click", + RoutingStrategy.Bubble, + typeof(RoutedEventHandler), + typeof(IconButton)); + + public static readonly DependencyProperty CommandProperty = DependencyProperty.Register("Command", + typeof(ICommand), + typeof(IconButton), + new PropertyMetadata(null)); + + public static readonly DependencyProperty IconProperty = + DependencyProperty.Register("Icon", + typeof(string), + typeof(IconButton), + new FrameworkPropertyMetadata(string.Empty)); + + public static readonly DependencyProperty IconWidthProperty = + DependencyProperty.Register("IconWidth", + typeof(int), + typeof(IconButton), + new FrameworkPropertyMetadata(24)); + + public static readonly DependencyProperty IconHeightProperty = + DependencyProperty.Register("IconHeight", + typeof(int), + typeof(IconButton), + new FrameworkPropertyMetadata(24)); + + public IconButton() + { + InitializeComponent(); + } + + public ICommand Command + { + get => (ICommand)GetValue(CommandProperty); + set => SetValue(CommandProperty, value); + } + + public string Icon + { + get => (string)GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + public int IconWidth + { + get => (int)GetValue(IconWidthProperty); + set => SetValue(IconWidthProperty, value); + } + + public int IconHeight + { + get => (int)GetValue(IconHeightProperty); + set => SetValue(IconHeightProperty, value); + } + + public event RoutedEventHandler Click + { + add => AddHandler(ClickEvent, value); + remove => RemoveHandler(ClickEvent, value); + } + + private void OnClick(object sender, RoutedEventArgs e) + { + RaiseEvent(new RoutedEventArgs(ClickEvent)); + } +} \ No newline at end of file diff --git a/src/RefScout.Wpf/Views/Controls/ReferenceList.xaml b/src/RefScout.Wpf/Views/Controls/ReferenceList.xaml new file mode 100644 index 0000000..ca2d92f --- /dev/null +++ b/src/RefScout.Wpf/Views/Controls/ReferenceList.xaml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/RefScout.Wpf/Views/Controls/ReferenceList.xaml.cs b/src/RefScout.Wpf/Views/Controls/ReferenceList.xaml.cs new file mode 100644 index 0000000..697854f --- /dev/null +++ b/src/RefScout.Wpf/Views/Controls/ReferenceList.xaml.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Controls; +using RefScout.Analyzer; + +namespace RefScout.Wpf.Views.Controls; + +public partial class ReferenceList : UserControl +{ + public static readonly DependencyProperty ReferencesProperty = + DependencyProperty.Register("References", + typeof(IReadOnlyList), + typeof(ReferenceList), + new PropertyMetadata(Array.Empty())); + + public static readonly DependencyProperty ReferencedByProperty = + DependencyProperty.Register("ReferencedBy", + typeof(bool), + typeof(ReferenceList), + new PropertyMetadata(false)); + + public ReferenceList() + { + InitializeComponent(); + } + + public IReadOnlyList References + { + get => (IReadOnlyList)GetValue(ReferencesProperty); + set => SetValue(ReferencesProperty, value); + } + + public bool ReferencedBy + { + get => (bool)GetValue(ReferencedByProperty); + set => SetValue(ReferencedByProperty, value); + } +} \ No newline at end of file diff --git a/src/RefScout.Wpf/Views/Controls/Spinner.xaml b/src/RefScout.Wpf/Views/Controls/Spinner.xaml new file mode 100644 index 0000000..535ddeb --- /dev/null +++ b/src/RefScout.Wpf/Views/Controls/Spinner.xaml @@ -0,0 +1,138 @@ + + + + \ No newline at end of file diff --git a/src/RefScout.Wpf/Views/Controls/Spinner.xaml.cs b/src/RefScout.Wpf/Views/Controls/Spinner.xaml.cs new file mode 100644 index 0000000..d9abab0 --- /dev/null +++ b/src/RefScout.Wpf/Views/Controls/Spinner.xaml.cs @@ -0,0 +1,11 @@ +using System.Windows.Controls; + +namespace RefScout.Wpf.Views.Controls; + +public partial class Spinner : UserControl +{ + public Spinner() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/RefScout.Wpf/Views/DetailsWindow.xaml b/src/RefScout.Wpf/Views/DetailsWindow.xaml new file mode 100644 index 0000000..9d764f3 --- /dev/null +++ b/src/RefScout.Wpf/Views/DetailsWindow.xaml @@ -0,0 +1,365 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/RefScout.Wpf/Views/GraphContainer.xaml.cs b/src/RefScout.Wpf/Views/GraphContainer.xaml.cs new file mode 100644 index 0000000..75c52ac --- /dev/null +++ b/src/RefScout.Wpf/Views/GraphContainer.xaml.cs @@ -0,0 +1,54 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using RefScout.Analyzer; +using RefScout.Wpf.ViewModels; + +namespace RefScout.Wpf.Views; + +public partial class GraphContainer : UserControl +{ + public static readonly DependencyProperty AnalyzerResultProperty = + DependencyProperty.Register("AnalyzerResult", + typeof(IAnalyzerResult), + typeof(GraphContainer), + new FrameworkPropertyMetadata(null, OnAnalyzerResultChanged)); + + public GraphContainer() + { + InitializeComponent(); + } + + public IAnalyzerResult AnalyzerResult + { + get => (IAnalyzerResult)GetValue(AnalyzerResultProperty); + set => SetValue(AnalyzerResultProperty, value); + } + + private static async void OnAnalyzerResultChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var container = (GraphContainer)d; + var context = (GraphContainerViewModel)container.DataContext; + context.AnalyzerResult = (IAnalyzerResult)e.NewValue; + await context.RenderAnalyzerResultAsync(); + } + + private void OnGraphRendered(object? _, EventArgs e) + { + var d = (GraphContainerViewModel)DataContext; + d.LoadingMessage = null; + } + + private void OnGraphRenderFailed(object? _, Exception e) + { + var d = (GraphContainerViewModel)DataContext; + d.LoadingMessage = null; + d.ErrorMessage = e.Message; + } + + private void OnNodeClicked(object? _, string id) + { + var d = (GraphContainerViewModel)DataContext; + d.OpenDetailsWindow(id); + } +} \ No newline at end of file diff --git a/src/RefScout.Wpf/Views/LoggingWindow.xaml b/src/RefScout.Wpf/Views/LoggingWindow.xaml new file mode 100644 index 0000000..dc2004f --- /dev/null +++ b/src/RefScout.Wpf/Views/LoggingWindow.xaml @@ -0,0 +1,81 @@ + + + + + +