diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..27fd46f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,49 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cs text +*.csx text +*.csproj text +*.sln text +*.xaml text +*.config text +*.xml text +*.json text +*.md text +*.txt text +*.yml text +*.yaml text +*.sh text eol=lf +*.ps1 text eol=crlf +*.cmd text eol=crlf +*.bat text eol=crlf + +# Declare files that will always have LF line endings on checkout. +.gitattributes text eol=lf +.gitignore text eol=lf +*.sh text eol=lf + +# Declare files that will always have CRLF line endings on checkout. +*.sln text eol=crlf +*.cmd text eol=crlf +*.bat text eol=crlf +*.ps1 text eol=crlf + +# Denote all files that are truly binary and should not be modified. +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.dll binary +*.exe binary +*.pdb binary +*.zip binary +*.gz binary +*.tar binary +*.7z binary +*.nupkg binary +*.snk binary +*.pfx binary \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cfe7a2e..8201a23 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,8 +60,7 @@ jobs: needs: prepareConfig uses: endjin/Endjin.RecommendedPractices.GitHubActions/.github/workflows/scripted-build-pipeline.yml@main with: - netSdkVersion: '8.x' - additionalNetSdkVersion: '9.x' + netSdkVersion: '10.x' # workflow_dispatch inputs are always strings, the type property is just for the UI forcePublish: ${{ github.event.inputs.forcePublish == 'true' }} skipCleanup: ${{ github.event.inputs.skipCleanup == 'true' }} diff --git a/.gitignore b/.gitignore index 5a7d4f5..31fe69e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,419 +1,422 @@ -## 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/main/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates -*.env - -# 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/ -[Aa][Rr][Mm]64[Ee][Cc]/ -bld/ -[Oo]bj/ -[Oo]ut/ -[Ll]og/ -[Ll]ogs/ - -# Build results on 'Bin' directories -**/[Bb]in/* -# Uncomment if you have tasks that rely on *.refresh files to move binaries -# (https://github.com/github/gitignore/pull/3736) -#!**/[Bb]in/*.refresh - -# 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.* -*.trx - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Approval Tests result files -*.received.* - -# 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 -*.idb -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -# but not Directory.Build.rsp, as it configures directory-level build defaults -!Directory.Build.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_* -.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 - -# 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 6 auto-generated project file (contains which files were open etc.) -*.vbp - -# Visual Studio 6 workspace and project file (working project files containing files to include in project) -*.dsw -*.dsp - -# Visual Studio 6 technical files -*.ncb -*.aps - -# 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 - -# 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 -MSBuild_Logs/ - -# AWS SAM Build and Temporary Artifacts folder -.aws-sam - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -**/.mfractor/ - -# Local History for Visual Studio -**/.localhistory/ - -# Visual Studio History (VSHistory) files -.vshistory/ - -# 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 -!.vscode/*.code-snippets - -# Local History for Visual Studio Code -.history/ - -# Built Visual Studio Code Extensions -*.vsix - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp -demo_output/ \ No newline at end of file +## 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/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates +*.env + +# 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/ +[Aa][Rr][Mm]64[Ee][Cc]/ +bld/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Build results on 'Bin' directories +**/[Bb]in/* +# Uncomment if you have tasks that rely on *.refresh files to move binaries +# (https://github.com/github/gitignore/pull/3736) +#!**/[Bb]in/*.refresh + +# 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.* +*.trx + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Approval Tests result files +*.received.* + +# 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 +*.idb +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +# but not Directory.Build.rsp, as it configures directory-level build defaults +!Directory.Build.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_* +.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 + +# 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 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# 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 + +# 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 +MSBuild_Logs/ + +# AWS SAM Build and Temporary Artifacts folder +.aws-sam + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +**/.mfractor/ + +# Local History for Visual Studio +**/.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# 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 +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp +demo_output/ + +/.devcontainer/ +/.claude/ \ No newline at end of file diff --git a/.zf/config.ps1 b/.zf/config.ps1 index 05757cc..656b6d8 100644 --- a/.zf/config.ps1 +++ b/.zf/config.ps1 @@ -1,65 +1,65 @@ -<# -This example demonstrates a software build process using the 'ZeroFailed.Build.DotNet' extension -to provide the features needed when building a .NET solutions. -#> - -$zerofailedExtensions = @( - @{ - # References the extension from its GitHub repository. If not already installed, use latest version from 'main' will be downloaded. - Name = "ZeroFailed.Build.DotNet" - GitRepository = "https://github.com/zerofailed/ZeroFailed.Build.DotNet" - GitRef = "main" - } -) - -# Load the tasks and process -. ZeroFailed.tasks -ZfPath $here/.zf - - -# -# Build process control options -# -$SkipInit = $false -$SkipVersion = $false -$SkipBuild = $false -$CleanBuild = $Clean -$SkipTest = $false -$SkipTestReport = $false -$SkipAnalysis = $false -$SkipPackage = $false - -# -# Build process configuration -# -$SolutionToBuild = (Resolve-Path (Join-Path $here ".\Solutions\DeadCode.sln")).Path -$ProjectsToPublish = @() -$NuSpecFilesToPackage = @() -$NugetPublishSource = property ZF_NUGET_PUBLISH_SOURCE "$here/_local-nuget-feed" -$IncludeAssembliesInCodeCoverage = "DeadCode*" - - -# Synopsis: Build, Test and Package -task . FullBuild - -# -# Build Process Extensibility Points - uncomment and implement as required -# - -# task RunFirst {} -# task PreInit {} -# task PostInit {} -# task PreVersion {} -# task PostVersion {} -# task PreBuild {} -# task PostBuild {} -# task PreTest {} -# task PostTest {} -# task PreTestReport {} -# task PostTestReport {} -# task PreAnalysis {} -# task PostAnalysis {} -# task PrePackage {} -# task PostPackage {} -# task PrePublish {} -# task PostPublish {} -# task RunLast {} +<# +This example demonstrates a software build process using the 'ZeroFailed.Build.DotNet' extension +to provide the features needed when building a .NET solutions. +#> + +$zerofailedExtensions = @( + @{ + # References the extension from its GitHub repository. If not already installed, use latest version from 'main' will be downloaded. + Name = "ZeroFailed.Build.DotNet" + GitRepository = "https://github.com/zerofailed/ZeroFailed.Build.DotNet" + GitRef = "main" + } +) + +# Load the tasks and process +. ZeroFailed.tasks -ZfPath $here/.zf + + +# +# Build process control options +# +$SkipInit = $false +$SkipVersion = $false +$SkipBuild = $false +$CleanBuild = $Clean +$SkipTest = $false +$SkipTestReport = $false +$SkipAnalysis = $false +$SkipPackage = $false + +# +# Build process configuration +# +$SolutionToBuild = (Resolve-Path (Join-Path $here ".\Solutions\DeadCode.slnx")).Path +$ProjectsToPublish = @() +$NuSpecFilesToPackage = @() +$NugetPublishSource = property ZF_NUGET_PUBLISH_SOURCE "$here/_local-nuget-feed" +$IncludeAssembliesInCodeCoverage = "DeadCode*" + + +# Synopsis: Build, Test and Package +task . FullBuild + +# +# Build Process Extensibility Points - uncomment and implement as required +# + +# task RunFirst {} +# task PreInit {} +# task PostInit {} +# task PreVersion {} +# task PostVersion {} +# task PreBuild {} +# task PostBuild {} +# task PreTest {} +# task PostTest {} +# task PreTestReport {} +# task PostTestReport {} +# task PreAnalysis {} +# task PostAnalysis {} +# task PrePackage {} +# task PostPackage {} +# task PrePublish {} +# task PostPublish {} +# task RunLast {} diff --git a/README.md b/README.md index dddcf9f..aae2fe2 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ DeadCode combines static reflection-based method extraction with runtime trace p - **Framework Filtering**: Automatically filters out System.*, Microsoft.*, and Internal.* methods - **Smart Method Handling**: Special handling for async state machines, lambdas, and constructors - **Interactive Setup**: Automatic dotnet-trace installation if missing -- **Modern .NET**: Built on .NET 9.0 with C# 12 language features +- **Modern .NET**: Built on .NET 10.0 with C# 13 language features ## Installation @@ -37,7 +37,7 @@ Run complete analysis pipeline: dotnet build -c Release # Run full analysis -deadcode full --assemblies ./bin/Release/net9.0/*.dll --executable ./bin/Release/net9.0/MyApp.exe +deadcode full --assemblies ./bin/Release/net10.0/*.dll --executable ./bin/Release/net10.0/MyApp.exe ``` ### Individual Commands @@ -45,7 +45,7 @@ deadcode full --assemblies ./bin/Release/net9.0/*.dll --executable ./bin/Release #### 1. Extract Method Inventory ```bash -deadcode extract bin/Release/net9.0/*.dll -o inventory.json +deadcode extract bin/Release/net10.0/*.dll -o inventory.json ``` Example `inventory.json`: @@ -255,7 +255,7 @@ Solutions/ ## Requirements -- .NET 9.0 SDK or later +- .NET 10.0 SDK or later - Windows, Linux, or macOS - dotnet-trace (automatically installed if missing) @@ -330,7 +330,7 @@ Apache License 2.0 - see LICENSE file for details. ## Example Workflow 1. **Build Project**: `dotnet build -c Release` -2. **Run Analysis**: `deadcode full --assemblies bin/Release/net9.0/*.dll --executable MyApp.exe` +2. **Run Analysis**: `deadcode full --assemblies bin/Release/net10.0/*.dll --executable MyApp.exe` 3. **Review Report**: Check `analysis/report.json` for unused methods 4. **LLM Cleanup**: Feed report to Claude/GPT for automated cleanup tasks diff --git a/Solutions/DeadCode.Tests/CLI/Commands/AnalyzeCommandTests.cs b/Solutions/DeadCode.Tests/CLI/Commands/AnalyzeCommandTests.cs index 196346f..11a2a02 100644 --- a/Solutions/DeadCode.Tests/CLI/Commands/AnalyzeCommandTests.cs +++ b/Solutions/DeadCode.Tests/CLI/Commands/AnalyzeCommandTests.cs @@ -187,7 +187,7 @@ public async Task ExecuteAsync_WithValidInput_SuccessfulExecution() .Returns(report); // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(0); @@ -230,7 +230,7 @@ public async Task ExecuteAsync_WithDirectoryTracePath_FindsTraceFiles() .Returns(report); // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(0); @@ -258,7 +258,7 @@ public async Task ExecuteAsync_WithNoTraceFiles_Returns1() }; // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(1); @@ -291,7 +291,7 @@ public async Task ExecuteAsync_WithException_LogsErrorAndReturns1() .ThrowsAsync(expectedException); // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(1); @@ -328,7 +328,7 @@ public async Task ExecuteAsync_FiltersReportByConfidenceLevel() .Returns(report); // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(0); @@ -373,7 +373,7 @@ public async Task ExecuteAsync_AggregatesMultipleTraceFiles() .Returns(report); // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(0); @@ -445,7 +445,7 @@ public async Task ExecuteAsync_WritesExpectedConsoleOutput() .Returns(report); // Act - await command.ExecuteAsync(context, settings); + await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert string output = testConsole.Output; diff --git a/Solutions/DeadCode.Tests/CLI/Commands/ExtractCommandTests.cs b/Solutions/DeadCode.Tests/CLI/Commands/ExtractCommandTests.cs index 561b887..180c3e6 100644 --- a/Solutions/DeadCode.Tests/CLI/Commands/ExtractCommandTests.cs +++ b/Solutions/DeadCode.Tests/CLI/Commands/ExtractCommandTests.cs @@ -129,7 +129,7 @@ public async Task ExecuteAsync_WithValidInput_SuccessfulExecution() .Returns(testInventory); // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(0); @@ -153,7 +153,7 @@ public async Task ExecuteAsync_WithException_LogsErrorAndReturns1() .ThrowsAsync(expectedException); // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(1); // The command returns 1 on exception @@ -179,7 +179,7 @@ public async Task ExecuteAsync_PassesCorrectExtractionOptions() .Returns(testInventory); // Act - await command.ExecuteAsync(context, settings); + await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert await mockExtractor.Received(1).ExtractAsync( @@ -204,7 +204,7 @@ public async Task ExecuteAsync_WithDefaultSettings_UsesDefaults() .Returns(testInventory); // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(0); @@ -238,7 +238,7 @@ public async Task ExecuteAsync_CallsProgressCallback() }); // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(0); @@ -259,7 +259,7 @@ public async Task ExecuteAsync_LogsInformationMessages() .Returns(testInventory); // Act - await command.ExecuteAsync(context, settings); + await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert mockLogger.Received().LogInformation("Starting method inventory extraction"); @@ -281,7 +281,7 @@ public async Task ExecuteAsync_WritesExpectedConsoleOutput() .Returns(testInventory); // Act - await command.ExecuteAsync(context, settings); + await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert string output = testConsole.Output; diff --git a/Solutions/DeadCode.Tests/CLI/Commands/FullCommandTests.cs b/Solutions/DeadCode.Tests/CLI/Commands/FullCommandTests.cs index 461a3d5..30f053a 100644 --- a/Solutions/DeadCode.Tests/CLI/Commands/FullCommandTests.cs +++ b/Solutions/DeadCode.Tests/CLI/Commands/FullCommandTests.cs @@ -181,15 +181,15 @@ public async Task ExecuteAsync_WithSuccessfulPipeline_Returns0() MinConfidence = "high" }; - mockExtractCommand.ExecuteAsync(context, Arg.Any()) + mockExtractCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(0); - mockProfileCommand.ExecuteAsync(context, Arg.Any()) + mockProfileCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(0); - mockAnalyzeCommand.ExecuteAsync(context, Arg.Any()) + mockAnalyzeCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(0); // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(0); @@ -217,16 +217,16 @@ public async Task ExecuteAsync_WithFailedExtraction_ReturnsExitCode() OutputDirectory = tempDir.FullName }; - mockExtractCommand.ExecuteAsync(context, Arg.Any()) + mockExtractCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(1); // Failed extraction // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(1); - await mockProfileCommand.DidNotReceive().ExecuteAsync(Arg.Any(), Arg.Any()); - await mockAnalyzeCommand.DidNotReceive().ExecuteAsync(Arg.Any(), Arg.Any()); + await mockProfileCommand.DidNotReceive().ExecuteAsync(Arg.Any(), Arg.Any(), Arg.Any()); + await mockAnalyzeCommand.DidNotReceive().ExecuteAsync(Arg.Any(), Arg.Any(), Arg.Any()); } finally { @@ -251,19 +251,19 @@ public async Task ExecuteAsync_WithFailedProfiling_ContinuesToAnalysis() OutputDirectory = tempDir.FullName }; - mockExtractCommand.ExecuteAsync(context, Arg.Any()) + mockExtractCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(0); - mockProfileCommand.ExecuteAsync(context, Arg.Any()) + mockProfileCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(1); // Failed profiling - mockAnalyzeCommand.ExecuteAsync(context, Arg.Any()) + mockAnalyzeCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(0); // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(0); - await mockAnalyzeCommand.Received(1).ExecuteAsync(Arg.Any(), Arg.Any()); + await mockAnalyzeCommand.Received(1).ExecuteAsync(Arg.Any(), Arg.Any(), Arg.Any()); } finally { @@ -288,15 +288,15 @@ public async Task ExecuteAsync_WithFailedAnalysis_ReturnsExitCode() OutputDirectory = tempDir.FullName }; - mockExtractCommand.ExecuteAsync(context, Arg.Any()) + mockExtractCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(0); - mockProfileCommand.ExecuteAsync(context, Arg.Any()) + mockProfileCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(0); - mockAnalyzeCommand.ExecuteAsync(context, Arg.Any()) + mockAnalyzeCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(1); // Failed analysis // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(1); @@ -326,35 +326,38 @@ public async Task ExecuteAsync_PassesCorrectSettingsToSubCommands() MinConfidence = "medium" }; - mockExtractCommand.ExecuteAsync(context, Arg.Any()) + mockExtractCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(0); - mockProfileCommand.ExecuteAsync(context, Arg.Any()) + mockProfileCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(0); - mockAnalyzeCommand.ExecuteAsync(context, Arg.Any()) + mockAnalyzeCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(0); // Act - await command.ExecuteAsync(context, settings); + await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert await mockExtractCommand.Received(1).ExecuteAsync(context, Arg.Is(s => s.Assemblies.SequenceEqual(settings.Assemblies) && s.OutputPath == Path.Combine(settings.OutputDirectory, "inventory.json") && - s.IncludeGenerated == false)); + s.IncludeGenerated == false), + Arg.Any()); await mockProfileCommand.Received(1).ExecuteAsync(context, Arg.Is(s => s.ExecutablePath == settings.ExecutablePath && s.ScenariosPath == settings.ScenariosPath && - s.OutputDirectory == Path.Combine(settings.OutputDirectory, "traces"))); + s.OutputDirectory == Path.Combine(settings.OutputDirectory, "traces")), + Arg.Any()); await mockAnalyzeCommand.Received(1).ExecuteAsync(context, Arg.Is(s => s.InventoryPath == Path.Combine(settings.OutputDirectory, "inventory.json") && s.TracePaths.SequenceEqual(new[] { Path.Combine(settings.OutputDirectory, "traces") }) && s.OutputPath == Path.Combine(settings.OutputDirectory, "report.json") && - s.MinConfidence == settings.MinConfidence)); + s.MinConfidence == settings.MinConfidence), + Arg.Any()); } finally { @@ -379,15 +382,15 @@ public async Task ExecuteAsync_CreatesOutputDirectory() OutputDirectory = tempDir }; - mockExtractCommand.ExecuteAsync(context, Arg.Any()) + mockExtractCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(0); - mockProfileCommand.ExecuteAsync(context, Arg.Any()) + mockProfileCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(0); - mockAnalyzeCommand.ExecuteAsync(context, Arg.Any()) + mockAnalyzeCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(0); // Act - await command.ExecuteAsync(context, settings); + await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert Directory.Exists(tempDir).ShouldBeTrue(); @@ -417,11 +420,11 @@ public async Task ExecuteAsync_WithException_LogsErrorAndReturns1() }; InvalidOperationException expectedException = new("Test error"); - mockExtractCommand.ExecuteAsync(context, Arg.Any()) + mockExtractCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .ThrowsAsync(expectedException); // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(1); @@ -453,15 +456,15 @@ public async Task ExecuteAsync_LogsInformationMessages() OutputDirectory = tempDir.FullName }; - mockExtractCommand.ExecuteAsync(context, Arg.Any()) + mockExtractCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(0); - mockProfileCommand.ExecuteAsync(context, Arg.Any()) + mockProfileCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(0); - mockAnalyzeCommand.ExecuteAsync(context, Arg.Any()) + mockAnalyzeCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(0); // Act - await command.ExecuteAsync(context, settings); + await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert mockLogger.Received().LogInformation("Starting full deadcode analysis pipeline"); @@ -490,15 +493,15 @@ public async Task ExecuteAsync_WritesExpectedConsoleOutput() OutputDirectory = tempDir.FullName }; - mockExtractCommand.ExecuteAsync(context, Arg.Any()) + mockExtractCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(0); - mockProfileCommand.ExecuteAsync(context, Arg.Any()) + mockProfileCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(0); - mockAnalyzeCommand.ExecuteAsync(context, Arg.Any()) + mockAnalyzeCommand.ExecuteAsync(context, Arg.Any(), Arg.Any()) .Returns(0); // Act - await command.ExecuteAsync(context, settings); + await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert string output = testConsole.Output; diff --git a/Solutions/DeadCode.Tests/CLI/Commands/ProfileCommandTests.cs b/Solutions/DeadCode.Tests/CLI/Commands/ProfileCommandTests.cs index f452d3f..7faafe0 100644 --- a/Solutions/DeadCode.Tests/CLI/Commands/ProfileCommandTests.cs +++ b/Solutions/DeadCode.Tests/CLI/Commands/ProfileCommandTests.cs @@ -139,6 +139,71 @@ public void Settings_Validation_SucceedsWithValidExecutable() } } + [TestMethod] + public void Settings_Validation_RejectsZeroDuration() + { + string tempExe = Path.GetTempFileName(); + try + { + ProfileCommand.Settings settings = new() { ExecutablePath = tempExe, Duration = 0 }; + Spectre.Console.ValidationResult result = settings.Validate(); + result.Successful.ShouldBeFalse(); + result.Message!.ShouldContain("Duration must be a positive number"); + } + finally + { + File.Delete(tempExe); + } + } + + [TestMethod] + public void Settings_Validation_RejectsNegativeDuration() + { + string tempExe = Path.GetTempFileName(); + try + { + ProfileCommand.Settings settings = new() { ExecutablePath = tempExe, Duration = -5 }; + Spectre.Console.ValidationResult result = settings.Validate(); + result.Successful.ShouldBeFalse(); + } + finally + { + File.Delete(tempExe); + } + } + + [TestMethod] + public void Settings_Validation_AcceptsPositiveDuration() + { + string tempExe = Path.GetTempFileName(); + try + { + ProfileCommand.Settings settings = new() { ExecutablePath = tempExe, Duration = 30 }; + Spectre.Console.ValidationResult result = settings.Validate(); + result.Successful.ShouldBeTrue(); + } + finally + { + File.Delete(tempExe); + } + } + + [TestMethod] + public void Settings_Validation_AcceptsNullDuration() + { + string tempExe = Path.GetTempFileName(); + try + { + ProfileCommand.Settings settings = new() { ExecutablePath = tempExe, Duration = null }; + Spectre.Console.ValidationResult result = settings.Validate(); + result.Successful.ShouldBeTrue(); + } + finally + { + File.Delete(tempExe); + } + } + [TestMethod] public async Task ExecuteAsync_WithMissingDependencies_Returns1() { @@ -156,7 +221,7 @@ public async Task ExecuteAsync_WithMissingDependencies_Returns1() testConsole.Input.PushTextWithEnter("n"); // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(1); @@ -192,7 +257,7 @@ public async Task ExecuteAsync_WithSuccessfulProfiling_Returns0() .Returns(CreateSuccessfulTraceResult()); // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(0); @@ -223,7 +288,7 @@ public async Task ExecuteAsync_WithFailedProfiling_Returns1() .Returns(CreateFailedTraceResult()); // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(1); @@ -270,7 +335,7 @@ public async Task ExecuteAsync_WithScenariosFile_LoadsScenarios() .Returns(CreateSuccessfulTraceResult()); // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(0); @@ -307,7 +372,7 @@ public async Task ExecuteAsync_WithoutScenariosFile_CreatesDefaultScenario() .Returns(CreateSuccessfulTraceResult()); // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(0); @@ -342,7 +407,7 @@ public async Task ExecuteAsync_WithProfilingException_LogsError() .ThrowsAsync(new InvalidOperationException("Test error")); // Act - int result = await command.ExecuteAsync(context, settings); + int result = await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert result.ShouldBe(1); @@ -384,7 +449,7 @@ public async Task ExecuteAsync_PassesCorrectProfilingOptions() .Returns(CreateSuccessfulTraceResult()); // Act - await command.ExecuteAsync(context, settings); + await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert await mockTraceRunner.Received(1).RunProfilingAsync( @@ -421,7 +486,7 @@ public async Task ExecuteAsync_WritesExpectedConsoleOutput() .Returns(CreateSuccessfulTraceResult()); // Act - await command.ExecuteAsync(context, settings); + await command.ExecuteAsync(context, settings, CancellationToken.None); // Assert string output = testConsole.Output; diff --git a/Solutions/DeadCode.Tests/CLI/Infrastructure/TypeRegistrarTests.cs b/Solutions/DeadCode.Tests/CLI/Infrastructure/TypeRegistrarTests.cs index 9dc74fd..244184e 100644 --- a/Solutions/DeadCode.Tests/CLI/Infrastructure/TypeRegistrarTests.cs +++ b/Solutions/DeadCode.Tests/CLI/Infrastructure/TypeRegistrarTests.cs @@ -7,19 +7,18 @@ namespace DeadCode.Tests.CLI.Infrastructure; [TestClass] public class TypeRegistrarTests { - private readonly IServiceProvider serviceProvider; + private readonly ServiceCollection services; private readonly TypeRegistrar registrar; public TypeRegistrarTests() { - ServiceCollection services = new(); + services = new ServiceCollection(); services.AddSingleton(); - serviceProvider = services.BuildServiceProvider(); - registrar = new TypeRegistrar(serviceProvider); + registrar = new TypeRegistrar(services); } [TestMethod] - public void Constructor_WithNullServiceProvider_ThrowsArgumentNullException() + public void Constructor_WithNullServices_ThrowsArgumentNullException() { // Act & Assert Should.Throw(() => new TypeRegistrar(null!)); @@ -37,27 +36,54 @@ public void Build_ReturnsTypeResolver() } [TestMethod] - public void Register_DoesNotThrow() + public void Register_MakesTypeResolvable() { - // Act & Assert - Should.NotThrow(() => registrar.Register(typeof(ITestService), typeof(TestService))); + // Arrange + ServiceCollection freshServices = new(); + TypeRegistrar freshRegistrar = new(freshServices); + + // Act + freshRegistrar.Register(typeof(ITestService), typeof(TestService)); + ITypeResolver resolver = freshRegistrar.Build(); + + // Assert + object? result = resolver.Resolve(typeof(ITestService)); + result.ShouldNotBeNull(); + result.ShouldBeOfType(); } [TestMethod] - public void RegisterInstance_DoesNotThrow() + public void RegisterInstance_MakesInstanceResolvable() { // Arrange + ServiceCollection freshServices = new(); + TypeRegistrar freshRegistrar = new(freshServices); TestService instance = new(); - // Act & Assert - Should.NotThrow(() => registrar.RegisterInstance(typeof(ITestService), instance)); + // Act + freshRegistrar.RegisterInstance(typeof(ITestService), instance); + ITypeResolver resolver = freshRegistrar.Build(); + + // Assert + object? result = resolver.Resolve(typeof(ITestService)); + result.ShouldBe(instance); } [TestMethod] - public void RegisterLazy_DoesNotThrow() + public void RegisterLazy_MakesLazyInstanceResolvable() { - // Act & Assert - Should.NotThrow(() => registrar.RegisterLazy(typeof(ITestService), () => new TestService())); + // Arrange + ServiceCollection freshServices = new(); + TypeRegistrar freshRegistrar = new(freshServices); + TestService instance = new(); + + // Act + freshRegistrar.RegisterLazy(typeof(ITestService), () => instance); + ITypeResolver resolver = freshRegistrar.Build(); + + // Assert + object? result = resolver.Resolve(typeof(ITestService)); + result.ShouldBe(instance); } [TestMethod] @@ -147,4 +173,4 @@ public void TypeResolver_ImplementsITypeResolver() public interface ITestService { } public class TestService : ITestService { } public class UnregisteredService { } -} \ No newline at end of file +} diff --git a/Solutions/DeadCode.Tests/CLI/TestHelpers/CommandTestHelpers.cs b/Solutions/DeadCode.Tests/CLI/TestHelpers/CommandTestHelpers.cs deleted file mode 100644 index e757622..0000000 --- a/Solutions/DeadCode.Tests/CLI/TestHelpers/CommandTestHelpers.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Spectre.Console.Testing; - -namespace DeadCode.Tests.CLI.TestHelpers; - -public static class CommandTestHelpers -{ - /// - /// Helper for creating test environments for CLI commands - /// Note: CommandContext is sealed and has internal constructors, - /// so we can't mock or create it directly for unit tests. - /// - public static TestConsole CreateTestConsole() - { - return new TestConsole(); - } -} \ No newline at end of file diff --git a/Solutions/DeadCode.Tests/Core/Models/MethodInventoryTests.cs b/Solutions/DeadCode.Tests/Core/Models/MethodInventoryTests.cs index fef5b67..34cf466 100644 --- a/Solutions/DeadCode.Tests/Core/Models/MethodInventoryTests.cs +++ b/Solutions/DeadCode.Tests/Core/Models/MethodInventoryTests.cs @@ -171,4 +171,23 @@ private static MethodInfo CreateTestMethod( SafetyLevel: safety ); } + + [TestMethod] + public void MethodsByAssembly_AfterAddMethod_ReflectsNewMethod() + { + // Arrange + MethodInventory inventory = new(); + inventory.AddMethod(CreateTestMethod("Method1")); + + // Act - access to populate cache + IReadOnlyDictionary> before = inventory.MethodsByAssembly; + before.Values.Sum(v => v.Count).ShouldBe(1); + + // Add another method (should invalidate cache) + inventory.AddMethod(CreateTestMethod("Method2")); + IReadOnlyDictionary> after = inventory.MethodsByAssembly; + + // Assert + after.Values.Sum(v => v.Count).ShouldBe(2); + } } \ No newline at end of file diff --git a/Solutions/DeadCode.Tests/Core/Models/RedundancyReportTests.cs b/Solutions/DeadCode.Tests/Core/Models/RedundancyReportTests.cs index 4cb00ca..8da1771 100644 --- a/Solutions/DeadCode.Tests/Core/Models/RedundancyReportTests.cs +++ b/Solutions/DeadCode.Tests/Core/Models/RedundancyReportTests.cs @@ -235,4 +235,21 @@ private static UnusedMethod CreateUnusedMethod(SafetyClassification safety) return new UnusedMethod(method, []); } + + [TestMethod] + public void HighConfidenceMethods_AfterAddUnusedMethod_ReflectsNewMethod() + { + // Arrange + RedundancyReport report = new() { AnalyzedAssemblies = [], TraceScenarios = [] }; + report.AddUnusedMethod(CreateUnusedMethod(SafetyClassification.HighConfidence)); + + // Act - access to populate cache + report.HighConfidenceMethods.Count().ShouldBe(1); + + // Add another method (should invalidate cache) + report.AddUnusedMethod(CreateUnusedMethod(SafetyClassification.HighConfidence)); + + // Assert + report.HighConfidenceMethods.Count().ShouldBe(2); + } } \ No newline at end of file diff --git a/Solutions/DeadCode.Tests/DeadCode.Tests.csproj b/Solutions/DeadCode.Tests/DeadCode.Tests.csproj index 18728fe..5c67259 100644 --- a/Solutions/DeadCode.Tests/DeadCode.Tests.csproj +++ b/Solutions/DeadCode.Tests/DeadCode.Tests.csproj @@ -1,25 +1,22 @@ - net9.0 - enable - enable false true - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + - - - + + + diff --git a/Solutions/DeadCode.Tests/Infrastructure/IO/ComparisonEngineTests.cs b/Solutions/DeadCode.Tests/Infrastructure/IO/ComparisonEngineTests.cs index 28740ec..30e6e35 100644 --- a/Solutions/DeadCode.Tests/Infrastructure/IO/ComparisonEngineTests.cs +++ b/Solutions/DeadCode.Tests/Infrastructure/IO/ComparisonEngineTests.cs @@ -205,6 +205,30 @@ public void IdentifyUnusedMethods_GroupsByConfidenceLevel() report.LowConfidenceMethods.Count().ShouldBe(1); } + [TestMethod] + public void IdentifyUnusedMethods_WithNullInventory_ThrowsArgumentNullException() + { + Should.Throw(() => engine.IdentifyUnusedMethods(null!, [])); + } + + [TestMethod] + public void IdentifyUnusedMethods_WithNullExecutedMethods_ThrowsArgumentNullException() + { + Should.Throw(() => engine.IdentifyUnusedMethods(new MethodInventory(), null!)); + } + + [TestMethod] + public async Task CompareAsync_WithNullInventory_ThrowsArgumentNullException() + { + await Should.ThrowAsync(() => engine.CompareAsync(null!, [])); + } + + [TestMethod] + public async Task CompareAsync_WithNullExecutedMethods_ThrowsArgumentNullException() + { + await Should.ThrowAsync(() => engine.CompareAsync(new MethodInventory(), null!)); + } + // Helper method private static MethodInfo CreateMethodInfo(string name, SafetyClassification safety) { diff --git a/Solutions/DeadCode.Tests/Infrastructure/IO/JsonReportGeneratorTests.cs b/Solutions/DeadCode.Tests/Infrastructure/IO/JsonReportGeneratorTests.cs index a43cf5f..3cb0a51 100644 --- a/Solutions/DeadCode.Tests/Infrastructure/IO/JsonReportGeneratorTests.cs +++ b/Solutions/DeadCode.Tests/Infrastructure/IO/JsonReportGeneratorTests.cs @@ -210,4 +210,24 @@ private static UnusedMethod CreateUnusedMethod(string name, SafetyClassification return new UnusedMethod(method, ["dep1", "dep2"]); } + + [TestMethod] + public async Task GenerateAsync_WithNullReport_ThrowsArgumentNullException() + { + await Should.ThrowAsync(() => generator.GenerateAsync(null!, "output.json")); + } + + [TestMethod] + public async Task GenerateAsync_WithNullOutputPath_ThrowsArgumentException() + { + RedundancyReport report = new() { AnalyzedAssemblies = [], TraceScenarios = [] }; + await Should.ThrowAsync(() => generator.GenerateAsync(report, null!)); + } + + [TestMethod] + public async Task GenerateAsync_WithEmptyOutputPath_ThrowsArgumentException() + { + RedundancyReport report = new() { AnalyzedAssemblies = [], TraceScenarios = [] }; + await Should.ThrowAsync(() => generator.GenerateAsync(report, "")); + } } \ No newline at end of file diff --git a/Solutions/DeadCode.Tests/Integration/DeadCodeDetectionIntegrationTests.cs b/Solutions/DeadCode.Tests/Integration/DeadCodeDetectionIntegrationTests.cs index b9939e4..32c4ef3 100644 --- a/Solutions/DeadCode.Tests/Integration/DeadCodeDetectionIntegrationTests.cs +++ b/Solutions/DeadCode.Tests/Integration/DeadCodeDetectionIntegrationTests.cs @@ -14,7 +14,7 @@ namespace DeadCode.Tests.Integration; [TestClass] public class DeadCodeDetectionIntegrationTests : IDisposable { - private const string SampleAppPath = "Samples/SampleAppWithDeadCode/bin/Debug/net9.0/SampleAppWithDeadCode.dll"; + private const string SampleAppPath = "Samples/SampleAppWithDeadCode/bin/Debug/net10.0/SampleAppWithDeadCode.dll"; private readonly string testOutputDir; private readonly IServiceProvider serviceProvider; private readonly TestConsole console; @@ -361,6 +361,8 @@ private async Task RunAnalysis(string traceFile) return report; } + private static readonly SemaphoreSlim buildLock = new(1, 1); + private async Task EnsureSampleAppBuilt() { // Navigate from test assembly location to project root @@ -376,25 +378,46 @@ private async Task EnsureSampleAppBuilt() // Check if the dll exists string dllPath = Path.Combine(projectRoot, SampleAppPath); - if (!File.Exists(dllPath)) + if (File.Exists(dllPath)) { + return; + } + + // Serialize concurrent build attempts (tests run in parallel) + await buildLock.WaitAsync(); + try + { + // Double-check after acquiring lock + if (File.Exists(dllPath)) + { + return; + } + // Build the project System.Diagnostics.ProcessStartInfo startInfo = new() { FileName = "dotnet", Arguments = $"build \"{projectPath}\" -c Debug", UseShellExecute = false, - CreateNoWindow = true + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true }; using System.Diagnostics.Process? process = System.Diagnostics.Process.Start(startInfo); - await process!.WaitForExitAsync(); + string stdout = await process!.StandardOutput.ReadToEndAsync(); + string stderr = await process.StandardError.ReadToEndAsync(); + await process.WaitForExitAsync(); if (process.ExitCode != 0) { - throw new InvalidOperationException("Failed to build sample app"); + Assert.Inconclusive($"Failed to build sample app:\n{stderr}\n{stdout}"); } } + finally + { + buildLock.Release(); + } } private void AssertMethodIsUnused(RedundancyReport report, string className, string methodName) diff --git a/Solutions/DeadCode.Tests/Integration/ProgramIntegrationTests.cs b/Solutions/DeadCode.Tests/Integration/ProgramIntegrationTests.cs index 7713d70..1b96f06 100644 --- a/Solutions/DeadCode.Tests/Integration/ProgramIntegrationTests.cs +++ b/Solutions/DeadCode.Tests/Integration/ProgramIntegrationTests.cs @@ -72,14 +72,13 @@ public async Task Main_WithVersionFlag_ShowsVersion() } [TestMethod] - public void TypeRegistrar_CanBeCreatedWithServiceProvider() + public void TypeRegistrar_CanBeCreatedWithServiceCollection() { // Arrange ServiceCollection services = new(); - ServiceProvider serviceProvider = services.BuildServiceProvider(); // Act - TypeRegistrar registrar = new(serviceProvider); + TypeRegistrar registrar = new(services); ITypeResolver resolver = registrar.Build(); // Assert diff --git a/Solutions/DeadCode.Tests/Integration/ReflectionMethodScannerIntegrationTests.cs b/Solutions/DeadCode.Tests/Integration/ReflectionMethodScannerIntegrationTests.cs index 628223e..c8bf24d 100644 --- a/Solutions/DeadCode.Tests/Integration/ReflectionMethodScannerIntegrationTests.cs +++ b/Solutions/DeadCode.Tests/Integration/ReflectionMethodScannerIntegrationTests.cs @@ -39,7 +39,7 @@ public void ExtractMethods_FromSampleConsoleApp_FindsExpectedMethods() // Arrange string assemblyPath = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, - "..", "..", "..", "TestFixtures", "SampleConsoleApp", "bin", "Debug", "net9.0", "SampleConsoleApp.dll"); + "..", "..", "..", "TestFixtures", "SampleConsoleApp", "bin", "Debug", "net10.0", "SampleConsoleApp.dll"); // Ensure the sample app is built if (!File.Exists(assemblyPath)) @@ -85,7 +85,7 @@ public void ExtractMethods_FromSampleAsyncApp_FindsAsyncMethods() // Arrange string assemblyPath = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, - "..", "..", "..", "TestFixtures", "SampleAsyncApp", "bin", "Debug", "net9.0", "SampleAsyncApp.dll"); + "..", "..", "..", "TestFixtures", "SampleAsyncApp", "bin", "Debug", "net10.0", "SampleAsyncApp.dll"); if (!File.Exists(assemblyPath)) { @@ -120,7 +120,7 @@ public void ExtractMethods_FromSampleInheritanceApp_HandlesInheritanceCorrectly( // Arrange string assemblyPath = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, - "..", "..", "..", "TestFixtures", "SampleInheritanceApp", "bin", "Debug", "net9.0", "SampleInheritanceApp.dll"); + "..", "..", "..", "TestFixtures", "SampleInheritanceApp", "bin", "Debug", "net10.0", "SampleInheritanceApp.dll"); if (!File.Exists(assemblyPath)) { @@ -159,7 +159,7 @@ public void ExtractMethods_ExcludesCompilerGeneratedMethods() // Arrange string assemblyPath = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, - "..", "..", "..", "TestFixtures", "SampleAsyncApp", "bin", "Debug", "net9.0", "SampleAsyncApp.dll"); + "..", "..", "..", "TestFixtures", "SampleAsyncApp", "bin", "Debug", "net10.0", "SampleAsyncApp.dll"); if (!File.Exists(assemblyPath)) { @@ -188,8 +188,8 @@ public void ExtractMethods_HandlesMultipleAssemblies() // Arrange string[] assemblyPaths = new[] { - Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "TestFixtures", "SampleConsoleApp", "bin", "Debug", "net9.0", "SampleConsoleApp.dll"), - Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "TestFixtures", "SampleAsyncApp", "bin", "Debug", "net9.0", "SampleAsyncApp.dll") + Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "TestFixtures", "SampleConsoleApp", "bin", "Debug", "net10.0", "SampleConsoleApp.dll"), + Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "TestFixtures", "SampleAsyncApp", "bin", "Debug", "net10.0", "SampleAsyncApp.dll") }.Where(File.Exists).ToArray(); if (assemblyPaths.Length < 2) diff --git a/Solutions/DeadCode.Tests/TestFixtures/SampleAsyncApp/SampleAsyncApp.csproj b/Solutions/DeadCode.Tests/TestFixtures/SampleAsyncApp/SampleAsyncApp.csproj index 3477df0..e3b6154 100644 --- a/Solutions/DeadCode.Tests/TestFixtures/SampleAsyncApp/SampleAsyncApp.csproj +++ b/Solutions/DeadCode.Tests/TestFixtures/SampleAsyncApp/SampleAsyncApp.csproj @@ -2,9 +2,6 @@ Exe - net9.0 - enable - enable \ No newline at end of file diff --git a/Solutions/DeadCode.Tests/TestFixtures/SampleConsoleApp/SampleConsoleApp.csproj b/Solutions/DeadCode.Tests/TestFixtures/SampleConsoleApp/SampleConsoleApp.csproj index 3477df0..e3b6154 100644 --- a/Solutions/DeadCode.Tests/TestFixtures/SampleConsoleApp/SampleConsoleApp.csproj +++ b/Solutions/DeadCode.Tests/TestFixtures/SampleConsoleApp/SampleConsoleApp.csproj @@ -2,9 +2,6 @@ Exe - net9.0 - enable - enable \ No newline at end of file diff --git a/Solutions/DeadCode.Tests/TestFixtures/SampleInheritanceApp/SampleInheritanceApp.csproj b/Solutions/DeadCode.Tests/TestFixtures/SampleInheritanceApp/SampleInheritanceApp.csproj index 3477df0..e3b6154 100644 --- a/Solutions/DeadCode.Tests/TestFixtures/SampleInheritanceApp/SampleInheritanceApp.csproj +++ b/Solutions/DeadCode.Tests/TestFixtures/SampleInheritanceApp/SampleInheritanceApp.csproj @@ -2,9 +2,6 @@ Exe - net9.0 - enable - enable \ No newline at end of file diff --git a/Solutions/DeadCode.sln b/Solutions/DeadCode.sln deleted file mode 100644 index 5a1de72..0000000 --- a/Solutions/DeadCode.sln +++ /dev/null @@ -1,65 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeadCode", "DeadCode\DeadCode.csproj", "{DC40DA0B-27F1-42A1-AC6E-1E0BA340D0D8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeadCode.Tests", "DeadCode.Tests\DeadCode.Tests.csproj", "{FE2CC271-3D17-4B67-A908-1B9CAAF8A94D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleAppWithDeadCode", "Samples\SampleAppWithDeadCode\SampleAppWithDeadCode.csproj", "{1B9FD385-C69E-E39A-FE02-33A3BDF02543}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {DC40DA0B-27F1-42A1-AC6E-1E0BA340D0D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DC40DA0B-27F1-42A1-AC6E-1E0BA340D0D8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DC40DA0B-27F1-42A1-AC6E-1E0BA340D0D8}.Debug|x64.ActiveCfg = Debug|Any CPU - {DC40DA0B-27F1-42A1-AC6E-1E0BA340D0D8}.Debug|x64.Build.0 = Debug|Any CPU - {DC40DA0B-27F1-42A1-AC6E-1E0BA340D0D8}.Debug|x86.ActiveCfg = Debug|Any CPU - {DC40DA0B-27F1-42A1-AC6E-1E0BA340D0D8}.Debug|x86.Build.0 = Debug|Any CPU - {DC40DA0B-27F1-42A1-AC6E-1E0BA340D0D8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DC40DA0B-27F1-42A1-AC6E-1E0BA340D0D8}.Release|Any CPU.Build.0 = Release|Any CPU - {DC40DA0B-27F1-42A1-AC6E-1E0BA340D0D8}.Release|x64.ActiveCfg = Release|Any CPU - {DC40DA0B-27F1-42A1-AC6E-1E0BA340D0D8}.Release|x64.Build.0 = Release|Any CPU - {DC40DA0B-27F1-42A1-AC6E-1E0BA340D0D8}.Release|x86.ActiveCfg = Release|Any CPU - {DC40DA0B-27F1-42A1-AC6E-1E0BA340D0D8}.Release|x86.Build.0 = Release|Any CPU - {FE2CC271-3D17-4B67-A908-1B9CAAF8A94D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FE2CC271-3D17-4B67-A908-1B9CAAF8A94D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FE2CC271-3D17-4B67-A908-1B9CAAF8A94D}.Debug|x64.ActiveCfg = Debug|Any CPU - {FE2CC271-3D17-4B67-A908-1B9CAAF8A94D}.Debug|x64.Build.0 = Debug|Any CPU - {FE2CC271-3D17-4B67-A908-1B9CAAF8A94D}.Debug|x86.ActiveCfg = Debug|Any CPU - {FE2CC271-3D17-4B67-A908-1B9CAAF8A94D}.Debug|x86.Build.0 = Debug|Any CPU - {FE2CC271-3D17-4B67-A908-1B9CAAF8A94D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FE2CC271-3D17-4B67-A908-1B9CAAF8A94D}.Release|Any CPU.Build.0 = Release|Any CPU - {FE2CC271-3D17-4B67-A908-1B9CAAF8A94D}.Release|x64.ActiveCfg = Release|Any CPU - {FE2CC271-3D17-4B67-A908-1B9CAAF8A94D}.Release|x64.Build.0 = Release|Any CPU - {FE2CC271-3D17-4B67-A908-1B9CAAF8A94D}.Release|x86.ActiveCfg = Release|Any CPU - {FE2CC271-3D17-4B67-A908-1B9CAAF8A94D}.Release|x86.Build.0 = Release|Any CPU - {1B9FD385-C69E-E39A-FE02-33A3BDF02543}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1B9FD385-C69E-E39A-FE02-33A3BDF02543}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1B9FD385-C69E-E39A-FE02-33A3BDF02543}.Debug|x64.ActiveCfg = Debug|Any CPU - {1B9FD385-C69E-E39A-FE02-33A3BDF02543}.Debug|x64.Build.0 = Debug|Any CPU - {1B9FD385-C69E-E39A-FE02-33A3BDF02543}.Debug|x86.ActiveCfg = Debug|Any CPU - {1B9FD385-C69E-E39A-FE02-33A3BDF02543}.Debug|x86.Build.0 = Debug|Any CPU - {1B9FD385-C69E-E39A-FE02-33A3BDF02543}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1B9FD385-C69E-E39A-FE02-33A3BDF02543}.Release|Any CPU.Build.0 = Release|Any CPU - {1B9FD385-C69E-E39A-FE02-33A3BDF02543}.Release|x64.ActiveCfg = Release|Any CPU - {1B9FD385-C69E-E39A-FE02-33A3BDF02543}.Release|x64.Build.0 = Release|Any CPU - {1B9FD385-C69E-E39A-FE02-33A3BDF02543}.Release|x86.ActiveCfg = Release|Any CPU - {1B9FD385-C69E-E39A-FE02-33A3BDF02543}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {CA1ADB48-2C9C-4507-B790-36FCB4DC2DE3} - EndGlobalSection -EndGlobal diff --git a/Solutions/DeadCode.slnx b/Solutions/DeadCode.slnx new file mode 100644 index 0000000..38cc313 --- /dev/null +++ b/Solutions/DeadCode.slnx @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Solutions/DeadCode/CLI/Commands/AnalyzeCommand.cs b/Solutions/DeadCode/CLI/Commands/AnalyzeCommand.cs index 1a38420..b21ea69 100644 --- a/Solutions/DeadCode/CLI/Commands/AnalyzeCommand.cs +++ b/Solutions/DeadCode/CLI/Commands/AnalyzeCommand.cs @@ -81,7 +81,7 @@ public override ValidationResult Validate() } } - public override async Task ExecuteAsync(CommandContext context, Settings settings) + public override async Task ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellation) { logger.LogInformation("Starting redundancy analysis"); @@ -95,7 +95,9 @@ public override async Task ExecuteAsync(CommandContext context, Settings se ctx.SpinnerStyle(Style.Parse("green")); }); - MethodInventory inventory = await LoadInventoryAsync(settings.InventoryPath); + cancellation.ThrowIfCancellationRequested(); + + MethodInventory inventory = await LoadInventoryAsync(settings.InventoryPath, cancellation); console.MarkupLine($"[green]✓[/] Loaded [blue]{inventory.Count}[/] methods from inventory"); // Find and parse trace files @@ -124,6 +126,8 @@ await console.Progress() foreach (string traceFile in traceFiles) { + cancellation.ThrowIfCancellationRequested(); + task.Description = $"[green]Parsing {Path.GetFileName(traceFile)}[/]"; HashSet methods = await traceParser.ParseTraceAsync(traceFile); @@ -171,16 +175,11 @@ await console.Status() } } - private async Task LoadInventoryAsync(string path) + private async Task LoadInventoryAsync(string path, CancellationToken cancellationToken = default) { - string json = await File.ReadAllTextAsync(path); - JsonSerializerOptions options = new() - { - PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase, - Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() } - }; + string json = await File.ReadAllTextAsync(path, cancellationToken); - return JsonSerializer.Deserialize(json, options) + return JsonSerializer.Deserialize(json, Infrastructure.IO.JsonOptions.ReadWrite) ?? throw new InvalidOperationException("Failed to deserialize inventory"); } diff --git a/Solutions/DeadCode/CLI/Commands/ExtractCommand.cs b/Solutions/DeadCode/CLI/Commands/ExtractCommand.cs index 5d7ff29..ecc7d0c 100644 --- a/Solutions/DeadCode/CLI/Commands/ExtractCommand.cs +++ b/Solutions/DeadCode/CLI/Commands/ExtractCommand.cs @@ -53,7 +53,7 @@ public override ValidationResult Validate() } } - public override async Task ExecuteAsync(CommandContext context, Settings settings) + public override async Task ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellation) { logger.LogInformation("Starting method inventory extraction"); @@ -92,8 +92,10 @@ await console.Progress() task.StopTask(); + cancellation.ThrowIfCancellationRequested(); + // Save inventory to JSON - await SaveInventoryAsync(inventory, settings.OutputPath); + await SaveInventoryAsync(inventory, settings.OutputPath, cancellation); // Display summary DisplaySummary(inventory); @@ -112,16 +114,11 @@ await console.Progress() return result; } - private async Task SaveInventoryAsync(MethodInventory inventory, string outputPath) + private async Task SaveInventoryAsync(MethodInventory inventory, string outputPath, CancellationToken cancellation = default) { - string json = System.Text.Json.JsonSerializer.Serialize(inventory, new System.Text.Json.JsonSerializerOptions - { - WriteIndented = true, - PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase, - Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() } - }); + string json = System.Text.Json.JsonSerializer.Serialize(inventory, Infrastructure.IO.JsonOptions.ReadWrite); - await File.WriteAllTextAsync(outputPath, json); + await File.WriteAllTextAsync(outputPath, json, cancellation); console.MarkupLine($"[green]✓[/] Inventory saved to [blue]{outputPath}[/]"); } diff --git a/Solutions/DeadCode/CLI/Commands/FullCommand.cs b/Solutions/DeadCode/CLI/Commands/FullCommand.cs index 6a66553..1e647e7 100644 --- a/Solutions/DeadCode/CLI/Commands/FullCommand.cs +++ b/Solutions/DeadCode/CLI/Commands/FullCommand.cs @@ -80,7 +80,7 @@ public override ValidationResult Validate() } } - public override async Task ExecuteAsync(CommandContext context, Settings settings) + public override async Task ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellation) { logger.LogInformation("Starting full deadcode analysis pipeline"); @@ -106,13 +106,15 @@ public override async Task ExecuteAsync(CommandContext context, Settings se IncludeGenerated = false }; - int extractResult = await extractCommand.ExecuteAsync(context, extractSettings); + int extractResult = await extractCommand.ExecuteAsync(context, extractSettings, cancellation); if (extractResult != 0) { console.MarkupLine("[red]Failed to extract method inventory[/]"); return extractResult; } + cancellation.ThrowIfCancellationRequested(); + // Step 2: Profile application console.MarkupLine("\n[bold]Step 2:[/] Profiling application execution"); @@ -124,12 +126,14 @@ public override async Task ExecuteAsync(CommandContext context, Settings se OutputDirectory = tracesDirectory }; - int profileResult = await profileCommand.ExecuteAsync(context, profileSettings); + int profileResult = await profileCommand.ExecuteAsync(context, profileSettings, cancellation); if (profileResult != 0) { console.MarkupLine("[yellow]Warning: Some profiling scenarios failed[/]"); } + cancellation.ThrowIfCancellationRequested(); + // Step 3: Analyze results console.MarkupLine("\n[bold]Step 3:[/] Analyzing for unused code"); @@ -142,7 +146,7 @@ public override async Task ExecuteAsync(CommandContext context, Settings se MinConfidence = settings.MinConfidence }; - int analyzeResult = await analyzeCommand.ExecuteAsync(context, analyzeSettings); + int analyzeResult = await analyzeCommand.ExecuteAsync(context, analyzeSettings, cancellation); if (analyzeResult != 0) { console.MarkupLine("[red]Failed to analyze redundancy[/]"); diff --git a/Solutions/DeadCode/CLI/Commands/ProfileCommand.cs b/Solutions/DeadCode/CLI/Commands/ProfileCommand.cs index 631665e..d32059a 100644 --- a/Solutions/DeadCode/CLI/Commands/ProfileCommand.cs +++ b/Solutions/DeadCode/CLI/Commands/ProfileCommand.cs @@ -73,11 +73,16 @@ public override ValidationResult Validate() return ValidationResult.Error($"Scenarios file not found: {ScenariosPath}"); } + if (Duration is <= 0) + { + return ValidationResult.Error("Duration must be a positive number of seconds"); + } + return ValidationResult.Success(); } } - public override async Task ExecuteAsync(CommandContext context, Settings settings) + public override async Task ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellation) { logger.LogInformation("Starting profiling session"); @@ -88,7 +93,7 @@ public override async Task ExecuteAsync(CommandContext context, Settings se } // Load scenarios or create default - List scenarios = await LoadScenariosAsync(settings); + List scenarios = await LoadScenariosAsync(settings, cancellation); List results = []; @@ -106,6 +111,8 @@ await console.Progress() foreach (ProfilingScenario scenario in scenarios) { + cancellation.ThrowIfCancellationRequested(); + task.Description = $"[green]Profiling: {scenario.Name}[/]"; try @@ -180,11 +187,11 @@ private async Task VerifyDependenciesAsync() return true; } - private async Task> LoadScenariosAsync(Settings settings) + private async Task> LoadScenariosAsync(Settings settings, CancellationToken cancellationToken = default) { if (settings.ScenariosPath != null) { - string json = await File.ReadAllTextAsync(settings.ScenariosPath); + string json = await File.ReadAllTextAsync(settings.ScenariosPath, cancellationToken); System.Text.Json.JsonSerializerOptions options = new() { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase diff --git a/Solutions/DeadCode/Core/Models/MethodInventory.cs b/Solutions/DeadCode/Core/Models/MethodInventory.cs index fb96041..8ce08b5 100644 --- a/Solutions/DeadCode/Core/Models/MethodInventory.cs +++ b/Solutions/DeadCode/Core/Models/MethodInventory.cs @@ -8,6 +8,7 @@ namespace DeadCode.Core.Models; public class MethodInventory { private readonly List methods = []; + private IReadOnlyDictionary>? cachedMethodsByAssembly; /// /// Gets or sets all methods in the inventory (for JSON serialization) @@ -28,7 +29,7 @@ public List Methods /// Gets methods grouped by assembly name /// public IReadOnlyDictionary> MethodsByAssembly => - methods.GroupBy(m => m.AssemblyName).ToDictionary(g => g.Key, g => g.ToList()); + cachedMethodsByAssembly ??= methods.GroupBy(m => m.AssemblyName).ToDictionary(g => g.Key, g => g.ToList()); /// /// Adds a method to the inventory @@ -37,6 +38,7 @@ public void AddMethod(MethodInfo method) { ArgumentNullException.ThrowIfNull(method); methods.Add(method); + cachedMethodsByAssembly = null; } /// @@ -46,6 +48,7 @@ public void AddMethods(IEnumerable methods) { ArgumentNullException.ThrowIfNull(methods); this.methods.AddRange(methods); + cachedMethodsByAssembly = null; } /// diff --git a/Solutions/DeadCode/Core/Models/RedundancyReport.cs b/Solutions/DeadCode/Core/Models/RedundancyReport.cs index 278d9ce..47db94a 100644 --- a/Solutions/DeadCode/Core/Models/RedundancyReport.cs +++ b/Solutions/DeadCode/Core/Models/RedundancyReport.cs @@ -6,6 +6,7 @@ namespace DeadCode.Core.Models; public class RedundancyReport { private readonly List unusedMethods = []; + private IReadOnlyDictionary>? cachedMethodsBySafety; /// /// Gets the timestamp when the report was generated @@ -30,27 +31,25 @@ public class RedundancyReport /// /// Gets unused methods grouped by safety classification /// - public IReadOnlyDictionary> MethodsBySafety => - unusedMethods.GroupBy(um => um.Method.SafetyLevel) - .ToDictionary(g => g.Key, g => g.ToList()); + public IReadOnlyDictionary> MethodsBySafety => GetOrBuildSafetyCache(); /// /// Gets high confidence unused methods (safe to remove) /// public IEnumerable HighConfidenceMethods => - unusedMethods.Where(um => um.Method.SafetyLevel == SafetyClassification.HighConfidence); + GetOrBuildSafetyCache().TryGetValue(SafetyClassification.HighConfidence, out var list) ? list : []; /// /// Gets medium confidence unused methods (requires review) /// public IEnumerable MediumConfidenceMethods => - unusedMethods.Where(um => um.Method.SafetyLevel == SafetyClassification.MediumConfidence); + GetOrBuildSafetyCache().TryGetValue(SafetyClassification.MediumConfidence, out var list) ? list : []; /// /// Gets low confidence unused methods (likely false positives) /// public IEnumerable LowConfidenceMethods => - unusedMethods.Where(um => um.Method.SafetyLevel == SafetyClassification.LowConfidence); + GetOrBuildSafetyCache().TryGetValue(SafetyClassification.LowConfidence, out var list) ? list : []; /// /// Adds an unused method to the report @@ -59,6 +58,7 @@ public void AddUnusedMethod(UnusedMethod method) { ArgumentNullException.ThrowIfNull(method); unusedMethods.Add(method); + cachedMethodsBySafety = null; } /// @@ -68,8 +68,13 @@ public void AddUnusedMethods(IEnumerable methods) { ArgumentNullException.ThrowIfNull(methods); unusedMethods.AddRange(methods); + cachedMethodsBySafety = null; } + private IReadOnlyDictionary> GetOrBuildSafetyCache() => + cachedMethodsBySafety ??= unusedMethods.GroupBy(um => um.Method.SafetyLevel) + .ToDictionary(g => g.Key, g => g.ToList()); + /// /// Gets summary statistics for the report /// diff --git a/Solutions/DeadCode/Core/Models/UnusedMethod.cs b/Solutions/DeadCode/Core/Models/UnusedMethod.cs index 59c6221..94eb08e 100644 --- a/Solutions/DeadCode/Core/Models/UnusedMethod.cs +++ b/Solutions/DeadCode/Core/Models/UnusedMethod.cs @@ -5,7 +5,7 @@ namespace DeadCode.Core.Models; /// public record UnusedMethod( MethodInfo Method, - List Dependencies + IReadOnlyList Dependencies ) { /// diff --git a/Solutions/DeadCode/DeadCode.csproj b/Solutions/DeadCode/DeadCode.csproj index f5f09c0..d61331f 100644 --- a/Solutions/DeadCode/DeadCode.csproj +++ b/Solutions/DeadCode/DeadCode.csproj @@ -2,9 +2,6 @@ Exe - net9.0 - enable - enable true deadcode @@ -25,12 +22,12 @@ - - - - - - + + + + + + diff --git a/Solutions/DeadCode/Infrastructure/IO/ComparisonEngine.cs b/Solutions/DeadCode/Infrastructure/IO/ComparisonEngine.cs index a821afb..8eb42df 100644 --- a/Solutions/DeadCode/Infrastructure/IO/ComparisonEngine.cs +++ b/Solutions/DeadCode/Infrastructure/IO/ComparisonEngine.cs @@ -20,6 +20,9 @@ public ComparisonEngine(ILogger logger) public Task CompareAsync(MethodInventory inventory, HashSet executedMethods) { + ArgumentNullException.ThrowIfNull(inventory); + ArgumentNullException.ThrowIfNull(executedMethods); + logger.LogInformation( "Comparing {InventoryCount} methods against {ExecutedCount} executed methods", inventory.Count, executedMethods.Count); @@ -31,16 +34,18 @@ public Task CompareAsync(MethodInventory inventory, HashSet executedMethods) { + ArgumentNullException.ThrowIfNull(inventory); + ArgumentNullException.ThrowIfNull(executedMethods); + RedundancyReport report = new() { AnalyzedAssemblies = inventory.MethodsByAssembly.Keys.ToList(), TraceScenarios = ["default"] }; - // Create a case-insensitive set for comparison + // Create a lowercase set for comparison HashSet executedMethodsLower = new( - executedMethods.Select(m => m.ToLowerInvariant()), - StringComparer.OrdinalIgnoreCase + executedMethods.Select(m => m.ToLowerInvariant()) ); diff --git a/Solutions/DeadCode/Infrastructure/IO/JsonOptions.cs b/Solutions/DeadCode/Infrastructure/IO/JsonOptions.cs new file mode 100644 index 0000000..274eb76 --- /dev/null +++ b/Solutions/DeadCode/Infrastructure/IO/JsonOptions.cs @@ -0,0 +1,14 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace DeadCode.Infrastructure.IO; + +public static class JsonOptions +{ + public static readonly JsonSerializerOptions ReadWrite = new() + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() } + }; +} diff --git a/Solutions/DeadCode/Infrastructure/IO/JsonReportGenerator.cs b/Solutions/DeadCode/Infrastructure/IO/JsonReportGenerator.cs index 2723537..5a33153 100644 --- a/Solutions/DeadCode/Infrastructure/IO/JsonReportGenerator.cs +++ b/Solutions/DeadCode/Infrastructure/IO/JsonReportGenerator.cs @@ -22,6 +22,9 @@ public JsonReportGenerator(ILogger logger) public async Task GenerateAsync(RedundancyReport report, string outputPath) { + ArgumentNullException.ThrowIfNull(report); + ArgumentException.ThrowIfNullOrEmpty(outputPath); + logger.LogInformation("Generating JSON report to {Path}", outputPath); JsonSerializerOptions options = new() @@ -33,36 +36,9 @@ public async Task GenerateAsync(RedundancyReport report, string outputPath) // Create minimal LLM-ready output - only include methods with source locations var output = new { - highConfidence = report.HighConfidenceMethods - .Where(m => m.FilePath != null && m.LineNumber != null) - .Select(m => new - { - file = m.FilePath, - line = m.LineNumber, - method = m.Method.MethodName, - dependencies = m.Dependencies - }) - .ToList(), // Force evaluation - mediumConfidence = report.MediumConfidenceMethods - .Where(m => m.FilePath != null && m.LineNumber != null) - .Select(m => new - { - file = m.FilePath, - line = m.LineNumber, - method = m.Method.MethodName, - dependencies = m.Dependencies - }) - .ToList(), // Force evaluation - lowConfidence = report.LowConfidenceMethods - .Where(m => m.FilePath != null && m.LineNumber != null) - .Select(m => new - { - file = m.FilePath, - line = m.LineNumber, - method = m.Method.MethodName, - dependencies = m.Dependencies - }) - .ToList() // Force evaluation + highConfidence = FormatMethods(report.HighConfidenceMethods), + mediumConfidence = FormatMethods(report.MediumConfidenceMethods), + lowConfidence = FormatMethods(report.LowConfidenceMethods) }; string json = JsonSerializer.Serialize(output, options); @@ -70,4 +46,16 @@ public async Task GenerateAsync(RedundancyReport report, string outputPath) logger.LogInformation("Report generated successfully"); } + + private static List FormatMethods(IEnumerable methods) => + methods + .Where(m => m.FilePath != null && m.LineNumber != null) + .Select(m => (object)new + { + file = m.FilePath, + line = m.LineNumber, + method = m.Method.MethodName, + dependencies = m.Dependencies + }) + .ToList(); } \ No newline at end of file diff --git a/Solutions/DeadCode/Infrastructure/Profiling/DotnetTraceVerifier.cs b/Solutions/DeadCode/Infrastructure/Profiling/DotnetTraceVerifier.cs index e6ddf82..b19db29 100644 --- a/Solutions/DeadCode/Infrastructure/Profiling/DotnetTraceVerifier.cs +++ b/Solutions/DeadCode/Infrastructure/Profiling/DotnetTraceVerifier.cs @@ -25,7 +25,7 @@ public async Task CheckDependenciesAsync() { try { - Process process = new() + using Process process = new() { StartInfo = new ProcessStartInfo { @@ -68,7 +68,7 @@ public async Task InstallMissingDependenciesAsync() { AnsiConsole.MarkupLine("[yellow]Installing dotnet-trace...[/]"); - Process process = new() + using Process process = new() { StartInfo = new ProcessStartInfo { diff --git a/Solutions/DeadCode/Infrastructure/Profiling/TraceParser.cs b/Solutions/DeadCode/Infrastructure/Profiling/TraceParser.cs index 75b620e..cf4fa39 100644 --- a/Solutions/DeadCode/Infrastructure/Profiling/TraceParser.cs +++ b/Solutions/DeadCode/Infrastructure/Profiling/TraceParser.cs @@ -265,8 +265,9 @@ private async Task IsBinaryTraceFileAsync(string filePath) return false; } - catch + catch (IOException ex) { + logger.LogDebug(ex, "Could not read trace file header: {Path}", filePath); return false; } } diff --git a/Solutions/DeadCode/Infrastructure/Reflection/PdbReader.cs b/Solutions/DeadCode/Infrastructure/Reflection/PdbReader.cs index 507f22a..15a76e9 100644 --- a/Solutions/DeadCode/Infrastructure/Reflection/PdbReader.cs +++ b/Solutions/DeadCode/Infrastructure/Reflection/PdbReader.cs @@ -25,7 +25,7 @@ public PdbReader(ILogger logger) public Task GetSourceLocationAsync(MethodBase method, string assemblyPath) { ArgumentNullException.ThrowIfNull(method); - if (string.IsNullOrEmpty(assemblyPath)) throw new ArgumentException("Assembly path cannot be null or empty", nameof(assemblyPath)); + ArgumentException.ThrowIfNullOrEmpty(assemblyPath); string pdbPath = Path.ChangeExtension(assemblyPath, ".pdb"); if (!File.Exists(pdbPath)) diff --git a/Solutions/DeadCode/Program.cs b/Solutions/DeadCode/Program.cs index a53d0c6..7c04034 100644 --- a/Solutions/DeadCode/Program.cs +++ b/Solutions/DeadCode/Program.cs @@ -38,11 +38,8 @@ services.AddSingleton(); services.AddSingleton(); -// Build service provider -ServiceProvider serviceProvider = services.BuildServiceProvider(); - -// Create type registrar for Spectre.Console.Cli -TypeRegistrar registrar = new(serviceProvider); +// Create type registrar for Spectre.Console.Cli (defers BuildServiceProvider until Build()) +TypeRegistrar registrar = new(services); // Create and configure the CLI app CommandApp app = new(registrar); @@ -54,7 +51,7 @@ // Add commands config.AddCommand("extract") .WithDescription("Extract method inventory from assemblies") - .WithExample("extract", "bin/Release/net9.0/*.dll", "-o", "inventory.json"); + .WithExample("extract", "bin/Release/net10.0/*.dll", "-o", "inventory.json"); config.AddCommand("profile") .WithDescription("Profile application execution") @@ -66,7 +63,7 @@ config.AddCommand("full") .WithDescription("Run complete analysis pipeline") - .WithExample("full", "--assemblies", "bin/Release/net9.0/*.dll", "--executable", "MyApp.exe"); + .WithExample("full", "--assemblies", "bin/Release/net10.0/*.dll", "--executable", "MyApp.exe"); // Configure help config.ValidateExamples(); @@ -78,32 +75,32 @@ // Type registrar implementation for DI public sealed class TypeRegistrar : ITypeRegistrar { - private readonly IServiceProvider serviceProvider; + private readonly IServiceCollection services; - public TypeRegistrar(IServiceProvider serviceProvider) + public TypeRegistrar(IServiceCollection services) { - ArgumentNullException.ThrowIfNull(serviceProvider); - this.serviceProvider = serviceProvider; + ArgumentNullException.ThrowIfNull(services); + this.services = services; } public ITypeResolver Build() { - return new TypeResolver(serviceProvider); + return new TypeResolver(services.BuildServiceProvider()); } public void Register(Type service, Type implementation) { - // Not needed for our use case + services.AddSingleton(service, implementation); } public void RegisterInstance(Type service, object implementation) { - // Not needed for our use case + services.AddSingleton(service, implementation); } public void RegisterLazy(Type service, Func factory) { - // Not needed for our use case + services.AddSingleton(service, _ => factory()); } } diff --git a/Solutions/DeadCode/README.md b/Solutions/DeadCode/README.md index 9e81f4d..4dac395 100644 --- a/Solutions/DeadCode/README.md +++ b/Solutions/DeadCode/README.md @@ -1,118 +1,118 @@ -# DeadCode - -A .NET global tool that identifies unused code through static and dynamic analysis, generating LLM-ready cleanup plans. - -## Installation - -```bash -dotnet tool install --global DeadCode -``` - -## Quick Start - -```bash -# Build your project -dotnet build -c Release - -# Run full analysis -deadcode full --assemblies ./bin/Release/net9.0/*.dll --executable ./bin/Release/net9.0/MyApp.exe - -# View the generated report -cat analysis/report.json -``` - -## Features - -- **Static Analysis**: Extracts all methods from compiled assemblies -- **Dynamic Profiling**: Captures runtime execution data using dotnet-trace -- **Safety Classification**: Categorizes methods by removal safety -- **LLM-Ready Output**: Generates minimal JSON optimized for AI code cleanup -- **Beautiful CLI**: Rich terminal interface with progress indicators - -## Basic Usage - -### Extract method inventory -```bash -deadcode extract bin/Release/net9.0/*.dll -o inventory.json -``` - -Example `inventory.json`: -```json -{ - "assemblyName": "MyApp", - "methods": [ - { - "id": "MyApp.Services.DataService::ProcessData(System.String)", - "name": "ProcessData", - "declaringType": "MyApp.Services.DataService", - "visibility": "Public", - "sourceLocation": { - "file": "Services/DataService.cs", - "line": 45 - } - }, - { - "id": "MyApp.Helpers.StringHelper::FormatOutput(System.String)", - "name": "FormatOutput", - "declaringType": "MyApp.Helpers.StringHelper", - "visibility": "Private", - "sourceLocation": { - "file": "Helpers/StringHelper.cs", - "line": 12 - } - } - ] -} - -### Profile execution -```bash -deadcode profile MyApp.exe --args "arg1 arg2" -o traces/ -``` - -Or use scenarios for comprehensive testing: -```bash -deadcode profile MyApp.exe --scenarios scenarios.json -o traces/ -``` - -Example `scenarios.json`: -```json -{ - "scenarios": [ - { - "name": "basic-functionality", - "arguments": ["--help", "--verbose"], - "duration": 30, - "description": "Test help and basic commands" - }, - { - "name": "data-processing", - "arguments": ["process", "--input", "data.csv", "--output", "results.json"], - "description": "Test main data processing workflow" - }, - { - "name": "api-endpoints", - "arguments": ["serve", "--port", "8080"], - "duration": 60, - "description": "Test API server with various endpoints" - } - ] -} - -### Analyze for unused code -```bash -deadcode analyze -i inventory.json -t traces/ -o report.json -``` - -## Documentation - -For detailed documentation, examples, and advanced usage, visit: -https://github.com/endjin/deadcode - -## License - -Apache License 2.0 - Copyright © 2024 Endjin Limited - -## Requirements - -- .NET 9.0 SDK or later +# DeadCode + +A .NET global tool that identifies unused code through static and dynamic analysis, generating LLM-ready cleanup plans. + +## Installation + +```bash +dotnet tool install --global DeadCode +``` + +## Quick Start + +```bash +# Build your project +dotnet build -c Release + +# Run full analysis +deadcode full --assemblies ./bin/Release/net10.0/*.dll --executable ./bin/Release/net10.0/MyApp.exe + +# View the generated report +cat analysis/report.json +``` + +## Features + +- **Static Analysis**: Extracts all methods from compiled assemblies +- **Dynamic Profiling**: Captures runtime execution data using dotnet-trace +- **Safety Classification**: Categorizes methods by removal safety +- **LLM-Ready Output**: Generates minimal JSON optimized for AI code cleanup +- **Beautiful CLI**: Rich terminal interface with progress indicators + +## Basic Usage + +### Extract method inventory +```bash +deadcode extract bin/Release/net10.0/*.dll -o inventory.json +``` + +Example `inventory.json`: +```json +{ + "assemblyName": "MyApp", + "methods": [ + { + "id": "MyApp.Services.DataService::ProcessData(System.String)", + "name": "ProcessData", + "declaringType": "MyApp.Services.DataService", + "visibility": "Public", + "sourceLocation": { + "file": "Services/DataService.cs", + "line": 45 + } + }, + { + "id": "MyApp.Helpers.StringHelper::FormatOutput(System.String)", + "name": "FormatOutput", + "declaringType": "MyApp.Helpers.StringHelper", + "visibility": "Private", + "sourceLocation": { + "file": "Helpers/StringHelper.cs", + "line": 12 + } + } + ] +} + +### Profile execution +```bash +deadcode profile MyApp.exe --args "arg1 arg2" -o traces/ +``` + +Or use scenarios for comprehensive testing: +```bash +deadcode profile MyApp.exe --scenarios scenarios.json -o traces/ +``` + +Example `scenarios.json`: +```json +{ + "scenarios": [ + { + "name": "basic-functionality", + "arguments": ["--help", "--verbose"], + "duration": 30, + "description": "Test help and basic commands" + }, + { + "name": "data-processing", + "arguments": ["process", "--input", "data.csv", "--output", "results.json"], + "description": "Test main data processing workflow" + }, + { + "name": "api-endpoints", + "arguments": ["serve", "--port", "8080"], + "duration": 60, + "description": "Test API server with various endpoints" + } + ] +} + +### Analyze for unused code +```bash +deadcode analyze -i inventory.json -t traces/ -o report.json +``` + +## Documentation + +For detailed documentation, examples, and advanced usage, visit: +https://github.com/endjin/deadcode + +## License + +Apache License 2.0 - Copyright © 2024 Endjin Limited + +## Requirements + +- .NET 10.0 SDK or later - Windows, Linux, or macOS \ No newline at end of file diff --git a/Solutions/Directory.Build.props b/Solutions/Directory.Build.props new file mode 100644 index 0000000..f3fc445 --- /dev/null +++ b/Solutions/Directory.Build.props @@ -0,0 +1,7 @@ + + + net10.0 + enable + enable + + diff --git a/Solutions/Samples/SampleAppWithDeadCode/SampleAppWithDeadCode.csproj b/Solutions/Samples/SampleAppWithDeadCode/SampleAppWithDeadCode.csproj index 3750ad2..d74e73b 100644 --- a/Solutions/Samples/SampleAppWithDeadCode/SampleAppWithDeadCode.csproj +++ b/Solutions/Samples/SampleAppWithDeadCode/SampleAppWithDeadCode.csproj @@ -2,9 +2,6 @@ Exe - net9.0 - enable - enable false diff --git a/Solutions/demo.ps1 b/Solutions/demo.ps1 index e1a476a..2d93a1f 100644 --- a/Solutions/demo.ps1 +++ b/Solutions/demo.ps1 @@ -30,8 +30,8 @@ Write-Host "✓ Build complete" -ForegroundColor Green # Step 2: Extract method inventory Write-Host "`nStep 2: Extracting method inventory through static analysis" -ForegroundColor Green Write-Host "This identifies all methods in the compiled assemblies..." -dotnet DeadCode/bin/Release/net9.0/DeadCode.dll extract ` - "$SAMPLE_APP/bin/Release/net9.0/*.dll" ` +dotnet DeadCode/bin/Release/net10.0/DeadCode.dll extract ` + "$SAMPLE_APP/bin/Release/net10.0/*.dll" ` -o "$DEMO_DIR/inventory.json" Write-Host "✓ Method inventory extracted" -ForegroundColor Green $methodCount = (Get-Content "$DEMO_DIR/inventory.json" | ConvertFrom-Json).methods.Count @@ -40,8 +40,8 @@ Write-Host "Found methods: $methodCount" # Step 3: Profile application execution Write-Host "`nStep 3: Profiling application execution with scenarios" -ForegroundColor Green Write-Host "This captures which methods are actually called at runtime..." -dotnet DeadCode/bin/Release/net9.0/DeadCode.dll profile ` - "$SAMPLE_APP/bin/Release/net9.0/SampleAppWithDeadCode" ` +dotnet DeadCode/bin/Release/net10.0/DeadCode.dll profile ` + "$SAMPLE_APP/bin/Release/net10.0/SampleAppWithDeadCode" ` --scenarios "$SAMPLE_APP/scenarios.json" ` -o "$DEMO_DIR/traces" Write-Host "✓ Profiling complete" -ForegroundColor Green @@ -49,7 +49,7 @@ Write-Host "✓ Profiling complete" -ForegroundColor Green # Step 4: Analyze to find unused code Write-Host "`nStep 4: Analyzing to identify unused code" -ForegroundColor Green Write-Host "Comparing static inventory against runtime execution..." -dotnet DeadCode/bin/Release/net9.0/DeadCode.dll analyze ` +dotnet DeadCode/bin/Release/net10.0/DeadCode.dll analyze ` -i "$DEMO_DIR/inventory.json" ` -t "$DEMO_DIR/traces" ` -o "$DEMO_DIR/report.json" ` @@ -93,7 +93,7 @@ Write-Host " Low confidence dead code: $lowConf" # Alternative: Run full pipeline in one command Write-Host "`nAlternative: Run complete pipeline with one command" -ForegroundColor Green Write-Host "You can also run the entire analysis with:" -Write-Host "dotnet DeadCode/bin/Release/net9.0/DeadCode.dll full --assemblies $SAMPLE_APP/bin/Release/net9.0/*.dll --executable $SAMPLE_APP/bin/Release/net9.0/SampleAppWithDeadCode" -ForegroundColor Blue +Write-Host "dotnet DeadCode/bin/Release/net10.0/DeadCode.dll full --assemblies $SAMPLE_APP/bin/Release/net10.0/*.dll --executable $SAMPLE_APP/bin/Release/net10.0/SampleAppWithDeadCode" -ForegroundColor Blue Write-Host "`nDemo complete!" -ForegroundColor Green Write-Host "Check the $DEMO_DIR directory for all generated files:" diff --git a/build.ps1 b/build.ps1 index eb4f988..bb95a74 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,121 +1,121 @@ -#Requires -Version 7 -<# -.SYNOPSIS - Runs a .NET flavoured build process. -.DESCRIPTION - This script was scaffolded using a template from the ZeroFailed project. - It uses the InvokeBuild module to orchestrate an opinionated software build process for .NET solutions. -.EXAMPLE - PS C:\> ./build.ps1 - Downloads any missing module dependencies (ZeroFailed & InvokeBuild) and executes - the build process. -.PARAMETER Tasks - Optionally override the default task executed as the entry-point of the build. -.PARAMETER Configuration - The build configuration, defaults to 'Release'. -.PARAMETER BuildRepositoryUri - Optional URI that supports pulling MSBuild logic from a web endpoint (e.g. a GitHub blob). -.PARAMETER SourcesDir - The path where the source code to be built is located, defaults to the current working directory. -.PARAMETER CoverageDir - The output path for the test coverage data, if run. -.PARAMETER TestReportTypes - The test report format that should be generated by the test report generator, if run. -.PARAMETER PackagesDir - The output path for any packages produced as part of the build. -.PARAMETER LogLevel - The logging verbosity. -.PARAMETER Clean - When true, the .NET solution will be cleaned and all output/intermediate folders deleted. -.PARAMETER ZfModulePath - The path to import the ZeroFailed module from. This is useful when testing pre-release - versions of ZeroFailed that are not yet available in the PowerShell Gallery. -.PARAMETER ZfModuleVersion - The version of the ZeroFailed module to import. This is useful when testing pre-release - versions of ZeroFailed that are not yet available in the PowerShell Gallery. -.PARAMETER InvokeBuildModuleVersion - The version of the InvokeBuild module to be used. -#> -[CmdletBinding()] -param ( - [Parameter(Position=0)] - [string[]] $Tasks = @("."), - - [Parameter()] - [string] $Configuration = "Debug", - - [Parameter()] - [string] $BuildRepositoryUri = "", - - [Parameter()] - [string] $SourcesDir = $PWD, - - [Parameter()] - [string] $CoverageDir = "_codeCoverage", - - [Parameter()] - [string] $TestReportTypes = "Cobertura", - - [Parameter()] - [string] $PackagesDir = "_packages", - - [Parameter()] - [ValidateSet("minimal","normal","detailed")] - [string] $LogLevel = "minimal", - - [Parameter()] - [switch] $Clean, - - [Parameter()] - [string] $ZfModulePath, - - [Parameter()] - [string] $ZfModuleVersion = "1.0.5", - - [Parameter()] - [version] $InvokeBuildModuleVersion = "5.12.1" -) -$ErrorActionPreference = 'Stop' -$here = Split-Path -Parent $PSCommandPath - -#region InvokeBuild setup -# This handles calling the build engine when this file is run like a normal PowerShell script -# (i.e. avoids the need to have another script to setup the InvokeBuild environment and issue the 'Invoke-Build' command ) -if ($MyInvocation.ScriptName -notlike '*Invoke-Build.ps1') { - Install-PSResource InvokeBuild -Version $InvokeBuildModuleVersion -Scope CurrentUser -TrustRepository -Verbose:$false | Out-Null - try { - Invoke-Build $Tasks $MyInvocation.MyCommand.Path @PSBoundParameters - } - catch { - Write-Host -f Yellow "`n`n***`n*** Build Failure Summary - check previous logs for more details`n***" - Write-Host -f Yellow $_.Exception.Message - Write-Host -f Yellow $_.ScriptStackTrace - exit 1 - } - return -} -#endregion - -#region Initialise build framework -$splat = @{ Force = $true; Verbose = $false} -Import-Module Microsoft.PowerShell.PSResourceGet -if (!($ZfModulePath)) { - Install-PSResource ZeroFailed -Version $ZfModuleVersion -Scope CurrentUser -TrustRepository | Out-Null - $ZfModulePath = "ZeroFailed" - $splat.Add("RequiredVersion", ($ZfModuleVersion -split '-')[0]) -} -else { - Write-Host "ZfModulePath: $ZfModulePath" -} -$splat.Add("Name", $ZfModulePath) -# Ensure only 1 version of the module is loaded -Get-Module ZeroFailed | Remove-Module -Import-Module @splat -$ver = "{0} {1}" -f (Get-Module ZeroFailed).Version, (Get-Module ZeroFailed).PrivateData.PsData.PreRelease -Write-Host "Using ZeroFailed module version: $ver" -#endregion - -$PSModuleAutoloadingPreference = 'none' - -# Load the build configuration -. $here/.zf/config.ps1 +#Requires -Version 7 +<# +.SYNOPSIS + Runs a .NET flavoured build process. +.DESCRIPTION + This script was scaffolded using a template from the ZeroFailed project. + It uses the InvokeBuild module to orchestrate an opinionated software build process for .NET solutions. +.EXAMPLE + PS C:\> ./build.ps1 + Downloads any missing module dependencies (ZeroFailed & InvokeBuild) and executes + the build process. +.PARAMETER Tasks + Optionally override the default task executed as the entry-point of the build. +.PARAMETER Configuration + The build configuration, defaults to 'Release'. +.PARAMETER BuildRepositoryUri + Optional URI that supports pulling MSBuild logic from a web endpoint (e.g. a GitHub blob). +.PARAMETER SourcesDir + The path where the source code to be built is located, defaults to the current working directory. +.PARAMETER CoverageDir + The output path for the test coverage data, if run. +.PARAMETER TestReportTypes + The test report format that should be generated by the test report generator, if run. +.PARAMETER PackagesDir + The output path for any packages produced as part of the build. +.PARAMETER LogLevel + The logging verbosity. +.PARAMETER Clean + When true, the .NET solution will be cleaned and all output/intermediate folders deleted. +.PARAMETER ZfModulePath + The path to import the ZeroFailed module from. This is useful when testing pre-release + versions of ZeroFailed that are not yet available in the PowerShell Gallery. +.PARAMETER ZfModuleVersion + The version of the ZeroFailed module to import. This is useful when testing pre-release + versions of ZeroFailed that are not yet available in the PowerShell Gallery. +.PARAMETER InvokeBuildModuleVersion + The version of the InvokeBuild module to be used. +#> +[CmdletBinding()] +param ( + [Parameter(Position=0)] + [string[]] $Tasks = @("."), + + [Parameter()] + [string] $Configuration = "Debug", + + [Parameter()] + [string] $BuildRepositoryUri = "", + + [Parameter()] + [string] $SourcesDir = $PWD, + + [Parameter()] + [string] $CoverageDir = "_codeCoverage", + + [Parameter()] + [string] $TestReportTypes = "Cobertura", + + [Parameter()] + [string] $PackagesDir = "_packages", + + [Parameter()] + [ValidateSet("minimal","normal","detailed")] + [string] $LogLevel = "minimal", + + [Parameter()] + [switch] $Clean, + + [Parameter()] + [string] $ZfModulePath, + + [Parameter()] + [string] $ZfModuleVersion = "1.0.5", + + [Parameter()] + [version] $InvokeBuildModuleVersion = "5.12.1" +) +$ErrorActionPreference = 'Stop' +$here = Split-Path -Parent $PSCommandPath + +#region InvokeBuild setup +# This handles calling the build engine when this file is run like a normal PowerShell script +# (i.e. avoids the need to have another script to setup the InvokeBuild environment and issue the 'Invoke-Build' command ) +if ($MyInvocation.ScriptName -notlike '*Invoke-Build.ps1') { + Install-PSResource InvokeBuild -Version $InvokeBuildModuleVersion -Scope CurrentUser -TrustRepository -Verbose:$false | Out-Null + try { + Invoke-Build $Tasks $MyInvocation.MyCommand.Path @PSBoundParameters + } + catch { + Write-Host -f Yellow "`n`n***`n*** Build Failure Summary - check previous logs for more details`n***" + Write-Host -f Yellow $_.Exception.Message + Write-Host -f Yellow $_.ScriptStackTrace + exit 1 + } + return +} +#endregion + +#region Initialise build framework +$splat = @{ Force = $true; Verbose = $false} +Import-Module Microsoft.PowerShell.PSResourceGet +if (!($ZfModulePath)) { + Install-PSResource ZeroFailed -Version $ZfModuleVersion -Scope CurrentUser -TrustRepository | Out-Null + $ZfModulePath = "ZeroFailed" + $splat.Add("RequiredVersion", ($ZfModuleVersion -split '-')[0]) +} +else { + Write-Host "ZfModulePath: $ZfModulePath" +} +$splat.Add("Name", $ZfModulePath) +# Ensure only 1 version of the module is loaded +Get-Module ZeroFailed | Remove-Module +Import-Module @splat +$ver = "{0} {1}" -f (Get-Module ZeroFailed).Version, (Get-Module ZeroFailed).PrivateData.PsData.PreRelease +Write-Host "Using ZeroFailed module version: $ver" +#endregion + +$PSModuleAutoloadingPreference = 'none' + +# Load the build configuration +. $here/.zf/config.ps1