foundry-habit-hooks-dotnet 1.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- foundry_habit_hooks_dotnet-1.5.0/.gitignore +6 -0
- foundry_habit_hooks_dotnet-1.5.0/LICENSE +21 -0
- foundry_habit_hooks_dotnet-1.5.0/PKG-INFO +79 -0
- foundry_habit_hooks_dotnet-1.5.0/README.md +57 -0
- foundry_habit_hooks_dotnet-1.5.0/pyproject.toml +51 -0
- foundry_habit_hooks_dotnet-1.5.0/src/foundry_habit_hooks_dotnet/__init__.py +1 -0
- foundry_habit_hooks_dotnet-1.5.0/src/foundry_habit_hooks_dotnet/config.toml +15 -0
- foundry_habit_hooks_dotnet-1.5.0/src/foundry_habit_hooks_dotnet/sensors/dotnet-build.toml +1 -0
- foundry_habit_hooks_dotnet-1.5.0/src/foundry_habit_hooks_dotnet/sensors/dotnet_build_sensor.py +201 -0
- foundry_habit_hooks_dotnet-1.5.0/src/foundry_habit_hooks_dotnet/sensors/sonar.ruleset +16 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CMaintz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: foundry-habit-hooks-dotnet
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: The C#/.NET Habit Hooks plugin
|
|
5
|
+
Project-URL: Repository, https://github.com/CMaintz/foundry-habit-hooks-dotnet
|
|
6
|
+
Project-URL: Issues, https://github.com/CMaintz/foundry-habit-hooks-dotnet/issues
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: ai-coding-agents,ci,code-quality,code-smells,csharp,dotnet,linter,refactoring,static-analysis,technical-debt
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: habit-hooks<1.6,>=1.5
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# foundry-habit-hooks-dotnet
|
|
24
|
+
|
|
25
|
+
The C#/.NET Habit Hooks plugin: wraps `dotnet build` with
|
|
26
|
+
[SonarAnalyzer.CSharp](https://www.nuget.org/packages/SonarAnalyzer.CSharp)
|
|
27
|
+
active, and maps its structural warnings to habit-hooks smell kinds.
|
|
28
|
+
|
|
29
|
+
It is the direct analog of the Java plugin's pmd sensor. Where a plain
|
|
30
|
+
`dotnet build -warnaserror` can only fail the build on any warning, this feeds
|
|
31
|
+
the warnings through habit-hooks' snooze index, so an existing backlog is
|
|
32
|
+
grandfathered and only new structural debt gates.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pip install habit-hooks foundry-habit-hooks-dotnet
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Installing a plugin does not switch it on. Name it in `plugins`:
|
|
41
|
+
|
|
42
|
+
```toml
|
|
43
|
+
# .habit-hooks/config.toml
|
|
44
|
+
plugins = ["dotnet", "generic"]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Requirements
|
|
48
|
+
|
|
49
|
+
- The .NET SDK (`dotnet` on PATH). `winget install Microsoft.DotNet.SDK.9`
|
|
50
|
+
- The target project references `SonarAnalyzer.CSharp` as an analyzer package.
|
|
51
|
+
|
|
52
|
+
The plugin ships a default ruleset that enables the structural rules
|
|
53
|
+
(SonarAnalyzer ships them off by default) and passes it with
|
|
54
|
+
`-p:CodeAnalysisRuleSet`. A project that wants its own thresholds or rule set
|
|
55
|
+
overrides it:
|
|
56
|
+
|
|
57
|
+
```toml
|
|
58
|
+
[sensors.dotnet-build]
|
|
59
|
+
args = ["-p:CodeAnalysisRuleSet=path/to/your.ruleset"]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Rule mapping
|
|
63
|
+
|
|
64
|
+
| Sonar rule | Smell |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| S138 (method length) | oversized-function |
|
|
67
|
+
| S1541 (cyclomatic complexity) | high-complexity |
|
|
68
|
+
| S3776 (cognitive complexity) | high-complexity |
|
|
69
|
+
| S107 (too many parameters) | too-many-parameters |
|
|
70
|
+
| S104 (file length) | oversized-file |
|
|
71
|
+
|
|
72
|
+
Thresholds are SonarAnalyzer's own defaults. A Sonar rule not in the table is
|
|
73
|
+
not reported.
|
|
74
|
+
|
|
75
|
+
## Versioning
|
|
76
|
+
|
|
77
|
+
Released in lockstep with habit-hooks core: the plugin version tracks the
|
|
78
|
+
core's major.minor so the sensor protocol it builds against never drifts. The
|
|
79
|
+
mise `HABIT_HOOKS_VERSION` variable pins both together.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# foundry-habit-hooks-dotnet
|
|
2
|
+
|
|
3
|
+
The C#/.NET Habit Hooks plugin: wraps `dotnet build` with
|
|
4
|
+
[SonarAnalyzer.CSharp](https://www.nuget.org/packages/SonarAnalyzer.CSharp)
|
|
5
|
+
active, and maps its structural warnings to habit-hooks smell kinds.
|
|
6
|
+
|
|
7
|
+
It is the direct analog of the Java plugin's pmd sensor. Where a plain
|
|
8
|
+
`dotnet build -warnaserror` can only fail the build on any warning, this feeds
|
|
9
|
+
the warnings through habit-hooks' snooze index, so an existing backlog is
|
|
10
|
+
grandfathered and only new structural debt gates.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pip install habit-hooks foundry-habit-hooks-dotnet
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Installing a plugin does not switch it on. Name it in `plugins`:
|
|
19
|
+
|
|
20
|
+
```toml
|
|
21
|
+
# .habit-hooks/config.toml
|
|
22
|
+
plugins = ["dotnet", "generic"]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- The .NET SDK (`dotnet` on PATH). `winget install Microsoft.DotNet.SDK.9`
|
|
28
|
+
- The target project references `SonarAnalyzer.CSharp` as an analyzer package.
|
|
29
|
+
|
|
30
|
+
The plugin ships a default ruleset that enables the structural rules
|
|
31
|
+
(SonarAnalyzer ships them off by default) and passes it with
|
|
32
|
+
`-p:CodeAnalysisRuleSet`. A project that wants its own thresholds or rule set
|
|
33
|
+
overrides it:
|
|
34
|
+
|
|
35
|
+
```toml
|
|
36
|
+
[sensors.dotnet-build]
|
|
37
|
+
args = ["-p:CodeAnalysisRuleSet=path/to/your.ruleset"]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Rule mapping
|
|
41
|
+
|
|
42
|
+
| Sonar rule | Smell |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| S138 (method length) | oversized-function |
|
|
45
|
+
| S1541 (cyclomatic complexity) | high-complexity |
|
|
46
|
+
| S3776 (cognitive complexity) | high-complexity |
|
|
47
|
+
| S107 (too many parameters) | too-many-parameters |
|
|
48
|
+
| S104 (file length) | oversized-file |
|
|
49
|
+
|
|
50
|
+
Thresholds are SonarAnalyzer's own defaults. A Sonar rule not in the table is
|
|
51
|
+
not reported.
|
|
52
|
+
|
|
53
|
+
## Versioning
|
|
54
|
+
|
|
55
|
+
Released in lockstep with habit-hooks core: the plugin version tracks the
|
|
56
|
+
core's major.minor so the sensor protocol it builds against never drifts. The
|
|
57
|
+
mise `HABIT_HOOKS_VERSION` variable pins both together.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "foundry-habit-hooks-dotnet"
|
|
7
|
+
version = "1.5.0"
|
|
8
|
+
description = "The C#/.NET Habit Hooks plugin"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
keywords = [
|
|
14
|
+
"ai-coding-agents",
|
|
15
|
+
"ci",
|
|
16
|
+
"code-quality",
|
|
17
|
+
"code-smells",
|
|
18
|
+
"csharp",
|
|
19
|
+
"dotnet",
|
|
20
|
+
"linter",
|
|
21
|
+
"refactoring",
|
|
22
|
+
"static-analysis",
|
|
23
|
+
"technical-debt",
|
|
24
|
+
]
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Development Status :: 4 - Beta",
|
|
27
|
+
"Environment :: Console",
|
|
28
|
+
"Intended Audience :: Developers",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3.11",
|
|
32
|
+
"Programming Language :: Python :: 3.12",
|
|
33
|
+
"Programming Language :: Python :: 3.13",
|
|
34
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
35
|
+
]
|
|
36
|
+
# Released in lockstep with the core: the plugin tracks habit-hooks' major.minor
|
|
37
|
+
# so the sensor protocol it builds against never drifts. Mise's
|
|
38
|
+
# HABIT_HOOKS_VERSION drives both pins together.
|
|
39
|
+
dependencies = ["habit-hooks>=1.5,<1.6"]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Repository = "https://github.com/CMaintz/foundry-habit-hooks-dotnet"
|
|
43
|
+
Issues = "https://github.com/CMaintz/foundry-habit-hooks-dotnet/issues"
|
|
44
|
+
|
|
45
|
+
# The entry point that registers this package under the plugin name `dotnet`,
|
|
46
|
+
# matching how the generic, java and typescript plugins register their own.
|
|
47
|
+
[project.entry-points."habit_hooks.plugins"]
|
|
48
|
+
dotnet = "foundry_habit_hooks_dotnet"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.wheel]
|
|
51
|
+
packages = ["src/foundry_habit_hooks_dotnet"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The dotnet Habit Hooks plugin: package data discovered via the habit_hooks.plugins entry point."""
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# C#/.NET plugin defaults.
|
|
2
|
+
language = "csharp"
|
|
3
|
+
# A project naming no `files` of its own scans what its plugins declare. `bin/`
|
|
4
|
+
# and `obj/` hold build output and generated sources (`*.g.cs`, AssemblyInfo)
|
|
5
|
+
# the project did not write and cannot change, so they are excluded; the
|
|
6
|
+
# exclusions name `*.cs` because a plugin's exclusions bind the union of every
|
|
7
|
+
# active plugin's globs, not just its own.
|
|
8
|
+
files = ["**/*.cs", "!**/bin/**/*.cs", "!**/obj/**/*.cs"]
|
|
9
|
+
sensors = ["dotnet-build"]
|
|
10
|
+
transformers = []
|
|
11
|
+
|
|
12
|
+
# The sensor spawns the dotnet SDK directly.
|
|
13
|
+
detectors = [
|
|
14
|
+
{ name = "dotnet", kind = "command", install = "winget install Microsoft.DotNet.SDK.9" },
|
|
15
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
argv = ["${python}", "${dir}/dotnet_build_sensor.py", "${detector:dotnet}", "${args}", "--", "${files}"]
|
foundry_habit_hooks_dotnet-1.5.0/src/foundry_habit_hooks_dotnet/sensors/dotnet_build_sensor.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Run ``dotnet build`` and print canonical habit-hooks smell findings.
|
|
2
|
+
|
|
3
|
+
The direct analog of the java plugin's pmd sensor: it wraps an external build,
|
|
4
|
+
reads the structural warnings SonarAnalyzer.CSharp emits, and shapes each one
|
|
5
|
+
into the canonical finding, mapping the Sonar rule id to a smell key.
|
|
6
|
+
|
|
7
|
+
``dotnet build`` operates on the project or solution in the working directory,
|
|
8
|
+
not on a file list, so (like the generic jscpd sensor) this does not splice
|
|
9
|
+
``${files}`` into the build command. It builds the whole project, then keeps
|
|
10
|
+
only the warnings whose file is in the run's scope, so a ``--file`` or
|
|
11
|
+
``--branch`` run reports scoped findings rather than the whole project's debt.
|
|
12
|
+
|
|
13
|
+
Warnings are left as warnings (TreatWarningsAsErrors off) so the build still
|
|
14
|
+
completes and this wrapper can read them. That is the whole point: a plain
|
|
15
|
+
``dotnet build -warnaserror`` can only fail the build on any warning, with no
|
|
16
|
+
way to grandfather the backlog and gate only new debt. Feeding the warnings
|
|
17
|
+
through habit-hooks' snooze index is what adds that ratchet.
|
|
18
|
+
|
|
19
|
+
The sensor is run as a loose script (``${python} ${dir}/dotnet_build_sensor.py``),
|
|
20
|
+
so its own directory is ``sys.path[0]``; the default ruleset it ships lives
|
|
21
|
+
next to it and is found by name, the same in the installed package and a
|
|
22
|
+
vendored copy.
|
|
23
|
+
|
|
24
|
+
Its argv spells ``${detector:dotnet} ${args} -- ${files}``: ``sys.argv[1]`` is
|
|
25
|
+
the dotnet executable, and ``sys.argv[2:]`` carries any ``[sensors.dotnet-build]
|
|
26
|
+
args`` on one side of a literal ``--`` and the scoped files on the other.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import os
|
|
33
|
+
import re
|
|
34
|
+
import subprocess
|
|
35
|
+
import sys
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
# Sonar rule id -> habit-hooks smell key (habit_hooks.catalogue). A rule not
|
|
39
|
+
# named here is dropped: the catalogue is the record of what is worth failing a
|
|
40
|
+
# build over, and inventing a smell for every Sonar rule would bury the ones
|
|
41
|
+
# that were chosen.
|
|
42
|
+
RULE_SMELLS = {
|
|
43
|
+
"S138": "oversized-function",
|
|
44
|
+
"S1541": "high-complexity",
|
|
45
|
+
"S3776": "high-complexity",
|
|
46
|
+
"S107": "too-many-parameters",
|
|
47
|
+
"S104": "oversized-file",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# One MSBuild warning line:
|
|
51
|
+
# path\File.cs(12,34): warning S138: message text [path\Project.csproj]
|
|
52
|
+
WARNING = re.compile(
|
|
53
|
+
r"^(?P<file>.+?)\((?P<line>\d+),\d+\): "
|
|
54
|
+
r"warning (?P<rule>S\d+): (?P<message>.*?)(?: \[(?P<project>.+?)\])?$"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# dotnet build exits non-zero on a compile error; only 0 is a clean read of its
|
|
58
|
+
# warnings. Anything else is a broken run, not a clean one.
|
|
59
|
+
BUILD_SUCCESS = 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def build_env() -> dict[str, str]:
|
|
63
|
+
"""The environment the build runs in: English, quiet, no telemetry.
|
|
64
|
+
|
|
65
|
+
The warning text is parsed, so a localised build would hand back messages
|
|
66
|
+
this sensor's regex and the mapper never saw.
|
|
67
|
+
"""
|
|
68
|
+
env = dict(os.environ)
|
|
69
|
+
env["DOTNET_CLI_UI_LANGUAGE"] = "en"
|
|
70
|
+
env["DOTNET_NOLOGO"] = "1"
|
|
71
|
+
env["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1"
|
|
72
|
+
return env
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def default_ruleset() -> Path:
|
|
76
|
+
"""The ruleset this plugin ships, which enables the structural rules.
|
|
77
|
+
|
|
78
|
+
SonarAnalyzer ships most structural rules off by default, so a bare package
|
|
79
|
+
reference emits none of them. This is the answer to "the project configured
|
|
80
|
+
none"; a project naming its own ``-p:CodeAnalysisRuleSet`` in the sensor's
|
|
81
|
+
args passes it after this one, and MSBuild takes the last.
|
|
82
|
+
"""
|
|
83
|
+
return Path(__file__).with_name("sonar.ruleset")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_command(dotnet: str, extra_args: list[str]) -> list[str]:
|
|
87
|
+
"""The ``dotnet build`` invocation, with this plugin's ruleset as the default."""
|
|
88
|
+
return [
|
|
89
|
+
dotnet,
|
|
90
|
+
"build",
|
|
91
|
+
"--no-incremental",
|
|
92
|
+
"-nologo",
|
|
93
|
+
"-p:TreatWarningsAsErrors=false",
|
|
94
|
+
f"-p:CodeAnalysisRuleSet={default_ruleset()}",
|
|
95
|
+
*extra_args,
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def run_build(dotnet: str, extra_args: list[str]) -> subprocess.CompletedProcess[str]:
|
|
100
|
+
"""What the build said, run in the working directory."""
|
|
101
|
+
return subprocess.run(
|
|
102
|
+
build_command(dotnet, extra_args),
|
|
103
|
+
capture_output=True,
|
|
104
|
+
encoding="utf-8",
|
|
105
|
+
errors="replace", # one invalid byte must not take the sensor down
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def split_argv(argv: list[str]) -> tuple[list[str], list[str]]:
|
|
110
|
+
"""``argv`` split on the last literal ``--``: build args before, files after."""
|
|
111
|
+
if "--" not in argv:
|
|
112
|
+
return argv, []
|
|
113
|
+
index = len(argv) - 1 - argv[::-1].index("--")
|
|
114
|
+
return argv[:index], argv[index + 1 :]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def parse_warnings(output: str) -> list[dict]:
|
|
118
|
+
"""Every structural warning the build printed, as ``{file, line, rule, message}``.
|
|
119
|
+
|
|
120
|
+
Deduplicated on ``(file, line, rule)``: a multi-target or multi-project
|
|
121
|
+
build compiles each source once per target and repeats every warning.
|
|
122
|
+
"""
|
|
123
|
+
seen: set[tuple[str, str, str]] = set()
|
|
124
|
+
warnings = []
|
|
125
|
+
for line in output.splitlines():
|
|
126
|
+
warning = _parsed_warning(line.strip())
|
|
127
|
+
if warning is None:
|
|
128
|
+
continue
|
|
129
|
+
identity = (warning["file"], warning["line"], warning["rule"])
|
|
130
|
+
if identity not in seen:
|
|
131
|
+
seen.add(identity)
|
|
132
|
+
warnings.append(warning)
|
|
133
|
+
return warnings
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _parsed_warning(line: str) -> dict | None:
|
|
137
|
+
match = WARNING.match(line)
|
|
138
|
+
if match is None or match["rule"] not in RULE_SMELLS:
|
|
139
|
+
return None
|
|
140
|
+
return match.groupdict()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def in_scope(warnings: list[dict], files: list[str]) -> list[dict]:
|
|
144
|
+
"""The warnings whose file is one the run scoped this sensor to.
|
|
145
|
+
|
|
146
|
+
MSBuild reports absolute paths and the scope arrives project-relative, so
|
|
147
|
+
both sides are compared as absolute, case-folded paths. An empty scope keeps
|
|
148
|
+
nothing: the run narrowed this sensor away and the build's own warnings are
|
|
149
|
+
not the scope's answer.
|
|
150
|
+
"""
|
|
151
|
+
scoped = {_normalised(path) for path in files}
|
|
152
|
+
return [w for w in warnings if _normalised(w["file"]) in scoped]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _normalised(path: str) -> str:
|
|
156
|
+
return os.path.normcase(os.path.abspath(path))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def issue(warning: dict) -> dict:
|
|
160
|
+
"""One warning as a canonical issue, keyed by its file like the pmd sensor."""
|
|
161
|
+
return {
|
|
162
|
+
"key": warning["file"],
|
|
163
|
+
"details": {
|
|
164
|
+
"file": warning["file"],
|
|
165
|
+
"line": int(warning["line"]),
|
|
166
|
+
"message": warning["message"],
|
|
167
|
+
"source": "sonar:" + warning["rule"],
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def findings(warnings: list[dict]) -> list[dict]:
|
|
173
|
+
"""The warnings grouped into one finding per smell."""
|
|
174
|
+
by_smell: dict[str, list[dict]] = {}
|
|
175
|
+
for warning in warnings:
|
|
176
|
+
by_smell.setdefault(RULE_SMELLS[warning["rule"]], []).append(issue(warning))
|
|
177
|
+
return [
|
|
178
|
+
{"smell": smell, "details": {}, "issues": issues}
|
|
179
|
+
for smell, issues in by_smell.items()
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def main() -> int:
|
|
184
|
+
dotnet, argv = sys.argv[1], sys.argv[2:]
|
|
185
|
+
extra_args, files = split_argv(argv)
|
|
186
|
+
result = subprocess.CompletedProcess([], BUILD_SUCCESS, "", "")
|
|
187
|
+
try:
|
|
188
|
+
result = run_build(dotnet, extra_args)
|
|
189
|
+
except OSError as error:
|
|
190
|
+
sys.stderr.write(f"dotnet build could not start: {error}\n")
|
|
191
|
+
return 2
|
|
192
|
+
if result.returncode != BUILD_SUCCESS:
|
|
193
|
+
sys.stderr.write(result.stdout[-4000:] or result.stderr[-4000:])
|
|
194
|
+
return 2
|
|
195
|
+
scoped = in_scope(parse_warnings(result.stdout), files)
|
|
196
|
+
print(json.dumps(findings(scoped)))
|
|
197
|
+
return 0
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
sys.exit(main())
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
Habit-hooks structural smell rules for C#. Reached for only when the project
|
|
4
|
+
names no CodeAnalysisRuleSet of its own; a project's own ruleset or
|
|
5
|
+
.editorconfig severity wins. SonarAnalyzer.CSharp ships these rules off by
|
|
6
|
+
default, so enabling them here is what makes the sensor emit anything.
|
|
7
|
+
-->
|
|
8
|
+
<RuleSet Name="habit-hooks dotnet structural smells" ToolsVersion="15.0">
|
|
9
|
+
<Rules AnalyzerId="SonarAnalyzer.CSharp" RuleNamespace="SonarAnalyzer.CSharp">
|
|
10
|
+
<Rule Id="S104" Action="Warning" />
|
|
11
|
+
<Rule Id="S107" Action="Warning" />
|
|
12
|
+
<Rule Id="S138" Action="Warning" />
|
|
13
|
+
<Rule Id="S1541" Action="Warning" />
|
|
14
|
+
<Rule Id="S3776" Action="Warning" />
|
|
15
|
+
</Rules>
|
|
16
|
+
</RuleSet>
|