| Abstract class not suffixed with Base |
info |
Naming consistency: an abstract class signals its role as a base type through its name. When that signal is absent, callers cannot distinguish a design contract from a concrete implementation at a glance, making the hierarchy harder to navigate and extend. |
| Abstract class without abstract members |
warn |
Abstraction without a contract: an abstract class that declares no abstract methods or properties defines nothing that subclasses must implement, making the abstraction hollow. Subclasses inherit shared behavior but are given no template — the class acts as a concrete base that happens to be un-instantiable, which misleads readers about its design intent. |
| Async method missing Async suffix |
warn |
The TAP naming convention requires every asynchronous method — whether declared with the async keyword or returning Task/ValueTask directly — to end with the Async suffix. Without it, callers cannot distinguish blocking from non-blocking methods by name alone, which leads to accidental synchronous use and broken composition in async call chains. |
| Async method without CancellationToken |
warn |
Cooperative cancellation: every public or protected async method, and every method returning Task or ValueTask, should accept a CancellationToken so callers can propagate cancellation through the entire call chain. Omitting it forces callers to abandon rather than cancel the operation, wasting resources and delaying shutdown. |
| Async void method |
error |
Swallowed failures: an async void method cannot be awaited, so any exception it throws escapes to the thread-pool's unhandled-exception handler and typically crashes the process or is silently lost — the caller has no way to observe completion or failure. |
| Async-suffixed method does not return an awaitable type |
warn |
Naming convention: the 'Async' suffix is a caller contract that signals the method can be awaited. A method that carries the suffix but returns a synchronous type breaks that contract, misleads callers into omitting await, and produces silent correctness errors at the call site. |
| Attribute class not suffixed with Attribute |
warn |
The .NET naming convention (CA1711) requires every attribute class to end with 'Attribute' so callers can reliably predict the short-form usage name and tooling can identify attribute types by name alone. A class or record that derives from System.Attribute but omits the suffix breaks this contract and causes confusion at every usage site. |
| Boolean method missing verb prefix |
info |
Command-query separation requires that query methods — those returning a boolean answer about state, capability, or membership — be named so the call site reads as a question. A bool-returning method without a recognised verb prefix (Is, Has, Can, Should, Are, Was, Were, Try, Check, Validate, Contains, Exists, Equals, Enable, Disable, Allow, Use, Needs, Supports) looks like a command or a noun and obscures intent, making call sites harder to read and misuse easier. |
| Boolean property missing verb prefix |
info |
Readable API design requires that boolean properties communicate their boolean nature through their name. A property named Active or Deleted gives callers no grammatical signal that the value is a yes/no flag; a reader must inspect the type to understand the intent. The result is code that reads as if (order.Deleted) instead of the self-documenting if (order.IsDeleted). |
| Calculate/Compute method returns void |
warn |
Command-query separation: a method named Calculate or Compute signals a query — it is expected to compute and return a value. A void return type breaks that contract, hiding a side effect behind a query name and making code harder to reason about. |
| CancellationToken parameter missing default value |
warn |
Caller ergonomics: a public API that requires an explicit CancellationToken forces every caller to construct or pass one even when no cancellation context exists, raising the barrier to correct usage. Add '= default' so the parameter is optional without removing the ability to pass a real token. |
| Catching System.Exception directly |
warn |
Swallowed failures: catching System.Exception is too broad and masks bugs the code was never designed to recover from — exceptions such as StackOverflowException or ThreadAbortException indicate fatal conditions and should never be silently handled alongside ordinary application errors. |
| Class implements too many interfaces |
warn |
Interface Segregation and Single Responsibility: a class that simultaneously honours more than five interface contracts almost always has mixed responsibilities. Each additional contract increases the blast radius of any change — altering one responsibility risks breaking callers of the others. |
| Class too large |
info |
A class with more than 500 lines is a signal that it has accumulated multiple responsibilities, violating the Single Responsibility Principle. Large classes are harder to test, reason about, and change safely — a modification for one concern risks breaking another. |
| Concrete class in deep inheritance hierarchy |
warn |
Deep inheritance hierarchies violate the principle of low coupling: a concrete class buried six or more levels below System.Object inherits state, behavior, and invariants from every ancestor, making the type hard to reason about, test in isolation, or substitute. Each additional layer multiplies the surface area of breaking changes. |
| Constant not in PascalCase |
info |
Consistent naming conventions make a codebase navigable and signal intent at a glance. The .NET naming guidelines (and Roslyn CA1707) require const fields to follow PascalCase — the same convention as other static members — so UPPER_SNAKE_CASE or camelCase constants stand out as broken contracts rather than deliberate choices. |
| Constructor has too many parameters |
warn |
Single Responsibility: a constructor with more than four real domain dependencies is a sign the class has taken on too many responsibilities. Each additional dependency is a coupling point that makes the class harder to understand, test, and change independently. |
| Constructor injects concrete class instead of interface |
warn |
Dependency inversion: depending on a concrete class instead of an abstraction couples the consumer to one implementation and makes it impossible to substitute a test double or alternative without changing the consuming type. |
| Conversion method returns void |
info |
Command-query separation: methods named Convert, Map, Transform, or ToXxx are queries — callers expect a return value. A void return means the result is silently discarded or stored as a side effect, making the naming misleading and the call sites error-prone. |
| Count-prefixed method does not return int |
info |
Naming contract violation: a method named Count… signals to every caller that it returns a numeric count, so returning any other type breaks the implicit contract and forces the caller to read the implementation to understand what they receive. |
| Create/Build method returns void |
info |
Command-Query Separation: a method named with a 'Create' or 'Build' prefix promises to produce a new object, but returning void means the constructed result is either lost or smuggled out as a side effect — breaking the naming contract and surprising callers. |
| Deep inheritance hierarchy |
info |
Deep inheritance couples every subclass to the full chain of ancestors, making the hierarchy fragile to change and hard to test in isolation. More than 5 levels of inheritance (matching the CA1501 threshold) is a strong signal that behaviour should be shared via composition rather than stacking base classes. |
| Deep nesting |
info |
Single Responsibility: a method whose nesting depth exceeds 4 is doing too much at once — each additional level encodes another sub-decision that belongs in its own method or guard clause. The result is code that is hard to read, hard to test, and fragile to change. |
| Direct DateTime/DateTimeOffset usage instead of TimeProvider |
warn |
Testability through dependency inversion: hard-coding the clock via DateTime.Now/UtcNow/Today or DateTimeOffset.Now/UtcNow couples the call site to the system clock, making time-sensitive logic impossible to test deterministically. Inject System.TimeProvider as an abstraction instead. Only the static BCL members are flagged; reading Now/UtcNow/Today off an injected abstraction (e.g. an IClock field) is the recommended pattern and is not flagged. |
| Direct instantiation in constructor or field initializer |
warn |
Dependency inversion: hard-coding 'new ConcreteType()' inside a constructor or field initializer binds the class to one specific implementation, making it impossible to substitute a test double or an alternative without modifying the class itself. |
| Documented method has undocumented parameter |
info |
Incomplete API contracts erode trust in documentation: a method that carries a but omits tags for one or more parameters signals to callers that the docs cannot be relied upon. Partial documentation is often worse than none, because it creates a false sense of completeness. |
| Empty catch block |
error |
Swallowed failures: an empty catch turns a runtime failure into silence — a bug that should surface immediately becomes invisible data corruption or a confusing error far from its true cause. |
| Empty interface |
info |
Interface segregation requires that an interface define a real contract. An interface with no methods, properties, or events conveys no contract and is likely a marker interface or a design accident — consumers cannot depend on it meaningfully, and it makes intent invisible to readers. |
| Exception class not suffixed with Exception |
warn |
By convention (CA1710), every class in the exception hierarchy must end its name with 'Exception' so callers can instantly identify throwable types without reading the inheritance chain. A name like 'NotFound' or 'Unauthorized' is indistinguishable from an ordinary domain object until the base type is checked. |
| Explicit type when var is obvious |
info |
Redundant type repetition: when the right-hand side of a local variable declaration already makes the type unambiguous — a constructor call, a cast, or a type-named factory — restating the type on the left is noise that adds no information and obscures the intent of the code (IDE0007). Code clarity is served best when each piece of information appears exactly once. |
| Field name too long |
info |
Readability and cohesion: a field name longer than 30 characters is a signal that the name is compensating for an overly broad concept or a missing abstraction, making the surrounding code harder to scan and maintain. |
| Find-prefixed method returns void |
info |
Command-Query Separation: a method named Find or Search signals a query — the caller expects a result back. Returning void means any located item is silently discarded or stored as a hidden side effect, breaking the naming contract and making intent opaque to every reader. |
| Fire-and-forget async call |
error |
Swallowed failures: discarding a Task without awaiting it, assigning it, or returning it silently absorbs every exception the operation may throw, giving the caller no way to observe failure or guarantee ordering. |
| Get-prefixed method returns void |
warn |
Command-Query Separation: a method named Get... signals a query that returns a value; returning void turns it into a command, breaking the contract implied by the name and making callers look for a return value that does not exist. |
| God class |
warn |
The Single Responsibility Principle (SRP) requires that a class has exactly one reason to change. A class exceeding 1000 lines of code and 30 methods has clearly taken on multiple responsibilities, making it hard to understand, test, and evolve without unintended side effects. |
| High afferent coupling |
info |
Coupling: a type with more than 30 in-solution dependents is a high-stakes stability anchor — every signature change cascades to all consumers, and a volatile implementation here raises the cost of the entire codebase. |
| High cognitive complexity |
warn |
Readability and maintainability degrade sharply when a method's control flow is too tangled to follow in a single reading. Cognitive complexity (SonarSource S3776) scores methods by counting and weighting control-flow breaks, with additional weight for every level of nesting, so a deeply nested branch costs far more than a flat one. A score above 15 indicates a method that is difficult to reason about, review, and safely change. |
| High cyclomatic complexity |
warn |
Single Responsibility: a method with cyclomatic complexity above 15 is doing too many things — each independent branch adds a path that must be tested separately, making the method hard to understand, maintain, and cover with tests. |
| High efferent coupling |
info |
High efferent coupling is a sign that a type violates the Single Responsibility Principle: it knows about too many other types, which makes it hard to test in isolation and expensive to change when any of those dependencies evolve. |
| High response for class |
warn |
A high Response For Class (RFC) metric is a coupling and cohesion signal: the class touches too many distinct methods — its own plus every distinct external method its own methods invoke — making it hard to test in isolation and risky to change because side-effects fan out across many collaborators. |
| High weighted methods per class |
warn |
Low cohesion and SRP violation: when the sum of cyclomatic complexity across all methods (WMC) exceeds 50, the class has accumulated too many decision paths, making it a maintenance burden and a sign that multiple independent responsibilities have been merged into one type. |
| Interface not prefixed with I |
warn |
Naming consistency: the established C# convention (CA1715) requires interface names to start with 'I' so that callers can instantly distinguish a contract from a concrete type. Violating this convention breaks the widely-shared mental model of .NET codebases and makes it harder to discover which types are injectable abstractions. |
| Internal/Impl method should not be public |
warn |
Encapsulation: a method whose name ends with 'Internal' or 'Impl' signals an implementation detail. Exposing it as public leaks internals into the API surface and invites callers to depend on behaviour that was never meant to be contracted. |
| Large enum |
info |
Low cohesion: an enum with more than 25 members has grown beyond a single well-bounded concept, making it harder to reason about, document, and evolve without unintended coupling across unrelated callers. |
| Large void method |
info |
Single Responsibility: a void method that has grown large is accumulating unrelated side effects, because it cannot communicate results to its caller through a return value and must instead operate entirely through mutations. The longer it grows, the harder it becomes to reason about what it changed and to test each concern in isolation. |
| Logic class without interface |
info |
Dependency inversion: a public class with significant logic but no interface couples every consumer to the concrete type, making it impossible to substitute a test double or swap the implementation via DI. The class becomes a seam that cannot be injected abstractly. |
| Low maintainability index |
warn |
Low cohesion and high complexity erode maintainability: when a type accumulates too many lines, too many branches, or too many distinct operations, its maintainability index — a composite of Halstead volume, cyclomatic complexity, and lines of code — drops below the point where safe modification is realistic. A score below 40 signals that the type is difficult to understand, test, and change without introducing regressions. |
| Magic number literal |
info |
Unnamed constants break the readability contract: a bare numeric literal forces every reader to infer its meaning from context, and a value repeated across the codebase must be hunted down whenever the business rule changes. Extracting it into a named constant makes intent explicit at the point of use and makes changes safe. Scoped to method, constructor, property-accessor, and field-initializer literals — a magic number anywhere in the type is in scope. |
| Method has boolean flag parameter |
info |
Single Responsibility: a boolean flag parameter is a sign the method does two different things depending on the value, violating the principle that a method should have one job. Call sites become unreadable (e.g. Process(data, true, false)) because the meaning of the argument is invisible without looking up the signature. |
| Method name too long |
info |
Cohesion: a method name longer than 30 characters is usually a sign the method does more than one thing, or that several responsibilities have not been separated into focused collaborators. Long names make call sites harder to read and suggest the abstraction is missing. |
| Method not in PascalCase |
warn |
Consistent naming conventions are a readability contract: a method starting with a lowercase letter breaks the .NET PascalCase convention, signalling to readers that the codebase may have been ported from Java or JavaScript without adaptation. Tooling such as Roslyn naming rules and StyleCop flags the same issue at Warning level. |
| Method too large |
warn |
Single Responsibility Principle: a method exceeding 80 lines almost always does more than one thing, making it hard to name, test in isolation, and understand without scrolling. Long methods accumulate multiple responsibilities that each deserve their own, focused unit. |
| Missing guard clause / early return |
info |
Fail-fast / readability: wrapping a method's entire body in an if-statement adds an unnecessary nesting level and pushes the happy path to the right. Inverting the condition and returning early at the top (a guard clause) keeps the body flat and makes the intent — 'exit immediately when the precondition is not met' — explicit. |
| Mutable struct |
warn |
Immutability: structs are value types and are copied on every assignment, so mutating a field on a local copy silently leaves the original unchanged — a classic source of hard-to-trace bugs. A struct whose instance fields can be written after construction violates the immutability contract that value-type semantics imply. |
| Mutually dependent types |
warn |
Tight coupling through a bilateral dependency violates the single-responsibility and dependency-inversion principles: when two types each reference the other, neither can be understood, tested, or reused without pulling in the other, and any change to either ripples into both. |
| Namespace far from main sequence |
info |
Stable abstractions principle: a namespace should balance abstractness (ratio of abstract types) with instability (ratio of outgoing to total dependencies). A high distance from the main sequence (|Abstractness + Instability - 1| > 0.7) signals either the zone of pain (stable + concrete, hard to change) or the zone of uselessness (unstable + abstract, unused scaffolding). |
| Non-public unsealed class with no derived types |
info |
Encapsulation: a non-public class that is never subclassed should be sealed to signal that inheritance is not part of its contract. Without 'sealed', readers cannot tell whether the open door is intentional design or an oversight, and the JIT cannot apply devirtualization. |
| Non-readonly static field |
warn |
Shared mutable state: a mutable static field is readable and writable by every thread simultaneously, making it a hidden coupling point between otherwise independent code paths and a persistent source of data races and non-deterministic behaviour. |
| Null check uses == instead of pattern matching |
info |
Operator-overloading safety: equality operators (== / !=) can be overloaded, so '== null' and '!= null' may invoke custom logic rather than a true reference test. Pattern matching ('is null' / 'is not null') is immune to overloading and expresses the intent of a reference test unambiguously — it is the idiomatic C# 8+ form recommended by IDE0150. |
| Override method missing inheritdoc |
info |
Documentation inheritance: an override that carries no XML doc comment breaks the contract that callers can rely on IntelliSense-level documentation without having to navigate to the base declaration. Using fulfils that contract with one line. |
| Parameter not in camelCase |
warn |
Naming consistency: a parameter that starts with an uppercase letter breaks the .NET convention for locals and parameters, making it visually indistinguishable from a property or type name at the call site and degrading readability across the codebase. |
| Parse method returns void |
info |
Command-Query Separation: a method named ParseXxx promises to return the parsed value, but a void return type either silently discards it or stores it as a side-effect, breaking the naming contract and making the method harder to compose and test. |
| Primitive type in logic class constructor |
warn |
Dependency inversion requires that logic classes receive their configuration through typed abstractions, not raw primitives. A public constructor that accepts a string, int, or bool cannot be registered in a DI container without a manual factory, and it couples the class directly to configuration concerns that belong in an options object. |
| Private field missing underscore prefix |
warn |
Consistent field naming (IDE1006 / _camelCase convention): private instance fields that do not follow the _camelCase pattern are indistinguishable from local variables and parameters at a glance, increasing the chance of accidental shadowing and making reading intent harder in non-trivial methods. |
| Property not in PascalCase |
warn |
C# naming guidelines (CA1507/IDE1006) require properties to use PascalCase, so callers can immediately distinguish them from local variables and parameters at the call site. A lowercase-initial property erodes that convention and generates IDE warnings across every consumer. |
| Public member missing XML documentation |
info |
API contract clarity requires that every public surface carries an XML documentation comment. Without it, consumers lose IntelliSense guidance, generated API docs have gaps, and CS1591 warnings accumulate when the compiler's documentation-output switch is enabled. Fires once per undocumented public type, method, or property (not once per type), so a class with three undocumented public methods produces three findings. |
| Public method uses ref or out parameter |
warn |
API surface design: ref and out parameters expose implementation-level calling conventions in a public contract, coupling callers to mutation patterns that cannot be used with async/await, LINQ, or expression trees, and that complicate every call site. The idiomatic .NET alternative — returning a result record or tuple — expresses the same intent without these constraints. |
| Public method uses Tuple in signature |
warn |
Encapsulation: exposing a raw tuple in a public API leaks untyped structural detail — callers must know element positions or rely on fragile member names that disappear at the call site. Replace the tuple with a named record so the contract is self-documenting and refactorable. |
| Public mutable field |
warn |
Encapsulation: a public mutable field exposes internal state directly, so the type cannot enforce its invariants or change its storage representation without breaking every caller. Adding validation, change notification, or computed access later becomes a breaking API change. |
| Public nested type |
info |
Encapsulation: a public nested type leaks the internal structure of its enclosing type into the public API, making the nested type hard to discover, hard to reference, and tightly coupled to its container. Consumers must know the outer type exists before they can use the inner one. |
| Record has public methods |
info |
Single Responsibility: a record is a data carrier — its role is to hold and transport values, not to execute domain logic. Public methods on a record pull behaviour into the data type, coupling callers to implementation details and making the logic harder to test or reuse independently. |
| Record input parameter has wrong suffix |
info |
Communicative naming: a record type accepted as a method parameter should carry a role suffix (Command, Query, Request, etc.) so that the method's intent is self-documenting at the call site. A bare domain-object name like OrderData leaves readers guessing whether the operation is a mutation, a read, or something else entirely. |
| Record return type has wrong suffix |
info |
Communicating intent through naming: a record type returned from a public method carries no meaning about its role in the design unless its name reflects that role. Without a recognized suffix, readers cannot tell whether the type is a command outcome, a data-transfer object, an API response, or a query projection — they must read the implementation to understand it. |
| Rethrow loses the original stack trace |
error |
Stack trace integrity: rethrowing a caught exception by name ('throw ex;') resets the stack trace to the rethrow site, so every log entry and debugger callstack points to the wrong location and the true origin of the fault is lost. |
| Sealed class with protected members |
warn |
Encapsulation: protected access implies derivability, but a sealed class can never be subclassed, so every non-override protected member — method, property, or field — is permanently unreachable by any derived type. This creates a false signal that the type was designed for inheritance, misleading readers and reviewers. |
| Static class has public non-extension methods |
info |
Cohesion / dependency inversion: a public static class whose public methods are not extension methods is a hidden service — it couples every caller directly to the implementation with no seam for testing or substitution. Static utility behaviour that cannot be expressed as an extension method belongs behind an interface. |
| Static class with too many methods |
warn |
SRP: a static class with more than 15 methods has accumulated too many responsibilities. Because static classes cannot be injected or substituted, every caller is tightly coupled to the entire class, making the design hard to test and evolve. |
| Static readonly field not in PascalCase |
warn |
Consistent naming conventions are a readability contract: non-private static readonly fields are part of a type's public surface and must follow PascalCase, the .NET Framework Design Guidelines convention for all non-private members. A field that uses UPPER_SNAKE_CASE or camelCase signals a C or Java heritage and misleads readers about the field's accessibility and intent. |
| String concatenation with + operator |
info |
Allocation efficiency: each + concatenation on strings allocates a new intermediate string object, so chaining multiple + expressions in a method body produces O(n) allocations that a single interpolated string or StringBuilder avoids. In loops or hot paths this degrades throughput; even in ordinary methods it adds unnecessary GC pressure. |
| Too many local variables |
info |
Single Responsibility: a method with more than 10 local variables is carrying too many concerns — it mixes multiple logical steps in one body, making it hard to read, test, and refactor without unintended side-effects. |
| Too many overloads |
info |
A proliferating overload set is a cohesion smell: each variant usually encodes a distinct configuration concern that belongs in a dedicated type or builder, not in a growing list of signatures. More than six overloads of the same method name makes the API hard to discover and the type hard to maintain. |
| Too many parameters |
warn |
Single Responsibility: a method with more than five non-infrastructure parameters has typically taken on too many responsibilities, or several parameters travel together and belong in their own type. Both problems reduce readability and make call sites brittle. |
| Too many public methods |
info |
A large public API surface is a sign that the Single Responsibility Principle is violated: the type has accumulated too many distinct concerns, making it harder to learn, test, and evolve without breaking callers. |
| Try-prefixed method does not return bool |
warn |
Convention contract: the Try-prefix establishes a well-known caller contract — the method signals success or failure via a bool return, with the result in an out parameter. A Try method that returns anything else breaks that contract and surprises every caller who has to inspect the actual return type to understand what the method means. |
| Type name starts with lowercase |
warn |
PascalCase is the .NET naming contract for all type declarations (classes, structs, interfaces, records, enums), as codified in CA1709. A type whose name begins with a lowercase letter is visually indistinguishable from a local variable at the use site, undermining readability and signalling that the codebase was likely ported from a language with different conventions. |
| Type name too long |
info |
Single Responsibility Principle: a type name longer than 40 characters is a strong signal that the type carries multiple responsibilities. When a name must enumerate several concerns to stay accurate, the type itself is doing too much. |
| Unstable highly-coupled type |
info |
The stable-dependency principle states that a heavily used type must be stable, because every change to it ripples through all its consumers. When a type is both widely depended on (high afferent coupling) and itself depends on many others (high efferent coupling, Instability > 0.8), it becomes a high-risk coupling point: one upstream change can cascade through the entire codebase. |
| Untracked TODO or FIXME comment |
info |
Inline TODO, FIXME, HACK, and UNDONE markers are a form of deferred-work debt: they are invisible to project-management tooling, easy to forget, and accumulate silently over time. The result is technical debt that never appears in a backlog and is never scheduled for resolution. |
| Unused method parameter |
info |
Dead code in a method signature violates the principle of minimal interface: a parameter that is never read misleads callers about the method's actual dependencies, inflates the public contract, and often signals an incomplete implementation or a forgotten refactoring. |
| Unused using directive |
info |
Dependency clarity: every using directive is a visible claim that the file depends on that namespace. An unused directive makes the actual dependency surface ambiguous and adds noise that slows code review. |
| Wide type hierarchy |
info |
Cohesion and the Open/Closed Principle: a type with more than 10 total derived types (direct and indirect) has become a broad extension point — each new subtype is a reason to reason about every sibling, making the hierarchy hard to extend safely and difficult to test in isolation. |