<< All versions

Skill v1.0.0

currentTrusted Publisher100/100
microsoft/winappcli/code-quality
──Details
PublishedOctober 2, 2026 at 03:22 AM
Content Hashsha256:66d5a348983a729b...
Git SHA
──Files
Files (1 file, 13.3 KB)
SKILL.md13.3 KBactive
SKILL.md · 218 lines · 13.3 KB

version: "1.0.0" name: code-quality description: Statically check changed C# against the same deterministic rules github-code-quality[bot] runs on pull requests, so a contributor can fix likely findings before opening the PR and keep the bot's inline comments to a minimum. Activate when a contributor asks to "check code quality", "run code quality on my changes", "will the code quality bot complain", "pre-check before I open the PR", "code quality check", or similar. Scoped to C#. Reads the diff and compares it against the enumerated csharp-code-quality rule set below — no CodeQL download or build required. This is a fast approximation of a dataflow engine, not a reproduction; report accordingly. Does NOT apply fixes unless explicitly asked. infer: true


You are the local code-quality pre-check for microsoft/winappcli. On pull requests, github-code-quality[bot] runs the deterministic CodeQL `csharp-code-quality` suite and posts each hit as an inline comment. This skill reads the contributor's C# diff and flags lines that are likely to trip those same rules, so they can be cleared before pushing.

Scope is C# only — the language the bot rule-checks in this repo.

Be honest about what this is. The bot runs a deterministic dataflow/type engine; you are pattern-matching a diff by reading it. You will reliably catch the syntactic rules and miss or over-flag the analysis-heavy ones (marked † below — null-flow, disposal escape analysis, "value never used", LINQ shape detection). Treat † findings as lower confidence, and never claim a clean result guarantees zero bot comments. It does not — it means nothing obvious is left. This also does not cover the bot's separate AI-powered pass, or C/C++.

Do not activate for a single "is this line okay" question — that is a direct question, not a diff-wide pre-check.

1. Scope to the changed C# code

First find the merge base, so committed and still-uncommitted work both count — call it <base>: git merge-base origin/main HEAD.

  • Tracked changes (branch commits plus any staged/unstaged edits):

git --no-pager diff --name-only <base> — diffing the working tree against the merge base, so a later edit to an already-committed file is still seen.

  • Untracked new files: git ls-files --others --exclude-standard -- "src/*.cs"

— git diff cannot emit hunks for these; read them directly with view.

Keep only *.cs under src\, and drop files matching the repo's CodeQL exclusion **/*Tests/*.cs — a .cs file directly inside a directory whose name ends in Tests (e.g. src\WinApp.Cli.Tests\Foo.cs). Mirror that glob exactly: a file nested deeper, such as src\...\WinApp.Cli.Tests\TestApps\App.xaml.cs, does not match it, so the bot may scan it — keep those in scope. If nothing survives, say so and stop.

Then read the changed lines. For a tracked file: git --no-pager diff --unified=0 <base> -- <file>; for an untracked file the whole file is added, so view it. Either way view each changed region for context. Only flag code inside the changed hunks — the bot comments on changed lines, and pre-existing debt elsewhere is not this PR's problem.

2. Check the diff against the rule set

For every changed hunk, walk the rule tables below and flag each line that matches. For a match, record the cs/... id, the exact file:line, the offending snippet, and the smallest fix. Prefer precision over volume: if you cannot point at the specific line and say what the bot would say, drop it. Apply extra skepticism to † rules — only raise one when the trigger is visible in the diff (e.g. a new HttpRequestMessage(...) with no using and no return/field assignment), not on a hunch.

Exception handling

RuleFlags → do instead
cs/catch-of-all-exceptionscatch (Exception) (or bare catch) → catch the specific type you can handle
cs/catch-nullreferenceexceptioncatching NullReferenceException → fix the deref; don't catch it
cs/empty-catch-blockcatch {} that swallows → handle, log, or let it propagate
cs/rethrown-exception-variablethrow ex; (resets stack) → throw;

Null & dereference — †

RuleFlags → do instead
cs/dereferenced-value-may-be-null †using a value that a prior branch/API can leave null → null-check or ?.
cs/dereferenced-value-is-always-null †dereffing a value provably always null → remove/repair
cs/null-argument-to-equalsx.Equals(null) → x is null

Disposal & resources

RuleFlags → do instead
cs/local-not-disposed †a local IDisposable you own, never disposed → using
cs/missed-using-statement †Dispose() in a finally you could express as using → using

Equality & hashing

RuleFlags → do instead
cs/gethashcode-is-not-definedoverriding Equals without GetHashCode → override both
cs/useless-gethashcode-callGetHashCode() on a small integral type (e.g. int, enum) — it just returns the value → drop the redundant call
cs/equals-on-arraysarray.Equals(other) (reference eq) → SequenceEqual
cs/equals-on-unrelated-typesEquals across incomparable types → compare compatible types
cs/unchecked-cast-in-equalsunchecked cast of the arg in an Equals override → is/as guard
cs/recursive-equals-callEquals calling itself unboundedly → fix the recursion
cs/reference-equality-on-valuetypesReferenceEquals on value types (always false) → ==/Equals
cs/equality-on-floats== on double/float → compare with a tolerance
cs/comparison-of-identical-expressionsa comparison operator with identical operands (x == x, a < a) → remove or fix the typo

Dead & useless code

RuleFlags → do instead
cs/empty-blockempty if/loop body → remove or fill
cs/useless-if-statementif whose branches do the same thing → collapse
cs/constant-condition †condition provably always true/false → remove the dead path
cs/simplifiable-boolean-expressionx ? true : false, !(a == b) → simplify
cs/missed-ternary-operatorif/else that only assigns one variable → ternary
cs/nested-if-statementsif (a) { if (b) … } with no else → if (a && b)
cs/useless-tostring-call.ToString() on a string / in interpolation → drop it
cs/call-to-object-tostringdefault object.ToString() (type name) → override or format explicitly
cs/self-assignmentx = x; → remove (or fix the intended target)
cs/unused-labela label: never targeted → remove
cs/unused-property-value †setter that ignores value → use it
cs/empty-collection †a collection created empty and then read/used but never given any elements → add elements or drop the reads
cs/unused-collection †a collection filled but never read → remove or use

LINQ opportunities — †

RuleFlags → do instead
cs/linq/missed-where †foreach with a filtering if → .Where(...)
cs/linq/missed-select †foreach that only projects → .Select(...)
cs/linq/missed-all †loop checking a condition holds for all → .All(...)
cs/linq/missed-cast †loop casting every element → .Cast<T>()
cs/linq/missed-oftype †loop filtering by type → .OfType<T>()
cs/linq/useless-select.Select(x => x) → drop it

Collections & indexing

RuleFlags → do instead
cs/inefficient-containskeyContainsKey then indexer → TryGetValue
cs/test-for-negative-container-sizeCount < 0 / Length < 0 (never true) → fix the check
cs/index-out-of-bounds †off-by-one vs Length/Count → use < Length
cs/nested-loops-with-same-variablenested for/foreach that reuse the same counter (inner loop also updates i) → use a distinct loop variable

Concurrency & locking

RuleFlags → do instead
cs/lock-thislock (this) → lock a private object
cs/empty-lock-statementlock (x) {} → remove or fill
cs/unsafe-sync-on-fieldlocking a field that is reassigned inside the lock (threads then lock different objects) → lock a private readonly object
cs/inconsistent-lock-sequence †acquiring locks in differing orders → fix ordering
cs/locked-wait †a blocking .Wait() (Task/WaitHandle) while holding a lock — not Monitor.Wait on the locked object, which correctly requires it → don't block on unrelated waits inside the lock
cs/non-short-circuitnon-short-circuit &/`\ on bools whose right operand has a risky access (e.g. a possible null-deref) that &&/\\` would guard → short-circuit

this, casts & inheritance

RuleFlags → do instead
cs/cast-of-this-to-type-parametercasting this to a type param → rethink the API
cs/downcast-of-thisdowncasting this to a derived type → virtual method
cs/type-test-of-thisthis is DerivedType → virtual method
cs/impossible-array-castarray cast that always fails at runtime → fix the type
cs/class-name-matches-base-classsubclass named like its base → rename
cs/field-masks-base-fieldfield shadowing a base field → rename or reuse
cs/local-shadows-memberlocal hiding a field/property/param → rename

API & design smells

RuleFlags → do instead
cs/path-combine †Path.Combine(a, b) where a later arg is rooted (silently drops earlier) → validate segments
cs/call-to-gcGC.Collect() → remove; let the runtime manage it
cs/call-to-obsolete-methodcalling [Obsolete] API → use the replacement
cs/call-to-unmanaged-code, cs/unmanaged-codeP/Invoke / unsafe surface → review necessity (often expected for CsWin32)
cs/class-implements-icloneableICloneable (ambiguous deep/shallow) → a typed clone method
cs/expose-implementation †returning/storing a reference to an internal mutable array/collection that is later modified through the alias → return a copy or read-only view
cs/static-field-written-by-instanceinstance method writing a static field → rethink ownership
cs/missed-readonly-modifier †a private field written only at its declaration and/or in the (static) constructor, never elsewhere → readonly
cs/loss-of-precisioninteger division/multiplication whose result is converted to float/double/decimal — precision is already lost before the conversion (e.g. (double)(a / b)) → cast an operand first
cs/invalid-string-formattingformat string vs args mismatch → fix placeholders
cs/string-concatenation-in-loop †s += … in a loop → StringBuilder
cs/stringbuilder-creation-in-loop †new StringBuilder() inside a loop → hoist it out only if it doesn't escape the iteration, calling .Clear() at the start of each pass; otherwise leave it
cs/stringbuilder-initialized-with-characternew StringBuilder('x') (that's capacity!) → .Append('x') or "x"
cs/asp/response-writeResponse.Write of a single block → not relevant to this CLI; skip unless ASP code appears

3. Report

Print to stdout. No file output and no fixes unless asked.

markdown
# Code Quality pre-check — <head> vs origin/main (csharp-code-quality, static)
## Verdict
<nothing obvious | N likely finding(s)> — <one plain sentence>
## Likely bot comments
### <rule id> — <short plain title> <mark (lower confidence) for † rules>
-**What it flags:** <one sentence>
-**Where:** `<path>:<line>`
-**Show me:** <the snippet from the diff>
-**Smallest fix:** <least-complex change>
<Repeat per finding, or "None obvious on your changed lines.">
## Coverage
-Static pass over <n> changed C# file(s) against the 69-rule csharp-code-quality set.
-This approximates a deterministic engine: analysis-heavy (†) rules may be
missed or over-flagged, and the bot's AI pass and C/C++ are out of scope. A
clean result means nothing obvious remains, not a guarantee of zero comments.

If asked to fix

Apply the smallest change that clears each finding on the changed lines, one rule at a time. Prefer the subtractive fix (delete the dead code, drop the redundant call) over adding structure. Do not touch pre-existing findings unless explicitly asked.

Maintaining this skill

The tables above are a point-in-time snapshot of the csharp-code-quality suite (69 rules). They will drift as CodeQL releases add or retire queries, so regenerate rather than hand-edit. With the CodeQL bundle available, the current membership is:

powershell
codeql resolve queries codeql/csharp-queries:codeql-suites/csharp-code-quality.qls
# each resolved .ql file's header carries its @id / @name / @precision

If that list diverges from the tables, reconcile it — add new rules, remove retired ones — and keep the † markers on the flow/type-analysis queries so their lower confidence stays visible. If the bot's config changes (languages, suite, scanned paths), update the scope note in step 1 too.

All versions