大重构

This commit is contained in:
2026-07-28 17:55:29 +08:00
parent d78dff5c8f
commit bde75af7c1
20 changed files with 1093 additions and 0 deletions
+228
View File
@@ -0,0 +1,228 @@
$ErrorActionPreference = "Stop"
$inputDirs = @PSC_COUPLING_INPUT_DIRS@
$excludeDirs = @PSC_COUPLING_EXCLUDE_DIRS@
$outputDir = "@PSC_COUPLING_OUTPUT_DIR@"
$topN = [int]"@PSC_COUPLING_TOP_N@"
$dotExecutable = "@PSC_COUPLING_DOT@"
function Normalize-Path([string]$Path) {
return [IO.Path]::GetFullPath($Path).Replace("\", "/").TrimEnd("/")
}
function Test-InRoots([string]$Path, [string[]]$Roots) {
$normalized = Normalize-Path $Path
foreach ($root in $Roots) {
if ($normalized.StartsWith($root + "/", [StringComparison]::OrdinalIgnoreCase) -or $normalized.Equals($root, [StringComparison]::OrdinalIgnoreCase)) {
return $true
}
}
return $false
}
function Add-Lookup([Collections.Generic.Dictionary[string, Collections.Generic.List[string]]]$Lookup, [string]$Key, [string]$Value) {
$normalizedKey = $Key.Replace("\", "/").TrimStart("/")
if (!$Lookup.ContainsKey($normalizedKey)) {
$Lookup[$normalizedKey] = [Collections.Generic.List[string]]::new()
}
if (!$Lookup[$normalizedKey].Contains($Value)) {
$Lookup[$normalizedKey].Add($Value)
}
}
$roots = @($inputDirs | ForEach-Object { Normalize-Path $_ })
$excludes = @($excludeDirs | ForEach-Object { Normalize-Path $_ })
$extensions = @(".h", ".hh", ".hpp", ".hxx", ".c", ".cc", ".cpp", ".cxx")
$files = [Collections.Generic.List[string]]::new()
foreach ($root in $roots) {
Get-ChildItem -LiteralPath $root -Recurse -File | ForEach-Object {
$path = Normalize-Path $_.FullName
if ($_.Extension.ToLowerInvariant() -in $extensions -and !(Test-InRoots $path $excludes)) {
$files.Add($path)
}
}
}
$files = @($files | Sort-Object -Unique)
$fileSet = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
$lookup = [Collections.Generic.Dictionary[string, Collections.Generic.List[string]]]::new([StringComparer]::OrdinalIgnoreCase)
foreach ($file in $files) {
$fileSet.Add($file) | Out-Null
foreach ($root in $roots) {
if (Test-InRoots $file @($root)) {
Add-Lookup $lookup $file.Substring($root.Length).TrimStart("/") $file
}
}
}
function Resolve-Include([string]$Source, [string]$Include) {
$relativeCandidate = Normalize-Path (Join-Path (Split-Path $Source -Parent) $Include)
if ($fileSet.Contains($relativeCandidate)) {
return $relativeCandidate
}
$key = $Include.Replace("\", "/").TrimStart("/")
if ($lookup.ContainsKey($key) -and $lookup[$key].Count -eq 1) {
return $lookup[$key][0]
}
foreach ($root in $roots) {
$rootCandidate = Normalize-Path (Join-Path $root $Include)
if ($fileSet.Contains($rootCandidate)) {
return $rootCandidate
}
}
$suffixMatches = @($files | Where-Object { $_.EndsWith("/" + $key, [StringComparison]::OrdinalIgnoreCase) })
if ($suffixMatches.Count -eq 1) {
return $suffixMatches[0]
}
return $null
}
$edges = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
$outgoing = [Collections.Generic.Dictionary[string, Collections.Generic.HashSet[string]]]::new([StringComparer]::OrdinalIgnoreCase)
$incoming = [Collections.Generic.Dictionary[string, Collections.Generic.HashSet[string]]]::new([StringComparer]::OrdinalIgnoreCase)
foreach ($file in $files) {
$outgoing[$file] = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
$incoming[$file] = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
}
foreach ($source in $files) {
foreach ($line in Get-Content -LiteralPath $source) {
if ($line -notmatch '^\s*#\s*include\s*["<](?<include>[^">]+)[">]') {
continue
}
$target = Resolve-Include $source $Matches.include
if (!$target -or $target -eq $source) {
continue
}
$edge = $source + "|" + $target
if ($edges.Add($edge)) {
$outgoing[$source].Add($target) | Out-Null
$incoming[$target].Add($source) | Out-Null
}
}
}
function Relative-Display([string]$Path) {
foreach ($root in $roots) {
if (Test-InRoots $Path @($root)) {
return (Split-Path $root -Leaf) + "/" + $Path.Substring($root.Length).TrimStart("/")
}
}
return $Path
}
function Module-Name([string]$Path) {
foreach ($root in $roots) {
if (Test-InRoots $Path @($root)) {
$relative = $Path.Substring($root.Length).TrimStart("/")
$parts = $relative.Split("/")
$rootName = Split-Path $root -Leaf
if ($parts.Count -gt 1) {
return $rootName + "/" + $parts[0]
}
return $rootName
}
}
return "<external>"
}
$fileStats = @($files | ForEach-Object {
$ca = $incoming[$_].Count
$ce = $outgoing[$_].Count
[pscustomobject]@{
File = Relative-Display $_
Afferent = $ca
Efferent = $ce
Coupling = $ca + $ce
Instability = if ($ca + $ce) { [Math]::Round($ce / ($ca + $ce), 4) } else { 0.0 }
}
})
$moduleEdges = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
$modules = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
foreach ($file in $files) {
$modules.Add((Module-Name $file)) | Out-Null
}
foreach ($edge in $edges) {
$parts = $edge.Split("|", 2)
$sourceModule = Module-Name $parts[0]
$targetModule = Module-Name $parts[1]
if ($sourceModule -ne $targetModule) {
$moduleEdges.Add($sourceModule + "|" + $targetModule) | Out-Null
}
}
$moduleStats = @($modules | ForEach-Object {
$module = $_
$ca = @($moduleEdges | Where-Object { $_.Split("|", 2)[1] -eq $module }).Count
$ce = @($moduleEdges | Where-Object { $_.Split("|", 2)[0] -eq $module }).Count
[pscustomobject]@{
Module = $module
Afferent = $ca
Efferent = $ce
Coupling = $ca + $ce
Instability = if ($ca + $ce) { [Math]::Round($ce / ($ca + $ce), 4) } else { 0.0 }
}
})
$script:index = 0
$script:indices = @{}
$script:lowLinks = @{}
$script:onStack = @{}
$script:stack = [Collections.Generic.Stack[string]]::new()
$script:components = [Collections.Generic.List[object]]::new()
function Visit-Node([string]$Node) {
$script:indices[$Node] = $script:index
$script:lowLinks[$Node] = $script:index
$script:index++
$script:stack.Push($Node)
$script:onStack[$Node] = $true
foreach ($next in $outgoing[$Node]) {
if (!$script:indices.ContainsKey($next)) {
Visit-Node $next
$script:lowLinks[$Node] = [Math]::Min($script:lowLinks[$Node], $script:lowLinks[$next])
}
elseif ($script:onStack[$next]) {
$script:lowLinks[$Node] = [Math]::Min($script:lowLinks[$Node], $script:indices[$next])
}
}
if ($script:lowLinks[$Node] -ne $script:indices[$Node]) {
return
}
$component = [Collections.Generic.List[string]]::new()
do {
$member = $script:stack.Pop()
$script:onStack[$member] = $false
$component.Add((Relative-Display $member))
} while ($member -ne $Node)
if ($component.Count -gt 1) {
$script:components.Add(@($component | Sort-Object))
}
}
foreach ($file in $files) {
if (!$script:indices.ContainsKey($file)) {
Visit-Node $file
}
}
$fileTop = @($fileStats | Sort-Object @{Expression = "Coupling"; Descending = $true}, @{Expression = "Efferent"; Descending = $true} | Select-Object -First $topN)
$moduleTop = @($moduleStats | Sort-Object @{Expression = "Coupling"; Descending = $true}, @{Expression = "Efferent"; Descending = $true} | Select-Object -First $topN)
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
$fileTop | Export-Csv -LiteralPath (Join-Path $outputDir "file_coupling.csv") -NoTypeInformation -Encoding UTF8
$moduleTop | Export-Csv -LiteralPath (Join-Path $outputDir "module_coupling.csv") -NoTypeInformation -Encoding UTF8
$edgeRecords = @($edges | ForEach-Object {
$parts = $_.Split("|", 2)
[pscustomobject]@{ Source = Relative-Display $parts[0]; Target = Relative-Display $parts[1] }
})
$edgeRecords | Export-Csv -LiteralPath (Join-Path $outputDir "include_edges.csv") -NoTypeInformation -Encoding UTF8
$dotPath = Join-Path $outputDir "module_coupling.dot"
$dotLines = [Collections.Generic.List[string]]::new()
$dotLines.Add("digraph coupling {")
$dotLines.Add(' graph [rankdir=LR, bgcolor="transparent"];')
$dotLines.Add(' node [shape=box, style="rounded,filled", fillcolor="#e8eef7", color="#6682a3", fontname="Segoe UI"];')
foreach ($edge in $moduleEdges) {
$parts = $edge.Split("|", 2)
$left = $parts[0].Replace('"', '\"')
$right = $parts[1].Replace('"', '\"')
$dotLines.Add(" `"$left`" -> `"$right`";")
}
$dotLines.Add("}")
[IO.File]::WriteAllLines($dotPath, $dotLines, [Text.UTF8Encoding]::new($false))
if (Test-Path -LiteralPath $dotExecutable) {
& $dotExecutable "-Tsvg" $dotPath "-o" (Join-Path $outputDir "module_coupling.svg")
}
$summary = [ordered]@{
files = $files.Count
include_edges = $edges.Count
modules = $modules.Count
module_edges = $moduleEdges.Count
cycles = @($script:components)
file_top = $fileTop
module_top = $moduleTop
}
[IO.File]::WriteAllText((Join-Path $outputDir "summary.json"), ($summary | ConvertTo-Json -Depth 10), [Text.UTF8Encoding]::new($false))
Write-Output "Coupling report: $outputDir"
+109
View File
@@ -0,0 +1,109 @@
$ErrorActionPreference = "Stop"
$outputDir = "@PSC_REPORT_OUTPUT_DIR@"
$projectName = "@PSC_REPORT_PROJECT_NAME@"
$paths = [ordered]@{
llvm = "@PSC_REPORT_LLVM_DIR@"
cppcheck = "@PSC_REPORT_CPPCHECK_DIR@"
lizard = "@PSC_REPORT_LIZARD_DIR@"
code_count = "@PSC_REPORT_CODE_COUNT_DIR@"
coupling = "@PSC_REPORT_COUPLING_DIR@"
}
function Read-Summary([string]$Directory) {
$path = Join-Path $Directory "summary.json"
if (!(Test-Path -LiteralPath $path)) {
throw "Metrics summary was not found at $path"
}
return Get-Content -LiteralPath $path -Raw | ConvertFrom-Json
}
function H([object]$Value) {
return [Net.WebUtility]::HtmlEncode([string]$Value)
}
function Metric-Table([string]$Title, [object[]]$Rows, [string[]]$Columns) {
$builder = [Text.StringBuilder]::new()
[void]$builder.Append("<section><h2>$(H $Title)</h2><div class=`"table-wrap`"><table><thead><tr>")
foreach ($column in $Columns) {
[void]$builder.Append("<th>$(H $column)</th>")
}
[void]$builder.Append("</tr></thead><tbody>")
foreach ($row in @($Rows)) {
[void]$builder.Append("<tr>")
foreach ($column in $Columns) {
[void]$builder.Append("<td>$(H $row.$column)</td>")
}
[void]$builder.Append("</tr>")
}
[void]$builder.Append("</tbody></table></div></section>")
return $builder.ToString()
}
$data = [ordered]@{}
foreach ($entry in $paths.GetEnumerator()) {
$data[$entry.Key] = Read-Summary $entry.Value
}
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
$generatedAt = [DateTimeOffset]::Now.ToString("yyyy-MM-dd HH:mm:ss zzz")
$html = [Text.StringBuilder]::new()
[void]$html.Append(@"
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>$(H $projectName) Engineering Metrics</title>
<style>
:root{font-family:"Segoe UI","Microsoft YaHei",sans-serif;color:#18212b;background:#f4f6f8}
body{margin:0}main{max-width:1500px;margin:auto;padding:28px}
header{display:flex;justify-content:space-between;align-items:flex-end;border-bottom:3px solid #1b6b5a;padding-bottom:16px}
h1{font-size:30px;margin:0}h2{font-size:20px;margin:0 0 12px}
.meta{color:#5d6975}.summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin:20px 0}
.metric{background:white;border:1px solid #d7dde3;border-left:4px solid #1b6b5a;padding:14px}.metric strong{display:block;font-size:24px}.metric span{color:#5d6975}
section{background:white;border:1px solid #d7dde3;margin:14px 0;padding:16px}.table-wrap{overflow:auto;max-height:520px}
table{border-collapse:collapse;width:100%;font-size:13px}th{position:sticky;top:0;background:#e8eef1;text-align:left}th,td{border-bottom:1px solid #e1e5e9;padding:7px 9px;white-space:nowrap}tr:nth-child(even){background:#fafbfc}
a{color:#075f52}footer{color:#68737d;padding:12px 0}
</style>
</head>
<body><main>
<header><div><h1>$(H $projectName) 软件工程指标</h1><div class="meta">静态分析、复杂度、代码规模与耦合度统一报告</div></div><div class="meta">$(H $generatedAt)</div></header>
<div class="summary">
<div class="metric"><strong>$($data.code_count.totals.code)</strong><span>代码行</span></div>
<div class="metric"><strong>$($data.lizard.functions)</strong><span>函数数</span></div>
<div class="metric"><strong>$($data.lizard.average_ccn)</strong><span>平均圈复杂度</span></div>
<div class="metric"><strong>$($data.llvm.findings)</strong><span>认知复杂度记录</span></div>
<div class="metric"><strong>$($data.coupling.include_edges)</strong><span>内部 include 边</span></div>
<div class="metric"><strong>$($data.cppcheck.findings)</strong><span>Cppcheck 发现</span></div>
</div>
"@)
[void]$html.Append((Metric-Table "语言与代码规模" @($data.code_count.languages) @("Language", "Files", "Code", "Comments", "Blanks", "Total")))
[void]$html.Append((Metric-Table "圈复杂度 Top" @($data.lizard.cyclomatic_top) @("CCN", "NLOC", "Function", "File", "StartLine")))
[void]$html.Append((Metric-Table "认知复杂度 Top" @($data.llvm.top) @("Score", "Function", "File", "Line")))
[void]$html.Append((Metric-Table "文件耦合度 Top" @($data.coupling.file_top) @("Coupling", "Afferent", "Efferent", "Instability", "File")))
[void]$html.Append((Metric-Table "模块耦合度 Top" @($data.coupling.module_top) @("Coupling", "Afferent", "Efferent", "Instability", "Module")))
[void]$html.Append((Metric-Table "Cppcheck 发现" @($data.cppcheck.top) @("Severity", "Id", "Message", "File", "Line")))
[void]$html.Append(@"
<section><h2>依赖图</h2><p><a href="coupling/module_coupling.svg">打开模块耦合 SVG</a></p><p class="meta">循环依赖组件数:$(@($data.coupling.cycles).Count)</p></section>
<footer>原始 JSON、CSV、XML、DOT 和 SVG 均保留在各子目录,可供 CI 或后续基准比较。</footer>
</main></body></html>
"@)
[IO.File]::WriteAllText((Join-Path $outputDir "index.html"), $html.ToString(), [Text.UTF8Encoding]::new($false))
$combined = [ordered]@{
project = $projectName
generated_at = $generatedAt
code_count = $data.code_count
lizard = $data.lizard
llvm = $data.llvm
cppcheck = $data.cppcheck
coupling = $data.coupling
}
[IO.File]::WriteAllText((Join-Path $outputDir "metrics.json"), ($combined | ConvertTo-Json -Depth 12), [Text.UTF8Encoding]::new($false))
$markdown = @(
"# $projectName 软件工程指标"
""
"- 代码行:$($data.code_count.totals.code)"
"- 函数数:$($data.lizard.functions)"
"- 平均圈复杂度:$($data.lizard.average_ccn)"
"- Cppcheck 发现:$($data.cppcheck.findings)"
"- 内部 include 边:$($data.coupling.include_edges)"
""
"详细表格请打开 `index.html`。"
)
[IO.File]::WriteAllLines((Join-Path $outputDir "README.md"), $markdown, [Text.UTF8Encoding]::new($false))
Write-Output "Engineering metrics report: $outputDir"
+1
View File
@@ -0,0 +1 @@
include(${CMAKE_CURRENT_LIST_DIR}/main.cmake)
+70
View File
@@ -0,0 +1,70 @@
function(psc_metrics_ps_array out_value)
set(values)
foreach(item IN LISTS ARGN)
string(REPLACE "'" "''" escaped "${item}")
list(APPEND values "'${escaped}'")
endforeach()
string(REPLACE ";" "," value "${values}")
set(${out_value} "@(${value})" PARENT_SCOPE)
endfunction()
function(psc_add_coupling_target target_name)
set(options)
set(oneValueArgs OUTPUT_DIR TOP_N DOT_EXECUTABLE)
set(multiValueArgs INPUT_DIRS EXCLUDE_DIRS)
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if (NOT ARG_OUTPUT_DIR)
set(ARG_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/metrics/${target_name}/coupling")
endif ()
if (NOT ARG_TOP_N)
set(ARG_TOP_N 50)
endif ()
if (NOT ARG_DOT_EXECUTABLE)
set(ARG_DOT_EXECUTABLE "C:/Program Files/Graphviz/bin/dot.exe")
endif ()
if (NOT ARG_INPUT_DIRS)
set(ARG_INPUT_DIRS "${CMAKE_SOURCE_DIR}")
endif ()
psc_metrics_ps_array(PSC_COUPLING_INPUT_DIRS ${ARG_INPUT_DIRS})
psc_metrics_ps_array(PSC_COUPLING_EXCLUDE_DIRS ${ARG_EXCLUDE_DIRS})
set(PSC_COUPLING_OUTPUT_DIR "${ARG_OUTPUT_DIR}")
set(PSC_COUPLING_TOP_N "${ARG_TOP_N}")
set(PSC_COUPLING_DOT "${ARG_DOT_EXECUTABLE}")
set(script_dir "${CMAKE_CURRENT_BINARY_DIR}/psc_metrics_scripts/${target_name}")
file(MAKE_DIRECTORY "${script_dir}")
set(script "${script_dir}/${target_name}_coupling.ps1")
configure_file("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Coupling.ps1.in" "${script}" @ONLY)
add_custom_target(${target_name}
COMMAND pwsh -NoProfile -ExecutionPolicy Bypass -File "${script}"
VERBATIM
)
set(${target_name}_OUTPUT_DIR "${ARG_OUTPUT_DIR}" PARENT_SCOPE)
endfunction()
function(psc_add_metrics_report_target target_name)
set(options)
set(oneValueArgs OUTPUT_DIR LLVM_DIR CPPCHECK_DIR LIZARD_DIR CODE_COUNT_DIR COUPLING_DIR PROJECT_NAME)
set(multiValueArgs DEPENDS)
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if (NOT ARG_OUTPUT_DIR)
set(ARG_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/metrics/${target_name}")
endif ()
if (NOT ARG_PROJECT_NAME)
set(ARG_PROJECT_NAME "${PROJECT_NAME}")
endif ()
set(PSC_REPORT_OUTPUT_DIR "${ARG_OUTPUT_DIR}")
set(PSC_REPORT_LLVM_DIR "${ARG_LLVM_DIR}")
set(PSC_REPORT_CPPCHECK_DIR "${ARG_CPPCHECK_DIR}")
set(PSC_REPORT_LIZARD_DIR "${ARG_LIZARD_DIR}")
set(PSC_REPORT_CODE_COUNT_DIR "${ARG_CODE_COUNT_DIR}")
set(PSC_REPORT_COUPLING_DIR "${ARG_COUPLING_DIR}")
set(PSC_REPORT_PROJECT_NAME "${ARG_PROJECT_NAME}")
set(script_dir "${CMAKE_CURRENT_BINARY_DIR}/psc_metrics_scripts/${target_name}")
file(MAKE_DIRECTORY "${script_dir}")
set(script "${script_dir}/${target_name}_report.ps1")
configure_file("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Report.ps1.in" "${script}" @ONLY)
add_custom_target(${target_name}
COMMAND pwsh -NoProfile -ExecutionPolicy Bypass -File "${script}"
DEPENDS ${ARG_DEPENDS}
VERBATIM
)
set(${target_name}_OUTPUT_DIR "${ARG_OUTPUT_DIR}" PARENT_SCOPE)
endfunction()
+123
View File
@@ -0,0 +1,123 @@
$ErrorActionPreference = "Stop"
$inputDirs = @PSC_CPPCHECK_INPUT_DIRS@
$excludeDirs = @PSC_CPPCHECK_EXCLUDE_DIRS@
$enabledChecks = @PSC_CPPCHECK_ENABLE@
$suppressions = @PSC_CPPCHECK_SUPPRESSIONS@
$outputDir = "@PSC_CPPCHECK_OUTPUT_DIR@"
$compileCommands = "@PSC_CPPCHECK_COMPILE_COMMANDS@"
$configuredCppcheck = "@PSC_CPPCHECK_EXECUTABLE@"
$checkLevel = "@PSC_CPPCHECK_CHECK_LEVEL@"
$jobs = [int]"@PSC_CPPCHECK_JOBS@"
function Resolve-Tool([string]$Configured, [string]$Name, [string[]]$Fallbacks) {
if ($Configured -and (Test-Path -LiteralPath $Configured)) {
return (Resolve-Path -LiteralPath $Configured).Path
}
$command = Get-Command $Name -ErrorAction SilentlyContinue | Select-Object -First 1
if ($command) {
return $command.Source
}
foreach ($fallback in $Fallbacks) {
if (Test-Path -LiteralPath $fallback) {
return (Resolve-Path -LiteralPath $fallback).Path
}
}
throw "$Name executable was not found"
}
function Normalize-Path([string]$Path) {
return [IO.Path]::GetFullPath($Path).Replace("\", "/").TrimEnd("/")
}
function Test-InRoots([string]$Path, [string[]]$Roots) {
$normalized = Normalize-Path $Path
foreach ($root in $Roots) {
if ($normalized.StartsWith($root + "/", [StringComparison]::OrdinalIgnoreCase) -or $normalized.Equals($root, [StringComparison]::OrdinalIgnoreCase)) {
return $true
}
}
return $false
}
$cppcheck = Resolve-Tool $configuredCppcheck "cppcheck" @("C:\Program Files\Cppcheck\cppcheck.exe")
if (!(Test-Path -LiteralPath $compileCommands)) {
throw "compile_commands.json was not found at $compileCommands"
}
$roots = @($inputDirs | ForEach-Object { Normalize-Path $_ })
$excludes = @($excludeDirs | ForEach-Object { Normalize-Path $_ })
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
$database = Get-Content -LiteralPath $compileCommands -Raw | ConvertFrom-Json
$selected = @($database | Where-Object {
$file = Normalize-Path $_.file
(Test-InRoots $file $roots) -and !(Test-InRoots $file $excludes) -and [IO.Path]::GetExtension($file) -in @(".c", ".cc", ".cpp", ".cxx")
})
if ($selected.Count -eq 0) {
throw "No translation units matched the configured input directories"
}
$filteredDatabase = Join-Path $outputDir "compile_commands.json"
[IO.File]::WriteAllText($filteredDatabase, ($selected | ConvertTo-Json -Depth 8), [Text.UTF8Encoding]::new($false))
$xmlPath = Join-Path $outputDir "cppcheck.xml"
$cacheDir = Join-Path $outputDir "cache"
New-Item -ItemType Directory -Force -Path $cacheDir | Out-Null
$arguments = @(
"--project=$filteredDatabase",
"--enable=$($enabledChecks -join ',')",
"--check-level=$checkLevel",
"--std=c++20",
"--inconclusive",
"--inline-suppr",
"--output-format=xmlv3",
"--output-file=$xmlPath",
"--cppcheck-build-dir=$cacheDir"
"-j"
$jobs
)
foreach ($suppression in $suppressions) {
$arguments += "--suppress=$suppression"
}
& $cppcheck @arguments
$toolExitCode = $LASTEXITCODE
if (!(Test-Path -LiteralPath $xmlPath)) {
throw "Cppcheck did not produce $xmlPath"
}
[xml]$xml = Get-Content -LiteralPath $xmlPath -Raw
$records = [Collections.Generic.List[object]]::new()
foreach ($errorNode in $xml.results.errors.error) {
$locations = @($errorNode.location)
if ($locations.Count -eq 0) {
$records.Add([pscustomobject]@{
Severity = [string]$errorNode.severity
Id = [string]$errorNode.id
Message = [string]$errorNode.msg
File = ""
Line = 0
Column = 0
})
continue
}
foreach ($location in $locations) {
$locationFile = [string]$location.file
if ($locationFile -and (!(Test-InRoots $locationFile $roots) -or (Test-InRoots $locationFile $excludes))) {
continue
}
$records.Add([pscustomobject]@{
Severity = [string]$errorNode.severity
Id = [string]$errorNode.id
Message = [string]$errorNode.msg
File = $locationFile
Line = [int]$location.line
Column = [int]$location.column
})
}
}
$records | Export-Csv -LiteralPath (Join-Path $outputDir "cppcheck.csv") -NoTypeInformation -Encoding UTF8
$severity = @($records | Group-Object Severity | Sort-Object Count -Descending | ForEach-Object {
[pscustomobject]@{ severity = $_.Name; count = $_.Count }
})
$summary = [ordered]@{
tool = "cppcheck"
version = (& $cppcheck --version | Select-Object -First 1).Trim()
translation_units = $selected.Count
tool_exit_code = $toolExitCode
findings = $records.Count
severity = $severity
top = @($records | Select-Object -First 100)
}
[IO.File]::WriteAllText((Join-Path $outputDir "summary.json"), ($summary | ConvertTo-Json -Depth 8), [Text.UTF8Encoding]::new($false))
Write-Output "Cppcheck report: $outputDir"
+21
View File
@@ -0,0 +1,21 @@
param(
[string]$Version = "",
[switch]$Force
)
$ErrorActionPreference = "Stop"
$arguments = @("install", "--id", "Cppcheck.Cppcheck", "--exact", "--silent", "--accept-package-agreements", "--accept-source-agreements", "--disable-interactivity")
if ($Version) {
$arguments += @("--version", $Version)
}
if ($Force) {
$arguments += "--force"
}
& winget @arguments
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$cppcheck = "C:\Program Files\Cppcheck\cppcheck.exe"
if (!(Test-Path -LiteralPath $cppcheck)) {
throw "Cppcheck was not installed at $cppcheck"
}
& $cppcheck --version
+1
View File
@@ -0,0 +1 @@
include(${CMAKE_CURRENT_LIST_DIR}/main.cmake)
+57
View File
@@ -0,0 +1,57 @@
function(psc_cppcheck_ps_array out_value)
set(values)
foreach(item IN LISTS ARGN)
string(REPLACE "'" "''" escaped "${item}")
list(APPEND values "'${escaped}'")
endforeach()
string(REPLACE ";" "," value "${values}")
set(${out_value} "@(${value})" PARENT_SCOPE)
endfunction()
function(psc_add_cppcheck_target target_name)
set(options)
set(oneValueArgs OUTPUT_DIR COMPILE_COMMANDS CPPCHECK_EXECUTABLE CHECK_LEVEL JOBS)
set(multiValueArgs INPUT_DIRS EXCLUDE_DIRS ENABLE SUPPRESSIONS)
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if (NOT ARG_OUTPUT_DIR)
set(ARG_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/metrics/${target_name}/cppcheck")
endif ()
if (NOT ARG_COMPILE_COMMANDS)
set(ARG_COMPILE_COMMANDS "${CMAKE_BINARY_DIR}/compile_commands.json")
endif ()
if (NOT ARG_CPPCHECK_EXECUTABLE)
set(ARG_CPPCHECK_EXECUTABLE "C:/Program Files/Cppcheck/cppcheck.exe")
endif ()
if (NOT ARG_CHECK_LEVEL)
set(ARG_CHECK_LEVEL "normal")
endif ()
if (NOT ARG_JOBS)
set(ARG_JOBS 8)
endif ()
if (NOT ARG_INPUT_DIRS)
set(ARG_INPUT_DIRS "${CMAKE_SOURCE_DIR}")
endif ()
if (NOT ARG_ENABLE)
set(ARG_ENABLE warning style performance portability)
endif ()
if (NOT ARG_SUPPRESSIONS)
set(ARG_SUPPRESSIONS missingIncludeSystem unmatchedSuppression)
endif ()
psc_cppcheck_ps_array(PSC_CPPCHECK_INPUT_DIRS ${ARG_INPUT_DIRS})
psc_cppcheck_ps_array(PSC_CPPCHECK_EXCLUDE_DIRS ${ARG_EXCLUDE_DIRS})
psc_cppcheck_ps_array(PSC_CPPCHECK_ENABLE ${ARG_ENABLE})
psc_cppcheck_ps_array(PSC_CPPCHECK_SUPPRESSIONS ${ARG_SUPPRESSIONS})
set(PSC_CPPCHECK_OUTPUT_DIR "${ARG_OUTPUT_DIR}")
set(PSC_CPPCHECK_COMPILE_COMMANDS "${ARG_COMPILE_COMMANDS}")
set(PSC_CPPCHECK_EXECUTABLE "${ARG_CPPCHECK_EXECUTABLE}")
set(PSC_CPPCHECK_CHECK_LEVEL "${ARG_CHECK_LEVEL}")
set(PSC_CPPCHECK_JOBS "${ARG_JOBS}")
set(script_dir "${CMAKE_CURRENT_BINARY_DIR}/psc_metrics_scripts/${target_name}")
file(MAKE_DIRECTORY "${script_dir}")
set(script "${script_dir}/${target_name}_cppcheck.ps1")
configure_file("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Cppcheck.ps1.in" "${script}" @ONLY)
add_custom_target(${target_name}
COMMAND pwsh -NoProfile -ExecutionPolicy Bypass -File "${script}"
VERBATIM
)
set(${target_name}_OUTPUT_DIR "${ARG_OUTPUT_DIR}" PARENT_SCOPE)
endfunction()
+119
View File
@@ -0,0 +1,119 @@
$ErrorActionPreference = "Stop"
$inputDirs = @PSC_LLVM_INPUT_DIRS@
$excludeDirs = @PSC_LLVM_EXCLUDE_DIRS@
$outputDir = "@PSC_LLVM_OUTPUT_DIR@"
$compileCommands = "@PSC_LLVM_COMPILE_COMMANDS@"
$configuredClangTidy = "@PSC_LLVM_CLANG_TIDY@"
$topN = [int]"@PSC_LLVM_TOP_N@"
$threshold = [int]"@PSC_LLVM_THRESHOLD@"
function Resolve-Tool([string]$Configured, [string]$Name, [string[]]$Fallbacks) {
if ($Configured -and (Test-Path -LiteralPath $Configured)) {
return (Resolve-Path -LiteralPath $Configured).Path
}
$command = Get-Command $Name -ErrorAction SilentlyContinue | Select-Object -First 1
if ($command) {
return $command.Source
}
foreach ($fallback in $Fallbacks) {
if (Test-Path -LiteralPath $fallback) {
return (Resolve-Path -LiteralPath $fallback).Path
}
}
throw "$Name executable was not found"
}
function Normalize-Path([string]$Path) {
return [IO.Path]::GetFullPath($Path).Replace("\", "/").TrimEnd("/")
}
function Test-InRoots([string]$Path, [string[]]$Roots) {
$normalized = Normalize-Path $Path
foreach ($root in $Roots) {
if ($normalized.StartsWith($root + "/", [StringComparison]::OrdinalIgnoreCase) -or $normalized.Equals($root, [StringComparison]::OrdinalIgnoreCase)) {
return $true
}
}
return $false
}
$clangTidy = Resolve-Tool $configuredClangTidy "clang-tidy" @("C:\Program Files\LLVM\bin\clang-tidy.exe")
$clangCl = Join-Path (Split-Path $clangTidy -Parent) "clang-cl.exe"
if (!(Test-Path -LiteralPath $clangCl)) {
throw "clang-cl executable was not found next to clang-tidy"
}
if (!(Test-Path -LiteralPath $compileCommands)) {
throw "compile_commands.json was not found at $compileCommands"
}
$roots = @($inputDirs | ForEach-Object { Normalize-Path $_ })
$excludes = @($excludeDirs | ForEach-Object { Normalize-Path $_ })
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
$databaseDir = Join-Path $outputDir "compile_database"
New-Item -ItemType Directory -Force -Path $databaseDir | Out-Null
$database = Get-Content -LiteralPath $compileCommands -Raw | ConvertFrom-Json
$selected = [Collections.Generic.List[object]]::new()
foreach ($entry in $database) {
$file = Normalize-Path $entry.file
if (!(Test-InRoots $file $roots) -or (Test-InRoots $file $excludes)) {
continue
}
if ([IO.Path]::GetExtension($file) -notin @(".c", ".cc", ".cpp", ".cxx")) {
continue
}
$command = [string]$entry.command
$escapedClang = '"' + $clangCl + '"'
$command = [regex]::Replace($command, '^\s*(?:"[^"]+"|[^\s]+?)(?:cl(?:\.exe|\.bat))\s+', $escapedClang + " ", [Text.RegularExpressions.RegexOptions]::IgnoreCase)
$command = [regex]::Replace($command, '(?i)(?:^|\s)[/-](?:Fo|Fd|Fp|FI|Yu|Yc)(?:"[^"]*"|\S+)', " ")
$command = [regex]::Replace($command, '(?i)(?:^|\s)(?:/FS|/nologo|/RTC1|/Ob\d|/Od|-Z7|-MDd?)\b', " ")
$command = [regex]::Replace($command, '(?i)-external:I', "-I")
$command = [regex]::Replace($command, '(?i)\s-external:W\d\b', " ")
$command = [regex]::Replace($command, '\s+', " ").Trim()
$selected.Add([ordered]@{
directory = $entry.directory
command = $command
file = $entry.file
output = $entry.output
})
}
if ($selected.Count -eq 0) {
throw "No translation units matched the configured input directories"
}
$sanitizedDatabase = Join-Path $databaseDir "compile_commands.json"
[IO.File]::WriteAllText($sanitizedDatabase, ($selected | ConvertTo-Json -Depth 8), [Text.UTF8Encoding]::new($false))
$rawPath = Join-Path $outputDir "clang_tidy_raw.txt"
$rawWriter = [IO.StreamWriter]::new($rawPath, $false, [Text.UTF8Encoding]::new($false))
$records = [Collections.Generic.List[object]]::new()
$diagnosticErrors = 0
$config = "{CheckOptions: {readability-function-cognitive-complexity.Threshold: $threshold, readability-function-cognitive-complexity.DescribeBasicIncrements: false, readability-function-cognitive-complexity.IgnoreMacros: false}}"
foreach ($entry in $selected) {
$lines = & $clangTidy $entry.file "-p=$databaseDir" "-checks=-*,readability-function-cognitive-complexity" "-config=$config" "--quiet" 2>&1
foreach ($lineObject in $lines) {
$line = [string]$lineObject
$rawWriter.WriteLine($line)
if ($line -match '^(?<file>.+?):(?<line>\d+):(?<column>\d+): warning: (?:(?:function ''(?<name>[^'']+)'')|(?<lambda>lambda)) has cognitive complexity of (?<score>\d+)') {
$file = Normalize-Path $Matches.file
if ((Test-InRoots $file $roots) -and !(Test-InRoots $file $excludes)) {
$records.Add([pscustomobject]@{
Score = [int]$Matches.score
Function = if ($Matches.name) { $Matches.name } else { "<lambda>" }
File = $file
Line = [int]$Matches.line
Column = [int]$Matches.column
})
}
}
elseif ($line -match '(^| )error:') {
$diagnosticErrors++
}
}
}
$rawWriter.Dispose()
$top = @($records | Sort-Object @{Expression = "Score"; Descending = $true}, File, Line | Select-Object -First $topN)
$top | Export-Csv -LiteralPath (Join-Path $outputDir "cognitive_complexity.csv") -NoTypeInformation -Encoding UTF8
$summary = [ordered]@{
tool = "clang-tidy"
version = (& $clangTidy --version | Select-Object -First 1).Trim()
threshold = $threshold
translation_units = $selected.Count
findings = $records.Count
diagnostic_errors = $diagnosticErrors
top = $top
}
[IO.File]::WriteAllText((Join-Path $outputDir "summary.json"), ($summary | ConvertTo-Json -Depth 8), [Text.UTF8Encoding]::new($false))
Write-Output "LLVM cognitive complexity report: $outputDir"
+21
View File
@@ -0,0 +1,21 @@
param(
[string]$Version = "",
[switch]$Force
)
$ErrorActionPreference = "Stop"
$arguments = @("install", "--id", "LLVM.LLVM", "--exact", "--silent", "--accept-package-agreements", "--accept-source-agreements", "--disable-interactivity")
if ($Version) {
$arguments += @("--version", $Version)
}
if ($Force) {
$arguments += "--force"
}
& winget @arguments
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$clangTidy = "C:\Program Files\LLVM\bin\clang-tidy.exe"
if (!(Test-Path -LiteralPath $clangTidy)) {
throw "clang-tidy was not installed at $clangTidy"
}
& $clangTidy --version
+1
View File
@@ -0,0 +1 @@
include(${CMAKE_CURRENT_LIST_DIR}/main.cmake)
+49
View File
@@ -0,0 +1,49 @@
function(psc_llvm_ps_array out_value)
set(values)
foreach(item IN LISTS ARGN)
string(REPLACE "'" "''" escaped "${item}")
list(APPEND values "'${escaped}'")
endforeach()
string(REPLACE ";" "," value "${values}")
set(${out_value} "@(${value})" PARENT_SCOPE)
endfunction()
function(psc_add_llvm_cognitive_target target_name)
set(options)
set(oneValueArgs OUTPUT_DIR COMPILE_COMMANDS CLANG_TIDY_EXECUTABLE TOP_N THRESHOLD)
set(multiValueArgs INPUT_DIRS EXCLUDE_DIRS)
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if (NOT ARG_OUTPUT_DIR)
set(ARG_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/metrics/${target_name}/llvm")
endif ()
if (NOT ARG_COMPILE_COMMANDS)
set(ARG_COMPILE_COMMANDS "${CMAKE_BINARY_DIR}/compile_commands.json")
endif ()
if (NOT ARG_CLANG_TIDY_EXECUTABLE)
set(ARG_CLANG_TIDY_EXECUTABLE "C:/Program Files/LLVM/bin/clang-tidy.exe")
endif ()
if (NOT ARG_TOP_N)
set(ARG_TOP_N 50)
endif ()
if (NOT DEFINED ARG_THRESHOLD)
set(ARG_THRESHOLD 0)
endif ()
if (NOT ARG_INPUT_DIRS)
set(ARG_INPUT_DIRS "${CMAKE_SOURCE_DIR}")
endif ()
psc_llvm_ps_array(PSC_LLVM_INPUT_DIRS ${ARG_INPUT_DIRS})
psc_llvm_ps_array(PSC_LLVM_EXCLUDE_DIRS ${ARG_EXCLUDE_DIRS})
set(PSC_LLVM_OUTPUT_DIR "${ARG_OUTPUT_DIR}")
set(PSC_LLVM_COMPILE_COMMANDS "${ARG_COMPILE_COMMANDS}")
set(PSC_LLVM_CLANG_TIDY "${ARG_CLANG_TIDY_EXECUTABLE}")
set(PSC_LLVM_TOP_N "${ARG_TOP_N}")
set(PSC_LLVM_THRESHOLD "${ARG_THRESHOLD}")
set(script_dir "${CMAKE_CURRENT_BINARY_DIR}/psc_metrics_scripts/${target_name}")
file(MAKE_DIRECTORY "${script_dir}")
set(script "${script_dir}/${target_name}_llvm_cognitive.ps1")
configure_file("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Cognitive_Complexity.ps1.in" "${script}" @ONLY)
add_custom_target(${target_name}
COMMAND pwsh -NoProfile -ExecutionPolicy Bypass -File "${script}"
VERBATIM
)
set(${target_name}_OUTPUT_DIR "${ARG_OUTPUT_DIR}" PARENT_SCOPE)
endfunction()
+89
View File
@@ -0,0 +1,89 @@
$ErrorActionPreference = "Stop"
$inputDirs = @PSC_CODE_COUNT_INPUT_DIRS@
$excludeDirs = @PSC_CODE_COUNT_EXCLUDE_DIRS@
$outputDir = "@PSC_CODE_COUNT_OUTPUT_DIR@"
$configuredTokei = "@PSC_CODE_COUNT_TOKEI@"
$configuredCloc = "@PSC_CODE_COUNT_CLOC@"
function Resolve-Tool([string]$Configured, [string]$Name, [string[]]$Fallbacks) {
if ($Configured -and (Test-Path -LiteralPath $Configured)) {
return (Resolve-Path -LiteralPath $Configured).Path
}
$command = Get-Command $Name -ErrorAction SilentlyContinue | Select-Object -First 1
if ($command) {
return $command.Source
}
foreach ($fallback in $Fallbacks) {
if (Test-Path -LiteralPath $fallback) {
return (Resolve-Path -LiteralPath $fallback).Path
}
}
throw "$Name executable was not found"
}
$wingetLinks = Join-Path $env:LOCALAPPDATA "Microsoft\WinGet\Links"
$tokei = Resolve-Tool $configuredTokei "tokei" @((Join-Path $wingetLinks "tokei.exe"))
$cloc = Resolve-Tool $configuredCloc "cloc" @((Join-Path $wingetLinks "cloc.exe"))
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
$tokeiArguments = [Collections.Generic.List[string]]::new()
$tokeiArguments.Add("--output")
$tokeiArguments.Add("json")
foreach ($exclude in $excludeDirs) {
$tokeiArguments.Add("--exclude")
$tokeiArguments.Add(($exclude.Replace("\", "/") + "/*"))
}
foreach ($inputDir in $inputDirs) {
$tokeiArguments.Add($inputDir)
}
$tokeiPath = Join-Path $outputDir "tokei.json"
& $tokei @tokeiArguments | Set-Content -LiteralPath $tokeiPath -Encoding UTF8
if ($LASTEXITCODE -ne 0) {
throw "tokei failed with exit code $LASTEXITCODE"
}
$excludePattern = if ($excludeDirs.Count) {
($excludeDirs | ForEach-Object { [regex]::Escape(([IO.Path]::GetFullPath($_)).Replace("\", "/")) }) -join "|"
} else {
"(?!)"
}
$clocJsonPath = Join-Path $outputDir "cloc.json"
$clocXmlPath = Join-Path $outputDir "cloc.xml"
& $cloc @inputDirs "--json" "--by-file" "--fullpath" "--not-match-d=$excludePattern" "--out=$clocJsonPath"
if ($LASTEXITCODE -ne 0) {
throw "cloc JSON generation failed with exit code $LASTEXITCODE"
}
& $cloc @inputDirs "--xml" "--by-file" "--fullpath" "--not-match-d=$excludePattern" "--out=$clocXmlPath"
if ($LASTEXITCODE -ne 0) {
throw "cloc XML generation failed with exit code $LASTEXITCODE"
}
$tokeiData = Get-Content -LiteralPath $tokeiPath -Raw | ConvertFrom-Json
$languages = [Collections.Generic.List[object]]::new()
foreach ($property in $tokeiData.PSObject.Properties) {
if ($property.Name -eq "Total") {
continue
}
$value = $property.Value
$fileCount = if ($value.reports) { @($value.reports).Count } else { 0 }
$languages.Add([pscustomobject]@{
Language = $property.Name
Files = $fileCount
Code = [int64]$value.code
Comments = [int64]$value.comments
Blanks = [int64]$value.blanks
Total = [int64]$value.code + [int64]$value.comments + [int64]$value.blanks
})
}
$languageSummary = @($languages | Sort-Object Code -Descending)
$languageSummary | Export-Csv -LiteralPath (Join-Path $outputDir "languages.csv") -NoTypeInformation -Encoding UTF8
$summary = [ordered]@{
tools = [ordered]@{
tokei = (& $tokei --version | Select-Object -First 1).Trim()
cloc = (& $cloc --version | Select-Object -First 1).Trim()
}
totals = [ordered]@{
files = [int64](($languageSummary | Measure-Object Files -Sum).Sum)
code = [int64](($languageSummary | Measure-Object Code -Sum).Sum)
comments = [int64](($languageSummary | Measure-Object Comments -Sum).Sum)
blanks = [int64](($languageSummary | Measure-Object Blanks -Sum).Sum)
}
languages = $languageSummary
}
[IO.File]::WriteAllText((Join-Path $outputDir "summary.json"), ($summary | ConvertTo-Json -Depth 8), [Text.UTF8Encoding]::new($false))
Write-Output "Code count report: $outputDir"
+17
View File
@@ -0,0 +1,17 @@
param(
[switch]$Force
)
$ErrorActionPreference = "Stop"
foreach ($package in @("XAMPPRocky.Tokei", "AlDanial.Cloc")) {
$arguments = @("install", "--id", $package, "--exact", "--silent", "--accept-package-agreements", "--accept-source-agreements", "--disable-interactivity")
if ($Force) {
$arguments += "--force"
}
& winget @arguments
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
}
$linkDir = Join-Path $env:LOCALAPPDATA "Microsoft\WinGet\Links"
& (Join-Path $linkDir "tokei.exe") --version
& (Join-Path $linkDir "cloc.exe") --version
+1
View File
@@ -0,0 +1 @@
include(${CMAKE_CURRENT_LIST_DIR}/main.cmake)
+41
View File
@@ -0,0 +1,41 @@
function(psc_code_count_ps_array out_value)
set(values)
foreach(item IN LISTS ARGN)
string(REPLACE "'" "''" escaped "${item}")
list(APPEND values "'${escaped}'")
endforeach()
string(REPLACE ";" "," value "${values}")
set(${out_value} "@(${value})" PARENT_SCOPE)
endfunction()
function(psc_add_code_count_target target_name)
set(options)
set(oneValueArgs OUTPUT_DIR TOKEI_EXECUTABLE CLOC_EXECUTABLE)
set(multiValueArgs INPUT_DIRS EXCLUDE_DIRS)
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if (NOT ARG_OUTPUT_DIR)
set(ARG_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/metrics/${target_name}/code_count")
endif ()
if (NOT ARG_TOKEI_EXECUTABLE)
set(ARG_TOKEI_EXECUTABLE "$ENV{LOCALAPPDATA}/Microsoft/WinGet/Links/tokei.exe")
endif ()
if (NOT ARG_CLOC_EXECUTABLE)
set(ARG_CLOC_EXECUTABLE "$ENV{LOCALAPPDATA}/Microsoft/WinGet/Links/cloc.exe")
endif ()
if (NOT ARG_INPUT_DIRS)
set(ARG_INPUT_DIRS "${CMAKE_SOURCE_DIR}")
endif ()
psc_code_count_ps_array(PSC_CODE_COUNT_INPUT_DIRS ${ARG_INPUT_DIRS})
psc_code_count_ps_array(PSC_CODE_COUNT_EXCLUDE_DIRS ${ARG_EXCLUDE_DIRS})
set(PSC_CODE_COUNT_OUTPUT_DIR "${ARG_OUTPUT_DIR}")
set(PSC_CODE_COUNT_TOKEI "${ARG_TOKEI_EXECUTABLE}")
set(PSC_CODE_COUNT_CLOC "${ARG_CLOC_EXECUTABLE}")
set(script_dir "${CMAKE_CURRENT_BINARY_DIR}/psc_metrics_scripts/${target_name}")
file(MAKE_DIRECTORY "${script_dir}")
set(script "${script_dir}/${target_name}_code_count.ps1")
configure_file("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Code_Count.ps1.in" "${script}" @ONLY)
add_custom_target(${target_name}
COMMAND pwsh -NoProfile -ExecutionPolicy Bypass -File "${script}"
VERBATIM
)
set(${target_name}_OUTPUT_DIR "${ARG_OUTPUT_DIR}" PARENT_SCOPE)
endfunction()
+20
View File
@@ -0,0 +1,20 @@
param(
[string]$Version = "1.23.0",
[string]$PythonVersion = "3.10"
)
$ErrorActionPreference = "Stop"
$package = if ($Version) { "lizard==$Version" } else { "lizard" }
& py "-$PythonVersion" -m pip install --user --upgrade $package
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$python = & py "-$PythonVersion" -c "import sys; print(sys.executable)"
$scripts = Join-Path (Split-Path (Split-Path $python -Parent) -Parent) "Scripts"
$lizard = Join-Path $env:APPDATA "Python\Python$($PythonVersion.Replace('.', ''))\Scripts\lizard.exe"
if (!(Test-Path -LiteralPath $lizard)) {
$lizard = Join-Path $scripts "lizard.exe"
}
if (!(Test-Path -LiteralPath $lizard)) {
throw "lizard executable was not found"
}
& $lizard --version
+71
View File
@@ -0,0 +1,71 @@
$ErrorActionPreference = "Stop"
$inputDirs = @PSC_LIZARD_INPUT_DIRS@
$excludeDirs = @PSC_LIZARD_EXCLUDE_DIRS@
$outputDir = "@PSC_LIZARD_OUTPUT_DIR@"
$configuredLizard = "@PSC_LIZARD_EXECUTABLE@"
$topN = [int]"@PSC_LIZARD_TOP_N@"
$ccnWarning = [int]"@PSC_LIZARD_CCN_WARNING@"
$lengthWarning = [int]"@PSC_LIZARD_LENGTH_WARNING@"
$argumentWarning = [int]"@PSC_LIZARD_ARGUMENT_WARNING@"
function Resolve-Lizard {
if ($configuredLizard -and (Test-Path -LiteralPath $configuredLizard)) {
return (Resolve-Path -LiteralPath $configuredLizard).Path
}
$command = Get-Command lizard -ErrorAction SilentlyContinue | Select-Object -First 1
if ($command) {
return $command.Source
}
$fallback = Join-Path $env:APPDATA "Python\Python310\Scripts\lizard.exe"
if (Test-Path -LiteralPath $fallback) {
return (Resolve-Path -LiteralPath $fallback).Path
}
throw "lizard executable was not found"
}
$lizard = Resolve-Lizard
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
$baseArguments = @("-C", $ccnWarning, "-L", $lengthWarning, "-a", $argumentWarning)
foreach ($exclude in $excludeDirs) {
$pattern = (Join-Path $exclude "*").Replace("\", "/")
$baseArguments += @("-x", $pattern)
}
$csvPath = Join-Path $outputDir "lizard_raw.csv"
$xmlPath = Join-Path $outputDir "lizard.xml"
$textPath = Join-Path $outputDir "lizard.txt"
& $lizard @baseArguments "--csv" @inputDirs | Set-Content -LiteralPath $csvPath -Encoding UTF8
$csvExitCode = $LASTEXITCODE
& $lizard @baseArguments "--xml" @inputDirs | Set-Content -LiteralPath $xmlPath -Encoding UTF8
& $lizard @baseArguments @inputDirs | Set-Content -LiteralPath $textPath -Encoding UTF8
$headers = @("NLOC", "CCN", "TokenCount", "ParameterCount", "Length", "Location", "File", "QualifiedName", "Signature", "StartLine", "EndLine")
$records = @(Get-Content -LiteralPath $csvPath | ConvertFrom-Csv -Header $headers | ForEach-Object {
[pscustomobject]@{
NLOC = [int]$_.NLOC
CCN = [int]$_.CCN
TokenCount = [int]$_.TokenCount
ParameterCount = [int]$_.ParameterCount
Length = [int]$_.Length
File = $_.File
Function = $_.QualifiedName
Signature = $_.Signature
StartLine = [int]$_.StartLine
EndLine = [int]$_.EndLine
}
})
$cyclomaticTop = @($records | Sort-Object @{Expression = "CCN"; Descending = $true}, @{Expression = "NLOC"; Descending = $true} | Select-Object -First $topN)
$lengthTop = @($records | Sort-Object @{Expression = "NLOC"; Descending = $true}, @{Expression = "CCN"; Descending = $true} | Select-Object -First $topN)
$parameterTop = @($records | Sort-Object @{Expression = "ParameterCount"; Descending = $true}, @{Expression = "CCN"; Descending = $true} | Select-Object -First $topN)
$cyclomaticTop | Export-Csv -LiteralPath (Join-Path $outputDir "cyclomatic_complexity.csv") -NoTypeInformation -Encoding UTF8
$lengthTop | Export-Csv -LiteralPath (Join-Path $outputDir "function_length.csv") -NoTypeInformation -Encoding UTF8
$parameterTop | Export-Csv -LiteralPath (Join-Path $outputDir "parameter_count.csv") -NoTypeInformation -Encoding UTF8
$summary = [ordered]@{
tool = "lizard"
version = (& $lizard --version | Select-Object -First 1).Trim()
tool_exit_code = $csvExitCode
functions = $records.Count
average_ccn = if ($records.Count) { [Math]::Round(($records | Measure-Object CCN -Average).Average, 3) } else { 0 }
thresholds = [ordered]@{ ccn = $ccnWarning; length = $lengthWarning; arguments = $argumentWarning }
cyclomatic_top = $cyclomaticTop
function_length_top = $lengthTop
parameter_top = $parameterTop
}
[IO.File]::WriteAllText((Join-Path $outputDir "summary.json"), ($summary | ConvertTo-Json -Depth 8), [Text.UTF8Encoding]::new($false))
Write-Output "Lizard report: $outputDir"
+1
View File
@@ -0,0 +1 @@
include(${CMAKE_CURRENT_LIST_DIR}/main.cmake)
+53
View File
@@ -0,0 +1,53 @@
function(psc_lizard_ps_array out_value)
set(values)
foreach(item IN LISTS ARGN)
string(REPLACE "'" "''" escaped "${item}")
list(APPEND values "'${escaped}'")
endforeach()
string(REPLACE ";" "," value "${values}")
set(${out_value} "@(${value})" PARENT_SCOPE)
endfunction()
function(psc_add_lizard_target target_name)
set(options)
set(oneValueArgs OUTPUT_DIR LIZARD_EXECUTABLE TOP_N CCN_WARNING FUNCTION_LENGTH_WARNING ARGUMENT_WARNING)
set(multiValueArgs INPUT_DIRS EXCLUDE_DIRS)
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if (NOT ARG_OUTPUT_DIR)
set(ARG_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/metrics/${target_name}/lizard")
endif ()
if (NOT ARG_LIZARD_EXECUTABLE)
set(ARG_LIZARD_EXECUTABLE "$ENV{APPDATA}/Python/Python310/Scripts/lizard.exe")
endif ()
if (NOT ARG_TOP_N)
set(ARG_TOP_N 50)
endif ()
if (NOT ARG_CCN_WARNING)
set(ARG_CCN_WARNING 15)
endif ()
if (NOT ARG_FUNCTION_LENGTH_WARNING)
set(ARG_FUNCTION_LENGTH_WARNING 200)
endif ()
if (NOT ARG_ARGUMENT_WARNING)
set(ARG_ARGUMENT_WARNING 8)
endif ()
if (NOT ARG_INPUT_DIRS)
set(ARG_INPUT_DIRS "${CMAKE_SOURCE_DIR}")
endif ()
psc_lizard_ps_array(PSC_LIZARD_INPUT_DIRS ${ARG_INPUT_DIRS})
psc_lizard_ps_array(PSC_LIZARD_EXCLUDE_DIRS ${ARG_EXCLUDE_DIRS})
set(PSC_LIZARD_OUTPUT_DIR "${ARG_OUTPUT_DIR}")
set(PSC_LIZARD_EXECUTABLE "${ARG_LIZARD_EXECUTABLE}")
set(PSC_LIZARD_TOP_N "${ARG_TOP_N}")
set(PSC_LIZARD_CCN_WARNING "${ARG_CCN_WARNING}")
set(PSC_LIZARD_LENGTH_WARNING "${ARG_FUNCTION_LENGTH_WARNING}")
set(PSC_LIZARD_ARGUMENT_WARNING "${ARG_ARGUMENT_WARNING}")
set(script_dir "${CMAKE_CURRENT_BINARY_DIR}/psc_metrics_scripts/${target_name}")
file(MAKE_DIRECTORY "${script_dir}")
set(script "${script_dir}/${target_name}_lizard.ps1")
configure_file("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Lizard.ps1.in" "${script}" @ONLY)
add_custom_target(${target_name}
COMMAND pwsh -NoProfile -ExecutionPolicy Bypass -File "${script}"
VERBATIM
)
set(${target_name}_OUTPUT_DIR "${ARG_OUTPUT_DIR}" PARENT_SCOPE)
endfunction()