大重构
This commit is contained in:
@@ -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"
|
||||
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/main.cmake)
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/main.cmake)
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/main.cmake)
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/main.cmake)
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/main.cmake)
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user