./rules/assets/img/QualityGateStatusPass32x32.png
Quality Gates Evolution - Quality Gates
Shows quality gates evolution between baseline and now. Gates relying on diff (like New Debt since Baseline) have no evolution available. Double-click a quality gate for editing.
// <Name>Quality Gates Evolution</Name>
from qg in QualityGates
let qgBaseline = qg.OlderVersion()
let relyOnDiff = qgBaseline == null
let evolution = (relyOnDiff || qgBaseline.Value == null) ? TrendIcon.None :
// When a quality gate relies on diff between now and baseline
// it is not executed against the baseline
qg.ValueDiff() == 0d ?
TrendIcon.Constant :
(qg.ValueDiff() > 0 ?
( qg.MoreIsBad ? TrendIcon.RedUp: TrendIcon.GreenUp) :
(!qg.MoreIsBad ? TrendIcon.RedDown: TrendIcon.GreenDown))
select new { qg,
// BaselineStatus = relyOnDiff? (QualityGateStatus?) null : qgBaseline.Status,
Status = qg.Status,
Evolution = evolution,
BaselineValue = relyOnDiff? " " : qgBaseline.ValueString,
Value = qg.ValueString,
}
// <Desc>
// Shows quality gates evolution between baseline and now. Gates relying on
// diff (like *New Debt since Baseline*) have no evolution available.
// Double-click a quality gate for editing.
// </Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
Percentage Coverage - Quality Gates
Code coverage measures how much code is executed by tests; high coverage suggests fewer undetected bugs. Coverage alone is not enough: tests must fail explicitly when a check/assertion gets unvalidated.
Warns below 80%, fails below 70% (indicative, the more the better). Aim for 100% coverage on new and refactored classes, with many checks/assertions.
// <QualityGate Name="Percentage Coverage" Unit="%" />
failif value < 70%
warnif value < 80%
// Coverage of the application code only: test code (the TestCode view) is not counted
let covered = (double)Application.Assemblies.Sum(a => a.NbLinesOfCodeCovered ?? 0)
let notCovered = (double)Application.Assemblies.Sum(a => a.NbLinesOfCodeNotCovered ?? 0)
select (covered + notCovered) == 0d ? (double?)null : 100d * covered / (covered + notCovered)
//<Desc>
// Code coverage measures how much code is executed by tests; high coverage
// suggests fewer undetected bugs. Coverage alone is not enough: tests must
// fail explicitly when a check/assertion gets unvalidated.
//
// Warns below 80%, fails below 70% (indicative, the more the better). Aim for
// 100% coverage on new and refactored classes, with many checks/assertions.
//</Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
Percentage Coverage on New Code - Quality Gates
New Code = methods added since the baseline. All non-UI new classes should be 100% covered.
The ~10% of a class that is hard to cover is not easily testable, hence badly designed and often error-prone: 100% coverage ensures this code gets tested.
// <QualityGate Name="Percentage Coverage on New Code" Unit="%" />
failif value < 70%
warnif value < 80%
let newMethods = Application.Methods.Where(m => m.WasAdded() && m.NbLinesOfCode > 0)
let locCovered = newMethods.Sum(m => m.NbLinesOfCodeCovered)
let loc = newMethods.Sum(m => m.NbLinesOfCode)
select 100d * locCovered / loc
//<Desc>
// *New Code* = methods added since the baseline. All non-UI new classes
// should be 100% covered.
//
// The ~10% of a class that is hard to cover is not easily testable, hence
// badly designed and often **error-prone**: 100% coverage ensures this code
// gets tested.
//</Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
Percentage Coverage on Refactored Code - Quality Gates
Refactored Code = methods where code was changed since the baseline (comment/formatting changes excluded). When refactoring, also write tests to reach 100% coverage.
The ~10% of a class that is hard to cover is not easily testable, hence badly designed and often error-prone: 100% coverage ensures this code gets tested.
// <QualityGate Name="Percentage Coverage on Refactored Code" Unit="%" />
failif value < 70%
warnif value < 80%
let newMethods = Application.Methods.Where(m => m.CodeWasChanged() && m.NbLinesOfCode > 0)
let locCovered = newMethods.Sum(m => m.NbLinesOfCodeCovered)
let loc = newMethods.Sum(m => m.NbLinesOfCode)
select 100d * locCovered / loc
//<Desc>
// *Refactored Code* = methods where *code was changed* since the baseline
// (comment/formatting changes excluded). When refactoring, also write tests
// to reach 100% coverage.
//
// The ~10% of a class that is hard to cover is not easily testable, hence
// badly designed and often **error-prone**: 100% coverage ensures this code
// gets tested.
//</Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
Percentage Coverage (JustMyCode) - Quality Gates
Same as Percentage Coverage, on JustMyCode only: the code that the notmycode queries of the group Defining JustMyCode discard is not counted, like code generated by the compiler or by tools (Entity Framework migrations, Razor views...). Like the rule Code should be tested, it ignores code that is not JustMyCode.
Deactivated by default. Activate it, and deactivate Percentage Coverage, to gate on the code your team writes.
// <QualityGate Name="Percentage Coverage (JustMyCode)" Unit="%" />
failif value < 70%
warnif value < 80%
// Coverage of JustMyCode only: test code, and the code the notmycode queries discard, are not counted
let covered = (double)JustMyCode.Methods.Sum(m => m.NbLinesOfCodeCovered ?? 0)
let notCovered = (double)JustMyCode.Methods.Sum(m => m.NbLinesOfCodeNotCovered ?? 0)
select (covered + notCovered) == 0d ? (double?)null : 100d * covered / (covered + notCovered)
//<Desc>
// Same as *Percentage Coverage*, on **JustMyCode** only: the code that the
// *notmycode* queries of the group *Defining JustMyCode* discard is not
// counted, like code generated by the compiler or by tools (Entity
// Framework migrations, Razor views...). Like the rule *Code should be
// tested*, it ignores code that is not JustMyCode.
//
// Deactivated by default. Activate it, and deactivate *Percentage Coverage*,
// to gate on the code your team writes.
//</Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
Percentage Coverage on New Code (JustMyCode) - Quality Gates
Same as Percentage Coverage on New Code, on JustMyCode only: new code generated by the compiler or by tools (a new Entity Framework migration...) is not counted. Like the rule New Types and Methods should be tested, it ignores code that is not JustMyCode.
Like Percentage Coverage, it counts the lines coverable by tests: code excluded from coverage is not counted.
Deactivated by default. Activate it, and deactivate Percentage Coverage on New Code, to gate on the code your team writes.
// <QualityGate Name="Percentage Coverage on New Code (JustMyCode)" Unit="%" />
failif value < 70%
warnif value < 80%
let newMethods = JustMyCode.Methods.Where(m => m.WasAdded() && m.NbLinesOfCode > 0)
let covered = (double)newMethods.Sum(m => m.NbLinesOfCodeCovered ?? 0)
let notCovered = (double)newMethods.Sum(m => m.NbLinesOfCodeNotCovered ?? 0)
select (covered + notCovered) == 0d ? (double?)null : 100d * covered / (covered + notCovered)
//<Desc>
// Same as *Percentage Coverage on New Code*, on **JustMyCode** only: new
// code generated by the compiler or by tools (a new Entity Framework
// migration...) is not counted. Like the rule *New Types and Methods should
// be tested*, it ignores code that is not JustMyCode.
//
// Like *Percentage Coverage*, it counts the lines coverable by tests: code
// excluded from coverage is not counted.
//
// Deactivated by default. Activate it, and deactivate *Percentage Coverage
// on New Code*, to gate on the code your team writes.
//</Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
Percentage Coverage on Refactored Code (JustMyCode) - Quality Gates
Same as Percentage Coverage on Refactored Code, on JustMyCode only: changed code generated by the compiler or by tools (the Entity Framework model snapshot, rewritten by each migration...) is not counted. Like the rule Methods refactored should be tested, it ignores code that is not JustMyCode.
Like Percentage Coverage, it counts the lines coverable by tests: code excluded from coverage is not counted.
Deactivated by default. Activate it, and deactivate Percentage Coverage on Refactored Code, to gate on the code your team writes.
// <QualityGate Name="Percentage Coverage on Refactored Code (JustMyCode)" Unit="%" />
failif value < 70%
warnif value < 80%
let refactoredMethods = JustMyCode.Methods.Where(m => m.CodeWasChanged() && m.NbLinesOfCode > 0)
let covered = (double)refactoredMethods.Sum(m => m.NbLinesOfCodeCovered ?? 0)
let notCovered = (double)refactoredMethods.Sum(m => m.NbLinesOfCodeNotCovered ?? 0)
select (covered + notCovered) == 0d ? (double?)null : 100d * covered / (covered + notCovered)
//<Desc>
// Same as *Percentage Coverage on Refactored Code*, on **JustMyCode** only:
// changed code generated by the compiler or by tools (the Entity Framework
// model snapshot, rewritten by each migration...) is not counted. Like the
// rule *Methods refactored should be tested*, it ignores code that is not
// JustMyCode.
//
// Like *Percentage Coverage*, it counts the lines coverable by tests: code
// excluded from coverage is not counted.
//
// Deactivated by default. Activate it, and deactivate *Percentage Coverage
// on Refactored Code*, to gate on the code your team writes.
//</Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
Blocker Issues - Quality Gates
A Blocker issue cannot move to production, it must be fixed.
Severity is defined in the rule source code or inferred from the issue annual interest and thresholds in Project Properties > Issue and Debt.
// <QualityGate Name="Blocker Issues" Unit="issues" />
failif count > 0 issues
from i in Issues
where i.Severity == Severity.Blocker
select new { i, i.Severity, i.Debt, i.AnnualInterest }
//<Desc>
// A **Blocker** issue cannot move to production, it must be fixed.
//
// Severity is defined in the rule source code or inferred from the issue
// *annual interest* and thresholds in Project Properties > Issue and Debt.
//</Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
Critical Issues - Quality Gates
A Critical issue shouldn't move to production (it can for business imperatives) but must be fixed during the next iterations.
Severity is defined in the rule source code or inferred from the issue annual interest and thresholds in Project Properties > Issue and Debt.
// <QualityGate Name="Critical Issues" Unit="issues" />
failif count > 10 issues
warnif count > 0 issues
from i in Issues
where i.Severity == Severity.Critical
select new { i, i.Severity, i.Debt, i.AnnualInterest }
//<Desc>
// A **Critical** issue shouldn't move to production (it can for business
// imperatives) but must be fixed during the next iterations.
//
// Severity is defined in the rule source code or inferred from the issue
// *annual interest* and thresholds in Project Properties > Issue and Debt.
//</Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
New Blocker / Critical / High Issues - Quality Gates
A Blocker issue cannot move to production. A Critical issue shouldn't (it can for business imperatives) but must be fixed during the next iterations. A High issue should be fixed quickly, but can wait until the next scheduled interval.
Severity is defined in the rule source code or inferred from the issue annual interest and thresholds in Project Properties > Issue and Debt.
// <QualityGate Name="New Blocker / Critical / High Issues" Unit="issues" />
failif count > 0 issues
from i in Issues
where i.Severity.EqualsAny(Severity.Blocker, Severity.Critical, Severity.High) &&
// Count both the new issues and the issues that became at least Critical
(i.WasAdded() || i.OlderVersion().Severity < Severity.High)
select new { i, i.Severity, i.Debt, i.AnnualInterest }
//<Desc>
// A **Blocker** issue cannot move to production. A **Critical** issue
// shouldn't (it can for business imperatives) but must be fixed during the
// next iterations. A **High** issue should be fixed quickly, but can wait
// until the next scheduled interval.
//
// Severity is defined in the rule source code or inferred from the issue
// *annual interest* and thresholds in Project Properties > Issue and Debt.
//</Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
Critical Rules Violated - Quality Gates
Fails if any critical rule is violated. Critical rules (made so via the Critical button in the rule edition control) should never be violated.
Without a baseline, rules relying on diff are not counted, which can explain an unexpected slight decrease of this count.
// <QualityGate Name="Critical Rules Violated" Unit="rules" />
failif count > 0 rules
from r in Rules where r.IsCritical && r.IsViolated()
select new { r, issues = r.Issues() }
//<Desc>
// Fails if any critical rule is violated. Critical rules (made so via the
// *Critical button* in the rule edition control) should never be violated.
//
// Without a baseline, rules relying on diff are not counted, which can
// explain an unexpected slight decrease of this count.
//</Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
Treat Compiler Warnings as Error - Quality Gates
Ignored compiler warnings can lead to runtime errors, unpredictable behavior or security vulnerabilities; fixing them early prevents production problems and fosters best practices.
Requires Roslyn or R# Analyzers issues to be imported:
https://www.ndepend.com/docs/roslyn-analyzer-issue-import https://www.ndepend.com/docs/resharper-code-inspection-issue-import
Compiler warnings are Roslyn issues whose rule id starts with "CS" or "VB". Warns on any warning, fails on 10 or more. C# compiler warnings list:
https://learn.microsoft.com/en-us/dotnet/csharp/misc/cs0183
// <QualityGate Name="Treat Compiler Warnings as Error" Unit="issues" />
failif count > 10
warnif count > 0
from issue in context.IssuesSet.AllIssues.Where(
i => (i.Rule.Provider == RuleProvider.Roslyn &&
i.Rule.Id.StartsWithAny("CS","VB")) ||
(i.Rule.Provider == RuleProvider.Resharper &&
i.Rule.Category.EndsWith("CompilerWarnings")))
select new { issue, Explanation =issue.GetExplanation() }
//<Desc>
// Ignored compiler warnings can lead to runtime errors, unpredictable
// behavior or security vulnerabilities; fixing them early prevents production
// problems and fosters best practices.
//
// Requires Roslyn or R# Analyzers issues to be imported:
// https://www.ndepend.com/docs/roslyn-analyzer-issue-import
// https://www.ndepend.com/docs/resharper-code-inspection-issue-import
//
// Compiler warnings are Roslyn issues whose rule id starts with "CS" or "VB".
// Warns on any warning, fails on 10 or more. C# compiler warnings list:
// https://learn.microsoft.com/en-us/dotnet/csharp/misc/cs0183
//</Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
Percentage Debt - Quality Gates
% Debt = estimated time to fix all issues (Debt) over estimated total effort to develop the code base, inferred from # lines of code and the
Estimated number of man-day to develop 1000 logical lines of code setting in Project Properties > Issue and Debt.
Fails above 30%, warns above 20%. Debt documentation:
https://www.ndepend.com/docs/technical-debt#Debt
// <QualityGate Name="Percentage Debt" Unit="%" />
failif value > 30%
warnif value > 20%
// Effort to develop the application code only: test code (the TestCode view) is not counted
let timeToDev = TimeSpan.FromMinutes(Application.Assemblies.Sum(a => (a.EffortToDevelop() ?? TimeSpan.Zero).TotalMinutes))
let debt = Issues.Sum(i => i.Debt)
select 100d * debt.ToManDay() / timeToDev.ToManDay()
// <Desc>
// % Debt = estimated time to fix all issues (Debt) over estimated total
// effort to develop the code base, inferred from # lines of code and the
// *Estimated number of man-day to develop 1000 logical lines of code*
// setting in Project Properties > Issue and Debt.
//
// Fails above 30%, warns above 20%.
// Debt documentation: https://www.ndepend.com/docs/technical-debt#Debt
// </Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
Debt - Quality Gates
Disabled per default: acceptable Debt thresholds in man-days depend on project size, team and context. Refer to
Percentage Debt instead.
Debt = sum of estimated effort to fix all issues. Debt documentation:
https://www.ndepend.com/docs/technical-debt#Debt
// <QualityGate Name="Debt" Unit="man-days" />
failif value > 50 man-days
warnif value > 30 man-days
Issues.Sum(i => i.Debt).ToManDay()
//<Desc>
// Disabled per default: acceptable Debt thresholds in man-days depend on
// project size, team and context. Refer to **Percentage Debt** instead.
//
// Debt = sum of estimated effort to fix all issues.
// Debt documentation: https://www.ndepend.com/docs/technical-debt#Debt
//</Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
New Debt since Baseline - Quality Gates
Fails if the effort to fix new or worsened issues (
New Debt since Baseline) exceeds 2 man-days; warns if positive. Debt documentation:
https://www.ndepend.com/docs/technical-debt#Debt
// <QualityGate Name="New Debt since Baseline" Unit="man-days" />
failif value > 2 man-days
warnif value > 0 man-days
let debt = Issues.Sum(i => i.Debt)
let debtInBaseline = IssuesInBaseline.Sum(i => i.Debt)
select (debt - debtInBaseline).ToManDay()
//<Desc>
// Fails if the effort to fix new or worsened issues (*New Debt since
// Baseline*) exceeds 2 man-days; warns if positive.
// Debt documentation: https://www.ndepend.com/docs/technical-debt#Debt
//</Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
Debt Rating per Namespace - Quality Gates
Forbid namespaces with a poor Debt Rating of E or D.
The Debt Rating derives from the Debt Ratio (% of Debt Amount over estimated effort to develop the code element) and the thresholds in Debt Settings. The effort to develop is inferred from lines of code and the man-days to develop 1000 logical lines of code setting.
The Quality Gate can be modified to match assemblies, types or methods.
// <QualityGate Name="Debt Rating per Namespace" Unit="namespaces" />
failif count > 0 namespaces
from n in Application.Namespaces
where n.DebtRating() != null &&
n.DebtRating().Value.EqualsAny(DebtRating.E, DebtRating.D)
select new {
n,
debtRating = n.DebtRating(),
debtRatio = n.DebtRatio(), // % of debt from which DebtRating is inferred
devTimeInManDay = n.EffortToDevelop().ToDebt(),
debtInManDay = n.AllDebt(),
issues = n.AllIssues()
}
// <Desc>
// Forbid namespaces with a poor Debt Rating of **E** or **D**.
//
// The **Debt Rating** derives from the **Debt Ratio** (% of **Debt Amount**
// over **estimated effort to develop the code element**) and the thresholds
// in *Debt Settings*. The effort to develop is inferred from lines of code
// and the *man-days to develop 1000 logical lines of code* setting.
//
// The Quality Gate can be modified to match assemblies, types or methods.
// </Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
Annual Interest - Quality Gates
Disabled per default: acceptable Annual-Interest thresholds in man-days depend on project size, team and context. Refer to
New Annual Interest since Baseline instead.
Annual-Interest = estimated annual cost in man-days to leave all issues unfixed; computed per issue by a rule formula, or from a
Severity level via thresholds in
Project Properties > Issue and Debt > Annual Interest. Annual Interest documentation:
https://www.ndepend.com/docs/technical-debt#AnnualInterest
// <QualityGate Name="Annual Interest" Unit="man-days" />
failif value > 50 man-days
warnif value > 30 man-days
Issues.Sum(i => i.AnnualInterest).ToManDay()
//<Desc>
// Disabled per default: acceptable Annual-Interest thresholds in man-days
// depend on project size, team and context. Refer to
// **New Annual Interest since Baseline** instead.
//
// Annual-Interest = estimated annual cost in man-days to leave all issues
// unfixed; computed per issue by a rule formula, or from a **Severity** level
// via thresholds in *Project Properties > Issue and Debt > Annual Interest*.
// Annual Interest documentation: https://www.ndepend.com/docs/technical-debt#AnnualInterest
//</Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
New Annual Interest since Baseline - Quality Gates
Fails if the
Annual-Interest (estimated annual cost of leaving all issues unfixed) increased by more than 2 man-days since baseline; warns if positive. It is computed per issue by a rule formula, or from a
Severity level via thresholds in
Project Properties > Issue and Debt. Annual Interest documentation:
https://www.ndepend.com/docs/technical-debt#AnnualInterest
// <QualityGate Name="New Annual Interest since Baseline" Unit="man-days" />
failif value > 2 man-days
warnif value > 0 man-days
let ai = Issues.Sum(i => i.AnnualInterest)
let aiInBaseline = IssuesInBaseline.Sum(i => i.AnnualInterest)
select (ai - aiInBaseline).ToManDay()
//<Desc>
// Fails if the **Annual-Interest** (estimated annual cost of leaving all
// issues unfixed) increased by more than 2 man-days since baseline; warns if
// positive. It is computed per issue by a rule formula, or from a **Severity**
// level via thresholds in *Project Properties > Issue and Debt*.
// Annual Interest documentation: https://www.ndepend.com/docs/technical-debt#AnnualInterest
//</Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
Unnecessary NDepend SuppressMessageAttribute Usage - Quality Gates
Finds SuppressMessageAttribute("NDepend", ...) usages that suppress no NDepend issue; they can be safely removed. Disabled by default (advanced usage only). On test code, only ND1611 suppressions are checked: no other default rule reports issues there. Doc:
https://www.ndepend.com/docs/suppress-issues
// <QualityGate Name="Unnecessary NDepend SuppressMessageAttribute Usage" Unit="usages" />
failif count > 50
warnif count > 0
// There can be several SuppressMessageAttribute
// in System.Runtime, netstandard and mscorlib for example
let suppressAttributes = ThirdParty.Types.
WithFullName("System.Diagnostics.CodeAnalysis.SuppressMessageAttribute")
// Get the tags of SuppressMessageAttribute used
// from suppressed issues
let ndpSuppressTagsUsed =
(from i in context.IssuesSet.AllSuppressedIssues
where i.Rule.Provider == RuleProvider.CodeQueryRule
select i.SuppressAttributeTag).ToHashSetEx()
// Get all tags like SuppressMessageAttribute("NDepend"...)
let allNdpSuppressTags = suppressAttributes
.SelectMany(sa => sa.TagsWithMeAsAttribute)
// Filter only NDepend category
.Where(s => s.ValueOf("Category") != null &&
s.ValueOf("Category").AsString.StartsWith("NDepend"))
// Infer unnecessary tags like SuppressMessageAttribute("NDepend"...)
let ndpTagsSuppressUnused = allNdpSuppressTags.Except(ndpSuppressTagsUsed)
// Uncomment to get instead all tags that actually suppress one or several issues
//from tag in ndpSuppressTagsUsed
from tag in ndpTagsSuppressUnused
let checkId = tag.ValueOf("CheckId") == null ? null : tag.ValueOf("CheckId").AsString
where !tag.CodeElementTagged.IsThirdParty &&
// Rules don't report issues on test code except ND1611 'Test methods should assert something':
// there, only an ND1611 suppression can be unnecessary
(!tag.CodeElementTagged.IsTestCode ||
(checkId != null && (checkId == "ND1611" || checkId.StartsWith("ND1611:"))))
select new {
tag.CodeElementTagged,
tag.AttributeType,
desc=tag.Parameters.Select(p =>p.Name +": "+p.Value.AsString).Aggregate(", ")
}
//<Desc>
// Finds SuppressMessageAttribute("NDepend", ...) usages that suppress no
// NDepend issue; they can be safely removed. Disabled by default (advanced
// usage only). On test code, only ND1611 suppressions are checked: no
// other default rule reports issues there.
// Doc: https://www.ndepend.com/docs/suppress-issues
//</Desc>
./rules/assets/img/Info32x32.png
Types Hot Spots - Hot Spots
Lists types with most Debt (largest fix effort), counting issues on the type and its members. Type size and coverage are shown since untested code generates much Debt (uncomment t.PercentageCoverage once coverage data is imported); Debt Rating and Debt Ratio too.
--
Debt estimates distance from clean code, not fix priority. Annual Interest estimates the annual cost of leaving issues unfixed (Severity derives from it). The Debt Breaking Point is when the cost of leaving an issue unfixed equals the fix effort: the shorter, the higher the ROI — the Breaking Point is the right metric to prioritize issues fix.
// <Name>Types Hot Spots</Name>
from t in JustMyCode.Types
where t.AllDebt() > Debt.Zero &&
t.AllAnnualInterest() > AnnualInterest.Zero
orderby t.AllDebt().Value.TotalMinutes descending
select new { t,
Debt = t.AllDebt(),
Issues = t.AllIssues(), // AllIssues = {types issues} union {members issues}
AnnualInterest = t.AllAnnualInterest(),
BreakingPoint = t.AllBreakingPoint(),
t.NbLinesOfCode,
// t.PercentageCoverage, to uncomment if coverage data is imported
DebtRating = t.DebtRating(),
DebtRatio = t.DebtRatio()
}
//<Desc>
// Lists **types with most Debt** (largest fix effort), counting issues on the
// type and its members. Type size and coverage are shown since untested code
// generates much Debt (uncomment *t.PercentageCoverage* once coverage data is
// imported); *Debt Rating* and *Debt Ratio* too.
//
// --
//
// *Debt* estimates distance from clean code, not fix priority. *Annual
// Interest* estimates the annual cost of leaving issues unfixed (*Severity*
// derives from it). The **Debt Breaking Point** is when the cost of leaving
// an issue unfixed equals the fix effort: the shorter, the higher the ROI —
// the **Breaking Point is the right metric to prioritize issues fix**.
//</Desc>
./rules/assets/img/Info32x32.png
Types to Fix Priority - Hot Spots
Lists types per increasing Debt Breaking Point: the duration from now when the cost of leaving issues unfixed (Annual Interest) equals the fix effort (Debt). The shorter, the higher the fix ROI.
New/refactored types since baseline often come first: their issues get a higher Annual Interest since new issues should be fixed first.
--
Issues on the type and its members are both counted. Only types with at least 30 minutes of Debt are listed (Breaking Point makes less sense on small Debt). Type size and coverage are shown since untested code generates much Debt (uncomment t.PercentageCoverage once coverage data is imported); Debt Rating and Debt Ratio too.
// <Name>Types to Fix Priority</Name>
from t in JustMyCode.Types
where t.AllBreakingPoint() > TimeSpan.Zero &&
t.AllDebt().Value > 30.ToMinutes()
orderby t.AllBreakingPoint().TotalMinutes ascending
select new { t,
BreakingPoint = t.AllBreakingPoint(),
Debt = t.AllDebt(),
AnnualInterest = t.AllAnnualInterest(),
Issues = t.AllIssues(),
t.NbLinesOfCode,
// t.PercentageCoverage, to uncomment if coverage data is imported
DebtRating = t.DebtRating(),
DebtRatio = t.DebtRatio()
}
//<Desc>
// Lists types per increasing **Debt Breaking Point**: the duration from now
// when the cost of leaving issues unfixed (*Annual Interest*) equals the fix
// effort (*Debt*). The shorter, the higher the fix ROI.
//
// New/refactored types since baseline often come first: their issues get a
// higher *Annual Interest* since new issues should be fixed first.
//
// --
//
// Issues on the type and its members are both counted. Only types with at
// least 30 minutes of Debt are listed (*Breaking Point* makes less sense on
// small Debt). Type size and coverage are shown since untested code generates
// much Debt (uncomment *t.PercentageCoverage* once coverage data is
// imported); *Debt Rating* and *Debt Ratio* too.
//</Desc>
./rules/assets/img/Info32x32.png
Issues to Fix Priority - Hot Spots
Lists issues per increasing
Debt Breaking Point: the duration from now when the cost of leaving the issue unfixed (
Annual Interest) equals the fix effort (
Debt). The shorter, the higher the fix ROI. Double-click an issue to edit its rule and see all its information.
Issues on new/refactored code since baseline often come first: they get a higher
Annual Interest since new issues should be fixed first.
More documentation:
https://www.ndepend.com/docs/technical-debt
// <Name>Issues to Fix Priority</Name>
from i in Issues
// Don't show first issues with BreakingPoint equals to zero.
orderby i.BreakingPoint != TimeSpan.Zero ? i.BreakingPoint : TimeSpan.MaxValue
select new { i,
Debt = i.Debt,
AnnualInterest = i.AnnualInterest,
BreakingPoint = i.BreakingPoint,
CodeElement = i.CodeElement
}
//<Desc>
// Lists issues per increasing **Debt Breaking Point**: the duration from now
// when the cost of leaving the issue unfixed (*Annual Interest*) equals the
// fix effort (*Debt*). The shorter, the higher the fix ROI. Double-click an
// issue to edit its rule and see all its information.
//
// Issues on new/refactored code since baseline often come first: they get a
// higher *Annual Interest* since new issues should be fixed first.
//
// More documentation: https://www.ndepend.com/docs/technical-debt
//</Desc>
./rules/assets/img/Info32x32.png
Debt and Issues per Rule - Hot Spots
Lists violated rules with most
Debt (estimated fix effort) first.
--
Debt estimates distance from clean code, not fix priority.
Annual Interest estimates the annual cost of leaving issues unfixed (
Severity derives from it). The
Debt Breaking Point is when the cost of leaving an issue unfixed equals the fix effort: the shorter, the higher the ROI — the
Breaking Point is the right metric to prioritize issues fix.
--
Group rules by
Rule Category to see which categories generate most
Debt (typically
Code Coverage by Tests,
Architecture and
Code Smells).
More documentation:
https://www.ndepend.com/docs/technical-debt
// <Name>Debt and Issues per Rule</Name>
from r in Rules
where r.IsViolated()
orderby r.Debt().Value descending
select new {
r,
Issues = r.Issues(),
Debt = r.Debt(),
AnnualInterest = r.AnnualInterest(),
BreakingPoint = r.BreakingPoint(),
Category = r.Category
}
//<Desc>
// Lists violated rules with most *Debt* (estimated fix effort) first.
//
// --
//
// *Debt* estimates distance from clean code, not fix priority. *Annual
// Interest* estimates the annual cost of leaving issues unfixed (*Severity*
// derives from it). The **Debt Breaking Point** is when the cost of leaving
// an issue unfixed equals the fix effort: the shorter, the higher the ROI —
// the **Breaking Point is the right metric to prioritize issues fix**.
//
// --
//
// Group rules by *Rule Category* to see which categories generate most *Debt*
// (typically *Code Coverage by Tests*, *Architecture* and *Code Smells*).
//
// More documentation: https://www.ndepend.com/docs/technical-debt
//</Desc>
./rules/assets/img/Info32x32.png
New Debt and Issues per Rule - Hot Spots
Lists violated rules with new issues since baseline, most
new Debt (estimated fix effort) first.
--
New issues stem from recent refactoring: the code is fresh in developers minds, so fixing now costs less than later, and it fosters quality practices like unit-testing and avoiding complexity.
--
Group rules by
Rule Category to see which categories generate most
Debt (typically
Code Coverage by Tests,
Architecture and
Code Smells).
More documentation:
https://www.ndepend.com/docs/technical-debt
// <Name>New Debt and Issues per Rule</Name>
from r in Rules
where r.IsViolated() && r.IssuesAdded().Count() > 0
orderby r.DebtDiff().Value descending
select new {
r,
IssuesAdded = r.IssuesAdded(),
IssuesFixed = r.IssuesFixed(),
Issues = r.Issues(),
Debt = r.Debt(),
DebtDiff = r.DebtDiff(),
Category = r.Category
}
//<Desc>
// Lists violated rules with new issues since baseline, most **new Debt**
// (estimated fix effort) first.
//
// --
//
// New issues stem from recent refactoring: the code is fresh in developers
// minds, so fixing now costs less than later, and it fosters quality
// practices like unit-testing and avoiding complexity.
//
// --
//
// Group rules by *Rule Category* to see which categories generate most *Debt*
// (typically *Code Coverage by Tests*, *Architecture* and *Code Smells*).
//
// More documentation: https://www.ndepend.com/docs/technical-debt
//</Desc>
./rules/assets/img/Info32x32.png
Debt and Issues per Code Element - Hot Spots
Lists code elements with issues, most Debt (estimated fix effort) first.
Debt estimates distance from clean code, not fix priority. Annual Interest estimates the annual cost of leaving issues unfixed (Severity derives from it). The Debt Breaking Point is when the cost of leaving issues unfixed equals the fix effort: the shorter, the higher the ROI — the Breaking Point is the right metric to prioritize issues fix.
// <Name>Debt and Issues per Code Element</Name>
from elem in CodeElements
where elem.HasIssue()
orderby elem.Debt().Value descending
select new {
elem,
Issues = elem.Issues(),
Debt = elem.Debt(),
AnnualInterest = elem.AnnualInterest(),
BreakingPoint = elem.BreakingPoint()
}
//<Desc>
// Lists code elements with issues, most *Debt* (estimated fix effort) first.
//
// *Debt* estimates distance from clean code, not fix priority. *Annual
// Interest* estimates the annual cost of leaving issues unfixed (*Severity*
// derives from it). The **Debt Breaking Point** is when the cost of leaving
// issues unfixed equals the fix effort: the shorter, the higher the ROI —
// the **Breaking Point is the right metric to prioritize issues fix**.
//</Desc>
./rules/assets/img/Info32x32.png
New Debt and Issues per Code Element - Hot Spots
Lists code elements with new issues since baseline, most new Debt (estimated fix effort) first.
New issues stem from recent refactoring: the code is fresh in developers minds, so fixing now costs less than later, and it fosters quality practices like unit-testing and avoiding complexity.
// <Name>New Debt and Issues per Code Element</Name>
from elem in CodeElements
where elem.HasIssue() && elem.IssuesAdded().Count() > 0
orderby elem.DebtDiff().Value descending
select new {
elem,
IssuesAdded = elem.IssuesAdded(),
IssuesFixed = elem.IssuesFixed(),
Issues = elem.Issues(),
Debt = elem.Debt(),
DebtDiff = elem.DebtDiff()
}
//<Desc>
// Lists code elements with new issues since baseline, most **new Debt**
// (estimated fix effort) first.
//
// New issues stem from recent refactoring: the code is fresh in developers
// minds, so fixing now costs less than later, and it fosters quality
// practices like unit-testing and avoiding complexity.
//</Desc>
./rules/assets/img/StatusBigOk32x32.png
Avoid types too big - Code Smells
Matches types with more than 200 lines of code (
JustMyCode methods only): extremely complex to develop and maintain. Maybe a
God Class: a class controlling too many others, grown into
The Class That Does Everything. NbLinesOfCode metric:
https://www.ndepend.com/docs/code-metrics#NbLinesOfCodeHow to Fix:Split into smaller types. Refactoring a
God Class takes patience:
- Split the logic into smaller classes (possibly private nested ones the
God Class becomes composed of).
- Partition by responsibilities: look for subsets of methods strongly coupled with subsets of fields.
- If mostly logic (few states), consider static classes with only pure static methods (no field read/assigned), which are easily testable.
- Keep the
God Class interface at first, delegating to the extracted classes, until it is a pure facade; then keep or discard it.
- Write tests for each method before extracting it.
Estimated Debt: linear from 1 hour (200 loc) to 10 hours (2.000+ loc). Static types with no mutable fields cost less: their static methods are easily moved between types.
// <Name>Avoid types too big</Name>
// <Id>ND1000:AvoidTypesTooBig</Id>
warnif count > 0 from t in JustMyCode.Types where
// First filter on type to optimize
t.NbLinesOfCode > 200
// # IL Instructions is commented, because with LINQ syntax, a few statements can compile to hundreds of IL instructions.
// || t.NbILInstructions > 3000
// What matters is the # lines of code in JustMyCode
let locJustMyCode = t.MethodsAndConstructors.Where(m => JustMyCode.Contains(m)).Sum(m => m.NbLinesOfCode)
where locJustMyCode > 200
let isStaticWithNoMutableState = (t.IsStatic && t.Fields.Any(f => !f.IsImmutable))
let staticFactor = (isStaticWithNoMutableState ? 0.2 : 1)
orderby locJustMyCode descending
select new {
t,
locJustMyCode,
t.NbILInstructions,
t.Methods,
t.Fields,
Debt = (staticFactor*locJustMyCode.Linear(200, 1, 2000, 10)).ToHours().ToDebt(),
// The annual interest varies linearly from interest for severity major for 300 loc
// to interest for severity critical for 2000 loc
AnnualInterest = staticFactor*(locJustMyCode.Linear(
200, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes,
2000, Severity.Critical.AnnualInterestThreshold().Value.TotalMinutes)).ToMinutes().ToAnnualInterest()
}
//<Expl>
// The *{0}* has {1} statements considered as handcrafted code.
// This is too large suggesting the need to split it into multiple
// smaller implementations, each dedicated to a specific responsibility.
// </Expl>
//<Desc>
// Matches types with more than 200 lines of code (**JustMyCode methods
// only**): extremely complex to develop and maintain. Maybe a **God Class**:
// a class controlling too many others, grown into *The Class That Does
// Everything*. NbLinesOfCode metric:
// https://www.ndepend.com/docs/code-metrics#NbLinesOfCode
//</Desc>
//<HowToFix>
// Split into smaller types. Refactoring a *God Class* takes patience:
//
// - Split the logic into smaller classes (possibly private nested ones the
// *God Class* becomes composed of).
//
// - Partition by responsibilities: look for subsets of methods strongly
// coupled with subsets of fields.
//
// - If mostly logic (few states), consider static classes with only pure
// static methods (no field read/assigned), which are easily testable.
//
// - Keep the *God Class* interface at first, delegating to the extracted
// classes, until it is a pure facade; then keep or discard it.
//
// - Write tests for each method before extracting it.
//
// Estimated Debt: linear from 1 hour (200 loc) to 10 hours (2.000+ loc).
// Static types with no mutable fields cost less: their static methods are
// easily moved between types.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid types with too many methods - Code Smells
Matches types with more than 20 methods (constructors, property and event accessors excluded): hard to understand and maintain, symptom of too many responsibilities, maybe a God Class (The Class That Does Everything). Types with only constant fields are skipped: many stateless methods is ok.
How to Fix:
See HowToFix advices of the default rule Types too Big.
Estimated Debt: linear from 1 hour (20 methods) to 10 hours (200+). Static types with no mutable fields cost less: their static methods are easily moved between types.
// <Name>Avoid types with too many methods</Name>
// <Id>ND1001:AvoidTypesWithTooManyMethods</Id>
warnif count > 0 from t in JustMyCode.Types
// Optimization: Fast discard of non-relevant types
where !t.IsInterface && // Specially handled by the rule 'Avoid interfaces too big'
t.NbMethods > 20 &&
// Require at least a non-constant field
// because it is ok to have a class with many stateless methods
t.Fields.Any(f => !f.IsLiteral)
// Don't match these methods
let methods = t.Methods.Where(
m => !(m.IsGeneratedByCompiler ||
// m.IsConstructor || m.IsClassConstructor || // ctor/cctor not enumerated through IType.Methods
m.IsPropertyGetter || m.IsPropertySetter ||
m.IsEventAdder || m.IsEventRemover))
where methods.Count() > 20
orderby methods.Count() descending
let isStaticWithNoMutableState = (t.IsStatic && t.Fields.Any(f => !f.IsImmutable))
let staticFactor = (isStaticWithNoMutableState ? 0.2 : 1)
select new {
t,
nbMethods = methods.Count(),
instanceMethods = methods.Where(m => !m.IsStatic),
staticMethods = methods.Where(m => m.IsStatic),
t.NbLinesOfCode,
Debt = (staticFactor*methods.Count().Linear(20, 1, 200, 10)).ToHours().ToDebt(),
// The annual interest varies linearly from interest for severity major for 30 methods
// to interest for severity critical for 200 methods
AnnualInterest = (staticFactor*methods.Count().Linear(
20, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes,
200, Severity.Critical.AnnualInterestThreshold().Value.TotalMinutes)).ToMinutes().ToAnnualInterest()
}
//<Expl>The *{0}* has {1} methods. This is too many suggesting
// the need to divide it into multiple smaller implementations,
// each focused on a single responsibility.</Expl>
//<Desc>
// Matches types with more than 20 methods (constructors, property and event
// accessors excluded): hard to understand and maintain, symptom of too many
// responsibilities, maybe a **God Class** (*The Class That Does Everything*).
// Types with only constant fields are skipped: many stateless methods is ok.
//</Desc>
//<HowToFix>
// See *HowToFix advices* of the default rule **Types too Big**.
//
// Estimated Debt: linear from 1 hour (20 methods) to 10 hours (200+).
// Static types with no mutable fields cost less: their static methods are
// easily moved between types.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid types with too many fields - Code Smells
Matches types with more than 15 fields (constants, static-readonly and enumerations excluded): hard to understand and maintain, symptom of too many responsibilities.
How to Fix:
Group subsets of fields into smaller types and dispatch the methods logic into them. More advices in Types too Big, HowToFix section.
Estimated Debt: linear from 1 hour (15 fields) to 10 hours (200+).
// <Name>Avoid types with too many fields</Name>
// <Id>ND1002:AvoidTypesWithTooManyFields</Id>
warnif count > 0
// Unity class tend to have more fields.
// Hence set a threshold of 30 instead of 15 for other kinds of projects.
let threshold = ThirdParty.Assemblies.Any(a => a.Name.StartsWith("UnityEngine")) ? 30 : 15
from t in JustMyCode.Types
// Optimization: Fast discard of non-relevant types
where !t.IsEnumeration &&
t.Fields.Count() > threshold
// Count instance fields and non-constant static fields
let fields = t.Fields.Where(f =>
!f.IsGeneratedByCompiler &&
!f.IsLiteral &&
!(f.IsStatic && f.IsInitOnly) &&
JustMyCode.Contains(f) )
where fields.Count() > threshold
let methodsAssigningFields = fields.SelectMany(f => f.MethodsAssigningMe)
orderby fields.Count() descending
select new {
t,
instanceFields = fields.Where(f => !f.IsStatic),
staticFields = fields.Where(f => f.IsStatic),
nbFields = fields.Count(),
methodsAssigningFields ,
// See definition of Size of Instances metric here:
// https://www.ndepend.com/docs/code-metrics#SizeOfInst
t.SizeOfInst,
Debt = fields.Count().Linear(15, 1, 200, 10).ToHours().ToDebt(),
// The annual interest varies linearly from interest for severity major for 30 methods
// to interest for severity critical for 200 methods
AnnualInterest = fields.Count().Linear(15, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes,
200, Severity.Critical.AnnualInterestThreshold().Value.TotalMinutes).ToMinutes().ToAnnualInterest()
}
//<Expl>The *{0}* has {3} fields.
// This may be excessive, and you might need to consider grouping
// related fields into smaller and more cohesive implementations.
// </Expl>
//<Desc>
// Matches types with more than 15 fields (constants, static-readonly and
// enumerations excluded): hard to understand and maintain, symptom of too
// many responsibilities.
//</Desc>
//<HowToFix>
// Group subsets of fields into smaller types and dispatch the methods logic
// into them. More advices in **Types too Big**, *HowToFix* section.
//
// Estimated Debt: linear from 1 hour (15 fields) to 10 hours (200+).
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid methods too big, too complex - Code Smells
Matches methods where
ILNestingDepth > 2 and
CyclomaticComplexity > 17: hard to understand and maintain, maybe a
God Method (
The Method That Does Everything, grown by adding
ifs instead of new methods). Metrics:
https://www.ndepend.com/docs/code-metrics#CC https://www.ndepend.com/docs/code-metrics#ILNestingDepthHow to Fix:Split into smaller methods or even new classes. Question each local variable's scope: it may become an instance field of the new class(es). Large
switch…case can become types implementing a common interface, polymorphism replacing the
switch cases tests. Write tests for each method before extracting it.
Estimated Debt: 20 minutes to 3 hours, linear in a weighted complexity score.
// <Name>Avoid methods too big, too complex</Name>
// <Id>ND1003:AvoidMethodsTooBigTooComplex</Id>
warnif count > 0 from m in JustMyCode.Methods where
// Don't match async methods here to avoid
// false positives because of special compiler tricks.
!m.IsAsync &&
m.ILNestingDepth > 2 &&
m.CyclomaticComplexity > 17
let complexityScore = m.NbLinesOfCode/2 + m.CyclomaticComplexity + m.ILCyclomaticComplexity/3 + 3*m.ILNestingDepth
orderby complexityScore descending,
m.CyclomaticComplexity descending,
m.ILCyclomaticComplexity descending,
m.ILNestingDepth descending
select new {
m,
m.NbLinesOfCode,
m.CyclomaticComplexity,
m.ILCyclomaticComplexity,
m.ILNestingDepth,
complexityScore,
Debt = complexityScore.Linear(30, 20, 400, 3*60).ToMinutes().ToDebt(),
// The annual interest varies linearly from interest for severity minor
// to interest for severity major
AnnualInterest = complexityScore .Linear(30, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes,
200, 2*(Severity.High.AnnualInterestThreshold().Value.TotalMinutes)).ToMinutes().ToAnnualInterest()
}
//<Expl>
// The *{0}* has a high *cyclomatic complexity* score of {2} and should be refactored into a set of smaller methods.
//</Expl>
//<Desc>
// Matches methods where *ILNestingDepth* > 2 and *CyclomaticComplexity* > 17:
// hard to understand and maintain, maybe a **God Method** (*The Method That
// Does Everything*, grown by adding *if*s instead of new methods).
// Metrics: https://www.ndepend.com/docs/code-metrics#CC
// https://www.ndepend.com/docs/code-metrics#ILNestingDepth
//</Desc>
//<HowToFix>
// Split into smaller methods or even new classes. Question each local
// variable's scope: it may become an instance field of the new class(es).
// Large *switch…case* can become types implementing a common interface,
// polymorphism replacing the *switch cases tests*. Write tests for each
// method before extracting it.
//
// Estimated Debt: 20 minutes to 3 hours, linear in a weighted complexity
// score.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid methods with too many parameters - Code Smells
Matches methods with 8 or more parameters: painful to call, possible performance cost.
NbParameters metric:
https://www.ndepend.com/docs/code-metrics#NbParameters
Overrides of third-party methods and constructors calling a base constructor with 8+ parameters are skipped (lower-level problem).
How to Fix:Add properties/fields to the declaring type, or pass a dedicated arguments class/structure (e.g.
System.Diagnostics.ProcessStartInfo passed to
System.Diagnostics.Process.Start(ProcessStartInfo)).
Estimated Debt: linear from 1 hour (8 parameters) to 6 hours (40+).
// <Name>Avoid methods with too many parameters</Name>
// <Id>ND1004:AvoidMethodsWithTooManyParameters</Id>
warnif count > 0 from m in JustMyCode.Methods where
m.NbParameters >= 8 &&
// Don't match a method that overrides a third-party method with many parameters
!m.OverriddensBase.Any(mo => mo.IsThirdParty) &&
// Don't match a constructor that calls a base constructor with many parameters
!(m.IsConstructor && m.MethodsCalled.Any(
mc => mc.IsConstructor &&
mc.NbParameters >= 8 &&
m.ParentType.DeriveFrom(mc.ParentType))) &&
// Don't match DllImport P/invoke methods with many parameters
!m.HasAttribute("System.Runtime.InteropServices.DllImportAttribute".AllowNoMatch())
orderby m.NbParameters descending
select new {
m,
m.NbParameters,
Debt = m.NbParameters.Linear(8, 1, 40, 6).ToHours().ToDebt(),
// The annual interest varies linearly from interest for severity Medium for 7 parameters
// to interest for severity Critical for 40 parameters
AnnualInterest = m.NbParameters.Linear(8, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes,
40, Severity.Critical.AnnualInterestThreshold().Value.TotalMinutes).ToMinutes().ToAnnualInterest()
}
//<Expl>
// The *{0}* has {1} parameters. This leads to confusion
// at each call site and negatively impacts overall maintainability.
//</Expl>
//<Desc>
// Matches methods with 8 or more parameters: painful to call, possible
// performance cost. *NbParameters* metric:
// https://www.ndepend.com/docs/code-metrics#NbParameters
//
// Overrides of third-party methods and constructors calling a base
// constructor with 8+ parameters are skipped (lower-level problem).
//</Desc>
//<HowToFix>
// Add properties/fields to the declaring type, or pass a dedicated arguments
// class/structure (e.g. *System.Diagnostics.ProcessStartInfo* passed to
// *System.Diagnostics.Process.Start(ProcessStartInfo)*).
//
// Estimated Debt: linear from 1 hour (8 parameters) to 6 hours (40+).
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid methods with too many overloads - Code Smells
Matches sets of methods with 7 overloads or more, typically introduced to accommodate combinations of input parameters: hard to maintain, ambiguous, less readable.
The
visitor design pattern http://en.wikipedia.org/wiki/Visitor_pattern requires a
Visit() overload per sub type, so overloads starting with "visit" or "dispatch" (case-insensitive) are skipped; adapt to your naming convention. Sometimes many overloads are fine, e.g. a conversion method covering all numeric types.
NbOverloads metric:
https://www.ndepend.com/docs/code-metrics#NbOverloads
Overrides of third-party methods and constructors calling a base constructor are not counted (lower-level problems).
How to Fix:Use optional parameters, named arguments or a parameter array (
params) instead.
Estimated Debt: 3 minutes per method overload.
// <Name>Avoid methods with too many overloads</Name>
// <Id>ND1005:AvoidMethodsWithTooManyOverloads</Id>
warnif count > 0
let max = 7
let lookup = JustMyCode.Methods.Where(m =>
m.NbOverloads >= max &&
!m.IsOperator && // Don't report operator overload
// Don't match overloads due to the visitor pattern, based on a naming convention.
!m.SimpleName.StartsWithAny("visit", "dispatch", StringComparison.OrdinalIgnoreCase)
).ToLookup(m => m.ParentType.FullName + "."+ m.SimpleName)
from @group in lookup
let overloads = @group.ToArray()
// Prune not fixable situations.
let overloadsPruned = overloads.Where(
m =>
// Don't match a method that overrides a third-party methods
!m.OverriddensBase.Any(mo => mo.IsThirdParty) &&
// Don't match a constructor that calls a base constructor.
!(m.IsConstructor && m.MethodsCalled.Any(
mc => mc.IsConstructor &&
m.ParentType.DeriveFrom(mc.ParentType)))
).ToArray()
where overloadsPruned.Length > max
orderby overloads.Length descending
select new {
m = @group.First(),
overloadsPruned,
nbOverloads = overloadsPruned.Length,
Debt = (3*overloads.Length).ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* has {2} overloads which leads to ambiguity and maintenance burden.
// Consider instead optional parameters, named arguments or parameter array (with the *params* keyword).
//</Expl>
//<Desc>
// Matches sets of methods with 7 overloads or more, typically introduced to
// accommodate combinations of input parameters: hard to maintain, ambiguous,
// less readable.
//
// The **visitor design pattern** http://en.wikipedia.org/wiki/Visitor_pattern
// requires a *Visit()* overload per sub type, so overloads starting with
// "visit" or "dispatch" (case-insensitive) are skipped; adapt to your naming
// convention. Sometimes many overloads are fine, e.g. a conversion method
// covering all numeric types. *NbOverloads* metric:
// https://www.ndepend.com/docs/code-metrics#NbOverloads
//
// Overrides of third-party methods and constructors calling a base
// constructor are not counted (lower-level problems).
//</Desc>
//<HowToFix>
// Use optional parameters, named arguments or a parameter array (*params*)
// instead.
//
// Estimated Debt: 3 minutes per method overload.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid methods potentially poorly commented - Code Smells
Matches methods with less than 10% comment lines and at least 20 lines of code; they might need more comments. Only comments in the method body are counted.
Comments metrics:
https://www.ndepend.com/docs/code-metrics#PercentageComment https://www.ndepend.com/docs/code-metrics#NbLinesOfCommentHow to Fix:Typically add comments — though clean code with well-named variables and methods, plus unit-tests, needs few comments. However
hacks (non-trivial code circumventing API limitations or bugs) must absolutely be commented: intention, need, and source of the solution.
Estimated Debt: linear from 2 minutes (10 uncommented loc) to 20 minutes (200+).
// <Name>Avoid methods potentially poorly commented</Name>
// <Id>ND1006:AvoidMethodsPotentiallyPoorlyCommented</Id>
warnif count > 0 from t in JustMyCode.Types where
// Entity Framework ModelSnapshot and DbContext and Migration have large uncommented methods.
!t.DeriveFrom("Microsoft.EntityFrameworkCore.Infrastructure.ModelSnapshot".AllowNoMatch()) &&
!t.DeriveFrom("Microsoft.EntityFrameworkCore.DbContext".AllowNoMatch()) &&
!t.DeriveFrom("Microsoft.EntityFrameworkCore.Migrations.Migration".AllowNoMatch())
from m in t.Methods where
m.PercentageComment <= 5 &&
m.NbLinesOfCode >= 30 &&
m.CyclomaticComplexity >=3 &&
JustMyCode.Contains(t)
let nbLinesOfCodeNotCommented = m.NbLinesOfCode - m.NbLinesOfComment
orderby nbLinesOfCodeNotCommented descending
select new {
m,
m.PercentageComment,
m.NbLinesOfCode,
m.NbLinesOfComment,
nbLinesOfCodeNotCommented,
Debt = nbLinesOfCodeNotCommented .Linear(20, 2, 200, 20).ToMinutes().ToDebt(),
// The annual interest varies linearly from interest for severity major for 300 loc
// to interest for severity critical for 2000 loc
AnnualInterest = m.PercentageComment.Linear(
0, 8 *(Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes),
20, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes).ToMinutes().ToAnnualInterest()
}
//<Expl>The *{0}* is sufficiently complex, consider adding more comments for clarity.</Expl>
//<Desc>
// Matches methods with less than 10% comment lines and at least 20 lines of
// code; they might need more comments. Only comments in the method body are
// counted. *Comments metrics*:
// https://www.ndepend.com/docs/code-metrics#PercentageComment
// https://www.ndepend.com/docs/code-metrics#NbLinesOfComment
//</Desc>
//<HowToFix>
// Typically add comments — though clean code with well-named variables and
// methods, plus unit-tests, needs few comments. However **hacks** (non-trivial
// code circumventing API limitations or bugs) must absolutely be commented:
// intention, need, and source of the solution.
//
// Estimated Debt: linear from 2 minutes (10 uncommented loc) to 20 minutes
// (200+).
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid types with poor cohesion - Code Smells
Matches types with
Lack Of Cohesion of Methods (LCOM, range [0-1]) higher than 0.84: a
poorly cohesive class. A class is utterly cohesive when all its methods use all its instance fields. Only types with enough methods and fields are matched to avoid bias. LCOM metric:
https://www.ndepend.com/docs/code-metrics#LCOMHow to Fix:Split the type into smaller, more cohesive types implementing the same logic. Right-click a matched type > "Show on the Dependency Graph" to visualize fields usage by methods and plan the refactoring.
Estimated Debt: linear from 5 minutes (low poorCohesionScore) to 4 hours (high).
// <Name>Avoid types with poor cohesion</Name>
// <Id>ND1007:AvoidTypesWithPoorCohesion</Id>
warnif count > 0 from t in JustMyCode.Types where
t.LCOM > 0.91 &&
t.NbFields > 10 &&
t.NbMethods > 10
// Don't take account of properties and events accessors and backing fields
let fields = t.Fields.Where(f => !f.IsStatic && f.ParentProperty == null && f.ParentEvent == null).ToArray()
let methods = t.Methods.Where(m => m.ParentProperty == null && m.ParentEvent == null).ToArray()
where fields.Length > 8 && methods.Length > 8
let poorCohesionScore = 1/(1.01 - t.LCOM)
orderby poorCohesionScore descending
select new {
t,
t.LCOM,
methods,
fields,
poorCohesionScore,
Debt = poorCohesionScore.Linear(5, 5, 50, 4*60).ToMinutes().ToDebt(),
// The annual interest varies linearly from interest for severity Medium for low poorCohesionScore
// to 4 times interest for severity High for high poorCohesionScore
AnnualInterest = poorCohesionScore.Linear(5, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes,
50, 4*(Severity.High.AnnualInterestThreshold().Value.TotalMinutes)).ToMinutes().ToAnnualInterest()
}
//<Expl>
// The *{0}* has a *Lack Of Cohesion of Methods* score of {1}. This means that
// its methods and fields are loosely related. Identify different responsibilities
// within the class and split them into separate classes if necessary.
//</Expl>
//<Desc>
// Matches types with **Lack Of Cohesion of Methods** (LCOM, range [0-1])
// higher than 0.84: a **poorly cohesive class**. A class is utterly cohesive
// when all its methods use all its instance fields. Only types with enough
// methods and fields are matched to avoid bias. LCOM metric:
// https://www.ndepend.com/docs/code-metrics#LCOM
//</Desc>
//<HowToFix>
// Split the type into smaller, more cohesive types implementing the same
// logic. Right-click a matched type > "Show on the Dependency Graph" to
// visualize fields usage by methods and plan the refactoring.
//
// Estimated Debt: linear from 5 minutes (low poorCohesionScore) to 4 hours
// (high).
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid methods with too many local variables - Code Smells
Matches methods with more than 15 variables: extremely complex, must be refactored (methods with more than 8 are already hard to maintain).
Variables are counted from compiled IL; compilers may add hidden variables (e.g. for lambdas), hence the threshold of 15 to avoid false positives.
How to Fix:
Split the method into smaller methods or even new classes. Question each local variable's scope: it may become an instance field of the new class(es).
Estimated Debt: linear from 10 minutes (15 variables) to 2 hours (80+).
// <Name>Avoid methods with too many local variables</Name>
// <Id>ND1008:AvoidMethodsWithTooManyLocalVariables</Id>
warnif count > 0 from m in JustMyCode.Methods where
m.NbVariables > 15
orderby m.NbVariables descending
select new {
m,
m.NbVariables,
Debt = m.NbVariables.Linear(15, 1, 80, 6).ToHours().ToDebt(),
// The annual interest varies linearly from interest for severity Medium for 15 variables
// to interest for severity Critical for 80 variables
AnnualInterest = m.NbVariables.Linear(15, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes,
80, Severity.Critical.AnnualInterestThreshold().Value.TotalMinutes).ToMinutes().ToAnnualInterest()
}
//<Expl>
// The *{0}* has {1} variables. This indicates potential complexity,
// suggesting that it might benefit from being broken down into smaller methods.
//</Expl>
//<Desc>
// Matches methods with more than 15 variables: extremely complex, must be
// refactored (methods with more than 8 are already hard to maintain).
//
// Variables are counted from compiled IL; compilers may add hidden variables
// (e.g. for lambdas), hence the threshold of 15 to avoid false positives.
//</Desc>
//<HowToFix>
// Split the method into smaller methods or even new classes. Question each
// local variable's scope: it may become an instance field of the new
// class(es).
//
// Estimated Debt: linear from 10 minutes (15 variables) to 2 hours (80+).
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid unmaintainable code - Code Smells
Detects methods with a low maintainability index and high cyclomatic complexity: hard to understand, error prone and risky to modify. Metrics definitions:
https://www.ndepend.com/docs/code-metrics#MaintainabilityIndex
https://www.ndepend.com/docs/code-metrics#CC
https://www.ndepend.com/docs/code-metrics#HalsteadVolumeHow to Fix:Refactor the method by splitting it into smaller, more focused components.
// <Name>Avoid unmaintainable code</Name>
// <Id>ND1009:AvoidUnmaintainableCode</Id>
warnif count > 0
from m in JustMyCode.Methods
where m.NbLinesOfCode >= 30 && m.MaintainabilityIndex <= 35 &&
m.CyclomaticComplexity != null && m.CyclomaticComplexity >= 20
orderby m.MaintainabilityIndex ascending
select new {
m,
m.MaintainabilityIndex,
m.CyclomaticComplexity,
m.NbLinesOfCode,
m.HalsteadVolume,
Debt =
(m.HalsteadVolume.Linear(1000, 6, 10000, 60) +
((40 - m.MaintainabilityIndex)/5f).Linear(1, 6, 20, 60))
.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* has a low maintainability index *{1}* and is overly
// complex. Consider breaking it down into smaller, more focused methods.
// Introducing new types where appropriate may also help improve clarity
// and maintainability.
//</Expl>
//<Desc>
// Detects methods with a low maintainability index and high cyclomatic
// complexity: hard to understand, error prone and risky to modify.
// Metrics definitions:
//
// https://www.ndepend.com/docs/code-metrics#MaintainabilityIndex
//
// https://www.ndepend.com/docs/code-metrics#CC
//
// https://www.ndepend.com/docs/code-metrics#HalsteadVolume
//</Desc>
//<HowToFix>
// Refactor the method by splitting it into smaller, more focused components.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
From now, all types added should respect basic quality principles - Code Smells Regression
Runs only in diff mode (baseline defined), on types added since baseline (easily modified to also match refactored types).
Matched types violate basic quality principles: too many methods, too many fields, or too many types used — often symptoms of too many responsibilities. Constructors, property/event accessors, constant and static-readonly fields, and enumerations are not counted.
How to Fix:
Split the type into smaller types implementing the same logic together.
Constant 10 minutes Debt: the fix effort is already estimated by the Code Smells rules. But severity is High, higher for new types: the time to improve them is now, before the next production release.
// <Name>From now, all types added should respect basic quality principles</Name>
// <Id>ND1100:FromNowAllTypesAddedShouldRespectBasicQualityPrinciples</Id>
warnif count > 0
// Gather typesMoved, types whose parent namespaces changed since baseline:
// removed on one side and added on the other, such a type is not a new type.
let typesRemoved = context.CompareContext.OlderCodeBase.Application.Types
.Where(t => t.WasRemoved()).Select(t => t.Name).ToHashSetEx()
let typesMoved = Application.Types
.Where(t => t.WasAdded() && typesRemoved.Contains(t.Name)).ToHashSetEx()
from t in JustMyCode.Types where
// Only match types added since Baseline.
// Uncomment this line to match also refactored types since Baseline.
// (t.WasAdded() || t.CodeWasChanged()) &&
t.WasAdded() &&
// Don't match types moved from one namespace to another
!typesMoved.Contains(t) &&
// Eliminate interfaces, enumerations or types only with constant fields
// by making sure we are matching type with significant code.
t.NbLinesOfCode >= 50 &&
// Optimization: Fast discard of non-relevant types
t.Fields.Count() >= 7 &&
t.Methods.Count() >= 18
// Count instance fields and non-constant static fields
let fields = t.Fields.Where(f =>
!f.IsLiteral &&
!(f.IsStatic && f.IsInitOnly))
// Don't match these methods
let methods = t.Methods.Where(
m => !(// m.IsConstructor || m.IsClassConstructor || // ctor/cctor not enumerated through IType.Methods
m.IsGeneratedByCompiler ||
m.IsPropertyGetter || m.IsPropertySetter ||
m.IsEventAdder || m.IsEventRemover))
let typesUsed = t.TypesUsed.ExceptThirdParty().ToArray()
where
// Low Quality types Metrics' definitions are available here:
// https://www.ndepend.com/docs/code-metrics#MetricsOnTypes
( // Types with too many methods
fields.Count() >= 7 &&
methods.Count() >= 18 &&
// Complex Types that use more than 40 application other types
typesUsed.Length >= 40
)
select new {
t,
t.NbLinesOfCode,
t.MaintainabilityIndex,
instanceMethods = methods.Where(m => !m.IsStatic),
staticMethods = methods.Where(m => m.IsStatic),
instanceFields = fields.Where(f => !f.IsStatic),
staticFields = fields.Where(f => f.IsStatic),
typesUsed,
// Constant Debt estimation, since for such type rules in category "Code Smells"
// accurately estimate the Debt.
Debt = 10.ToMinutes().ToDebt(),
// The Severity is higher for new types than for refactored types
AnnualInterest= (t.WasAdded() ? 3 : 1) *
Severity.High.AnnualInterestThreshold()
}
//<Expl>
// The new *{0}* doesn't meet quality standard detailed in other issues.
//</Expl>
//<Desc>
// Runs only in *diff mode* (baseline defined), on types added since baseline
// (easily modified to also match refactored types).
//
// Matched types violate basic quality principles: too many methods, too many
// fields, or too many types used — often symptoms of too many
// responsibilities. Constructors, property/event accessors, constant and
// static-readonly fields, and enumerations are not counted.
//</Desc>
//<HowToFix>
// Split the type into smaller types implementing the same logic together.
//
// Constant 10 minutes Debt: the fix effort is already estimated by the
// **Code Smells** rules. But severity is **High**, higher for new types:
// the time to improve them is **now**, before the next production release.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
From now, all types added should be 100% covered by tests - Code Smells Regression
Runs only in diff mode (baseline defined) and when coverage data is imported, on types added since baseline (easily modified to also match refactored types) not fully covered by tests.
The last ~10% of a class is as hard to cover as the first 90%, so teams often settle for 90%. But untestable code usually means poorly written, error prone code: most tricky bugs might come from this small portion of hard-to-test code. Not all classes need 100% coverage (e.g. UI), but most of the application logic should live in easy-to-test classes that are 100% covered.
How to Fix:
Write more unit-tests for uncovered code. Hard-to-test code signals a design needing refactoring. For code impossible to cover (like MessageBox.Show()), define an infrastructure to mock it at test-time.
Constant 10 minutes Debt: the testing effort is already estimated by the Code Coverage rules. But severity is High, higher for new types: the time to write tests is now, before the next production release.
// <Name>From now, all types added should be 100% covered by tests</Name>
// <Id>ND1101:FromNowAllTypesAddedShouldBe100PercentCoveredByTests</Id>
warnif count > 0
// Gather typesMoved, types whose parent namespaces changed since baseline
let typesRemoved = context.CompareContext.OlderCodeBase.Application.Types
.Where(t => t.WasRemoved()).Select(t => t.Name).ToHashSetEx()
let typesMoved = Application.Types
.Where(t => t.WasAdded() && typesRemoved.Contains(t.Name)).ToHashSetEx()
from t in JustMyCode.Types where
// Only match types added since Baseline.
// Uncomment this line to match also refactored types since Baseline.
// (t.WasAdded() || t.CodeWasChanged()) &&
t.WasAdded() &&
// …that are not 100% covered by tests
t.PercentageCoverage < 100 &&
// Don't match types moved from one namespace to another
!typesMoved.Contains(t)
let methodsCulprit = t.Methods.Where(m => m.PercentageCoverage < 100)
select new {
t,
t.PercentageCoverage,
methodsCulprit,
t.NbLinesOfCodeNotCovered,
t.NbLinesOfCode,
// Constant Debt estimation, since for such type rules in category "Coverage"
// accurately estimate the untested code Debt.
Debt = 10.ToMinutes().ToDebt(),
// The Severity is higher for new types than for refactored types
AnnualInterest= (t.WasAdded() ? 3 : 1) *
Severity.High.AnnualInterestThreshold()
}
//<Expl>
// The new *{0}* is {1}% covered by tests
// with {3} statement{3?:s} not covered.
// When adding a new type, it is important to also write
// associated unit tests to thoroughly validate its
// functionality before it goes to production.
//</Expl>
//<Desc>
// Runs only in *diff mode* (baseline defined) and when coverage data is
// imported, on types added since baseline (easily modified to also match
// refactored types) not fully covered by tests.
//
// The last ~10% of a class is as hard to cover as the first 90%, so teams
// often settle for 90%. But *untestable code* usually means *poorly written*,
// *error prone* code: **most tricky bugs might come from this small portion
// of hard-to-test code**. Not all classes need 100% coverage (e.g. UI), but
// most of the application logic should live in *easy-to-test classes* that
// are 100% covered.
//</Desc>
//<HowToFix>
// Write more unit-tests for uncovered code. *Hard-to-test code* signals a
// design needing refactoring. For code impossible to cover (like
// *MessageBox.Show()*), define an infrastructure to *mock* it at test-time.
//
// Constant 10 minutes Debt: the testing effort is already estimated by the
// **Code Coverage** rules. But severity is **High**, higher for new types:
// the time to write tests is **now**, before the next production release.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
From now, all methods added should respect basic quality principles - Code Smells Regression
Runs only in diff mode (baseline defined), on methods added since baseline (easily modified to also match refactored methods).
Matched methods violate basic quality principles: too large, too complex (many if, switch case, loops…), too many variables, parameters or overloads.
How to Fix:
Split the method into smaller methods or even new classes. Question each local variable's scope: it may become an instance field of the new class(es). Large switch…case can become types implementing a common interface, polymorphism replacing the switch cases tests. Write tests for each method before extracting it.
Constant 5 minutes Debt: the fix effort is already estimated by the Code Smells rules. But severity is High, higher for new methods: the time to improve them is now, before the next production release.
// <Name>From now, all methods added should respect basic quality principles</Name>
// <Id>ND1102:FromNowAllMethodsAddedShouldRespectBasicQualityPrinciples</Id>
warnif count > 0
// Gather typesMoved, types whose parent namespaces changed since baseline:
// the methods of such a type are added on one side and removed on the other, not new.
let typesRemoved = context.CompareContext.OlderCodeBase.Application.Types
.Where(t => t.WasRemoved()).Select(t => t.Name).ToHashSetEx()
let typesMoved = Application.Types
.Where(t => t.WasAdded() && typesRemoved.Contains(t.Name)).ToHashSetEx()
// Gather methodsMoved keys: a method removed from a type of the baseline and added, with the
// same name (signature embedded), lines of code and IL size, to another type is relocated
// code (a class split into helpers), not a new method.
let methodsRemovedKeys = context.CompareContext.OlderCodeBase.Application.Methods
.Where(m => m.WasRemoved() && m.NbILInstructions >= 10)
.Select(m => m.Name + "|" + m.NbLinesOfCode.ToString() + "|" + m.NbILInstructions.ToString()).ToHashSetEx()
from m in JustMyCode.Methods where
// Only match methods added since Baseline.
// Uncomment this line to match also refactored methods since Baseline.
// (m.WasAdded() || m.CodeWasChanged()) &&
m.WasAdded() &&
// Don't match methods in types moved, nor methods relocated to another type
!typesMoved.Contains(m.ParentType) &&
!methodsRemovedKeys.Contains(m.Name + "|" + m.NbLinesOfCode.ToString() + "|" + m.NbILInstructions.ToString()) &&
// Don't match async methods here to avoid
// false positives because of special compiler tricks.
!m.IsAsync &&
!m.IsAbstract &&
// Only match methods with enough statements
m.NbLinesOfCode > 20 &&
// Low Quality methods// Metrics' definitions
((m.CyclomaticComplexity >= 12 && // https://www.ndepend.com/docs/code-metrics#CC
m.ILNestingDepth >= 4) || // https://www.ndepend.com/docs/code-metrics#ILNestingDepth
m.NbParameters >= 8) // https://www.ndepend.com/docs/code-metrics#NbParameters
// Don't match methods considered as new because its parameters changed
&& (!m.ParentType.IsPresentInBothBuilds() ||
!m.ParentType.OlderVersion().MethodsAndConstructors.Any(m1 => m1.WasRemoved() && m1.SimpleNameEquals(m.SimpleName)))
select new {
m,
m.NbLinesOfCode,
m.CyclomaticComplexity,
m.ILCyclomaticComplexity,
m.ILNestingDepth,
m.NbParameters,
// Constant Debt estimation, since for such method rules in category "Code Smells"
// accurately estimate the Debt.
Debt = 5.ToMinutes().ToDebt(),
// The Severity is higher for new methods than for refactored methods
AnnualInterest= (m.WasAdded() ? 3 : 1) *
Severity.High.AnnualInterestThreshold()
}
//<Expl>
// The new *{0}* doesn't meet quality standard detailed in other issues.
//</Expl>
//<Desc>
// Runs only in *diff mode* (baseline defined), on methods added since
// baseline (easily modified to also match refactored methods).
//
// Matched methods violate basic quality principles: too large, too complex
// (many *if*, *switch case*, loops…), too many variables, parameters or
// overloads.
//</Desc>
//<HowToFix>
// Split the method into smaller methods or even new classes. Question each
// local variable's scope: it may become an instance field of the new
// class(es). Large *switch…case* can become types implementing a common
// interface, polymorphism replacing the *switch cases tests*. Write tests
// for each method before extracting it.
//
// Constant 5 minutes Debt: the fix effort is already estimated by the
// **Code Smells** rules. But severity is **High**, higher for new methods:
// the time to improve them is **now**, before the next production release.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid decreasing code coverage by tests of types - Code Smells Regression
Runs only in diff mode (baseline defined) and when coverage data is imported. Warns when a type's covered lines decreased since baseline (for refactored types, accounting for lines of code added).
This can mean tests were removed, but more often that changes haven't been properly covered by tests.
To visualize changes, right-click a matched type and select:
- Compare older and newer versions of source file
- or Compare older and newer versions decompiled with Reflector
How to Fix:
Write more unit-tests covering the changes. Hard-to-test code signals a design needing refactoring.
Estimated Debt: linear from 15 minutes to 3 hours with the number of lines no longer covered. Severity varies from High to Critical likewise: fix these issues now, before such code gets released in production.
// <Name>Avoid decreasing code coverage by tests of types</Name>
// <Id>ND1103:AvoidDecreasingCodeCoverageByTestsOfTypes</Id>
warnif count > 0
from t in JustMyCode.Types where
t.IsPresentInBothBuilds() && t.CoverageDataAvailable && t.OlderVersion().CoverageDataAvailable
&& t.NbLinesOfCode.HasValue && t.OlderVersion().NbLinesOfCode.HasValue
let locDiff = (int)t.NbLinesOfCode.Value - (int)t.OlderVersion().NbLinesOfCode.Value
where locDiff >= 0
let uncoveredLoc = (int)t.NbLinesOfCodeNotCovered.Value - ((int)t.OlderVersion().NbLinesOfCodeNotCovered.Value + locDiff)
where uncoveredLoc > 0
orderby uncoveredLoc descending
select new {
t,
OldCoveragePercent = t.OlderVersion().PercentageCoverage,
NewCoveragePercent = t.PercentageCoverage,
OldLoc = t.OlderVersion().NbLinesOfCode,
NewLoc = t.NbLinesOfCode,
uncoveredLoc,
Debt = uncoveredLoc.Linear(1, 15, 100, 3*60).ToMinutes().ToDebt(),
// The annual interest varies linearly from interest for severity High for one line of code that is not covered by tests anymore
// to interest for severity Critical for 50 lines of code that are not covered by tests anymore
AnnualInterest = uncoveredLoc.Linear(1, Severity.High.AnnualInterestThreshold().Value.TotalMinutes,
50, 2*Severity.Critical.AnnualInterestThreshold().Value.TotalMinutes).ToMinutes().ToAnnualInterest()
}
//<Expl>
// The *{0}* used to be {1}% covered by tests in the baseline and is
// now {2}% covered, with {5} new statement{5?:s} not being covered.
// Write additional unit tests to cover changes.
// Identifying *hard-to-test* code often indicates *poor design*
// that needs refactoring.
//</Expl>
//<Desc>
// Runs only in *diff mode* (baseline defined) and when coverage data is
// imported. Warns when a type's covered lines decreased since baseline (for
// refactored types, accounting for lines of code added).
//
// This can mean tests were removed, but more often that changes haven't been
// properly covered by tests.
//
// To visualize changes, right-click a matched type and select:
//
// - Compare older and newer versions of source file
//
// - or Compare older and newer versions decompiled with Reflector
//</Desc>
//<HowToFix>
// Write more unit-tests covering the changes. *Hard-to-test code* signals a
// design needing refactoring.
//
// Estimated Debt: linear from 15 minutes to 3 hours with the number of lines
// no longer covered. Severity varies from **High** to **Critical** likewise:
// fix these issues **now**, before such code gets released in production.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid making complex methods even more complex - Code Smells Regression
Runs only in
diff mode (baseline defined). Warns when an already complex method (
Cyclomatic Complexity > 6) became even more complex since baseline. Metric:
https://www.ndepend.com/docs/code-metrics#CC
Requires PDB files and source code at analysis time. See:
https://www.ndepend.com/docs/ndepend-analysis-inputs-explanation
To visualize changes, right-click a matched method and select:
- Compare older and newer versions of source file
- or Compare older and newer versions decompiled with Reflector
How to Fix:Split into smaller methods or even new classes. Question each local variable's scope: it may become an instance field of the new class(es). Large
switch…case can become types implementing a common interface, polymorphism replacing the
switch cases tests. Write tests for each method before extracting it.
Estimated Debt: linear from 15 to 60 minutes with the extra complexity. Severity is
High: fix these issues
now, before such code gets released in production.
// <Name>Avoid making complex methods even more complex</Name>
// <Id>ND1104:AvoidMakingComplexMethodsEvenMoreComplex</Id>
warnif count > 0
let complexityScoreProc = new Func<IMethod, double>(m =>
(m.CyclomaticComplexity + m.ILCyclomaticComplexity/3 + 2*m.ILNestingDepth).Value)
from m in JustMyCode.Methods where
// Don't match async methods here to avoid
// false positives because of special compiler tricks.
!m.IsAsync &&
!m.IsAbstract &&
m.IsPresentInBothBuilds() &&
m.CodeWasChanged() &&
m.OlderVersion().CyclomaticComplexity > 6
let complexityScore = complexityScoreProc(m)
let oldComplexityScore = complexityScoreProc(m.OlderVersion())
where complexityScore > oldComplexityScore
let complexityScoreDiff = complexityScoreProc(m) - complexityScoreProc(m.OlderVersion())
orderby complexityScoreDiff descending
select new {
m,
oldComplexityScore ,
complexityScore ,
diff= complexityScoreDiff,
Debt = complexityScoreDiff.Linear(1, 15, 50, 60).ToMinutes().ToDebt(),
// The annual interest varies linearly from interest for severity Medium for a tiny complexity increment
// to interest for severity critical for 2000 loc
AnnualInterest = complexityScoreDiff.Linear(1, Severity.High.AnnualInterestThreshold().Value.TotalMinutes,
50, 4*(Severity.High.AnnualInterestThreshold().Value.TotalMinutes)).ToMinutes().ToAnnualInterest()
}
//<Expl>
// The *{0}* was already too complex in the baseline
// and is now even more complex.
// Consider splitting it into smaller, less complex methods.
//</Expl>
//<Desc>
// Runs only in *diff mode* (baseline defined). Warns when an already complex
// method (*Cyclomatic Complexity* > 6) became even more complex since
// baseline. Metric: https://www.ndepend.com/docs/code-metrics#CC
//
// Requires PDB files and source code at analysis time. See:
// https://www.ndepend.com/docs/ndepend-analysis-inputs-explanation
//
// To visualize changes, right-click a matched method and select:
//
// - Compare older and newer versions of source file
//
// - or Compare older and newer versions decompiled with Reflector
//</Desc>
//<HowToFix>
// Split into smaller methods or even new classes. Question each local
// variable's scope: it may become an instance field of the new class(es).
// Large *switch…case* can become types implementing a common interface,
// polymorphism replacing the *switch cases tests*. Write tests for each
// method before extracting it.
//
// Estimated Debt: linear from 15 to 60 minutes with the extra complexity.
// Severity is **High**: fix these issues **now**, before such code gets
// released in production.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid making large methods even larger - Code Smells Regression
Runs only in
diff mode (baseline defined). Warns when an already large method (more than 15 lines of code) became even larger since baseline. Metric:
https://www.ndepend.com/docs/code-metrics#NbLinesOfCode
Requires PDB files at analysis time. See:
https://www.ndepend.com/docs/ndepend-analysis-inputs-explanation
To visualize changes, right-click a matched method and select:
- Compare older and newer versions of source file
- or Compare older and newer versions decompiled with Reflector
How to Fix:Usually split too big methods into smaller ones. But long branch-free methods (typically data initialization) are not necessarily a problem.
Estimated Debt: linear from 10 to 60 minutes with the lines of code added. Severity is
High: fix these issues
now, before such code gets released in production.
// <Name>Avoid making large methods even larger</Name>
// <Id>ND1105:AvoidMakingLargeMethodsEvenLarger</Id>
warnif count > 0
from m in JustMyCode.Methods where
!m.IsAbstract &&
// Eliminate constructors from match, since they get larger
// as soon as some field initializations are added.
!m.IsConstructor &&
!m.IsClassConstructor &&
// Filter just here for optimization
m.NbLinesOfCode > 25 &&
m.IsPresentInBothBuilds() &&
m.CodeWasChanged()
let oldLoc = m.OlderVersion().NbLinesOfCode
where oldLoc > 25 && m.NbLinesOfCode > oldLoc
let diff = m.NbLinesOfCode - oldLoc
where diff > 0
orderby diff descending
select new {
m,
oldLoc,
newLoc = m.NbLinesOfCode,
diff,
Debt = diff.Linear(1, 10, 100, 60).ToMinutes().ToDebt(),
// The annual interest varies linearly from interest for severity Medium for a tiny complexity increment
// to interest for severity critical for 2000 loc
AnnualInterest = diff .Linear(1, Severity.High.AnnualInterestThreshold().Value.TotalMinutes,
100, 4*(Severity.High.AnnualInterestThreshold().Value.TotalMinutes)).ToMinutes().ToAnnualInterest()
}
//<Expl>
// The *{0}* was already large in the baseline, comprising {1} statements,
// and has now expanded further, encompassing {2} statements.
// The implementation of a method should be concise enough to fit comfortably
// within the vertical screen space, ensuring readability.
// </Expl>
//<Desc>
// Runs only in *diff mode* (baseline defined). Warns when an already large
// method (more than 15 lines of code) became even larger since baseline.
// Metric: https://www.ndepend.com/docs/code-metrics#NbLinesOfCode
//
// Requires PDB files at analysis time. See:
// https://www.ndepend.com/docs/ndepend-analysis-inputs-explanation
//
// To visualize changes, right-click a matched method and select:
//
// - Compare older and newer versions of source file
//
// - or Compare older and newer versions decompiled with Reflector
//</Desc>
//<HowToFix>
// Usually split too big methods into smaller ones. But long branch-free
// methods (typically data initialization) are not necessarily a problem.
//
// Estimated Debt: linear from 10 to 60 minutes with the lines of code added.
// Severity is **High**: fix these issues **now**, before such code gets
// released in production.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid adding methods to a type that already had many methods - Code Smells Regression
Runs only in diff mode (baseline defined). Lists types that already had more than 40 methods at baseline and got new ones. Such types are hard to maintain, symptom of too many responsibilities. Constructors and compiler-generated methods are not counted.
How to Fix:
Split the type into smaller types implementing the same logic together.
Estimated Debt: 10 minutes per method added. Severity is High: fix these issues now, before such code gets released in production.
// <Name>Avoid adding methods to a type that already had many methods</Name>
// <Id>ND1106:AvoidAddingMethodsToATypeThatAlreadyHadManyMethods</Id>
warnif count > 0
// Don't count constructors and methods generated by the compiler!
let getMethodsProc = new Func<IType, IList<IMethod>>(
t0 => t0.Methods.Where(m =>
//!m.IsConstructor && !m.IsClassConstructor && // ctor/cctor not enumerated through IType.Methods
!m.IsGeneratedByCompiler).ToArray())
from t in JustMyCode.Types where
t.NbMethods > 40 && // Just here for optimization
t.IsPresentInBothBuilds()
// Optimization: fast discard of non-relevant types
where t.OlderVersion().NbMethods > 40
let oldMethods = getMethodsProc(t.OlderVersion())
where oldMethods.Count > 40
let newMethods = getMethodsProc(t)
where newMethods.Count > oldMethods.Count
let addedMethods = newMethods.Where(m => m.WasAdded())
let removedMethods = oldMethods.Where(m => m.WasRemoved())
orderby addedMethods.Count() descending
select new {
t,
nbOldMethods = oldMethods.Count,
nbNewMethods = newMethods.Count,
addedMethods,
removedMethods,
Debt = (10*addedMethods.Count()).ToMinutes().ToDebt(),
AnnualInterest = addedMethods.Count().Linear(
1, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes,
100, 4*(Severity.High.AnnualInterestThreshold().Value.TotalMinutes)).ToMinutes().ToAnnualInterest()
}
//<Expl>
// The *{0}* previously contained {1} methods
// in the baseline and now has {2} methods.
// This increase is indicative of excessive complexity,
// suggesting the necessity of breaking it down into multiple
// smaller implementations, each dedicated to a single
// responsibility.
//</Expl>
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists types that already had
// more than 40 methods at baseline and got new ones. Such types are hard to
// maintain, symptom of too many responsibilities. Constructors and
// compiler-generated methods are not counted.
//</Desc>
//<HowToFix>
// Split the type into smaller types implementing the same logic together.
//
// Estimated Debt: 10 minutes per method added. Severity is **High**: fix
// these issues **now**, before such code gets released in production.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid adding instance fields to a type that already had many instance fields - Code Smells Regression
Runs only in diff mode (baseline defined). Lists types that already had more than 15 fields at baseline and got new ones. Such types are hard to maintain, symptom of too many responsibilities. Constants, static-readonly fields and enumerations are not counted.
How to Fix:
Group subsets of fields into smaller types and dispatch the methods logic into them.
Estimated Debt: 10 minutes per field added. Severity is High: fix these issues now, before such code gets released in production.
// <Name>Avoid adding instance fields to a type that already had many instance fields</Name>
// <Id>ND1107:AvoidAddingInstanceFieldsToATypeThatAlreadyHadManyInstanceFields</Id>
warnif count > 0
let getFieldsProc = new Func<IType, IList<IField>>(
t0 => t0.Fields.Where(f =>
!f.IsLiteral &&
!f.IsGeneratedByCompiler &&
!f.IsStatic).ToArray())
from t in JustMyCode.Types where
!t.IsEnumeration &&
t.IsPresentInBothBuilds()
// Optimization: fast discard of non-relevant types
where t.OlderVersion().NbFields > 15
let oldFields = getFieldsProc(t.OlderVersion())
where oldFields.Count > 15
let newFields = getFieldsProc(t)
where newFields.Count > oldFields.Count
let addedFields = newFields.Where(f => f.WasAdded())
let removedFields = oldFields.Where(f => f.WasRemoved())
orderby addedFields.Count() descending
select new {
t,
nbOldFields = oldFields.Count,
nbNewFields = newFields.Count,
addedFields,
removedFields,
Debt = (10*addedFields.Count()).ToMinutes().ToDebt(),
AnnualInterest = addedFields.Count().Linear(
1, Severity.High.AnnualInterestThreshold().Value.TotalMinutes,
100, 4*(Severity.High.AnnualInterestThreshold().Value.TotalMinutes)).ToMinutes().ToAnnualInterest()
}
//<Expl>
// The *{0}* previously contained {1} instance fields
// in the baseline and now has {2} instance fields.
// This increase is indicative of excessive complexity,
// suggesting the necessity of breaking it down into multiple
// smaller implementations, each dedicated to a single
// responsibility.
//</Expl>
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists types that already had
// more than 15 fields at baseline and got new ones. Such types are hard to
// maintain, symptom of too many responsibilities. *Constants*,
// *static-readonly* fields and enumerations are not counted.
//</Desc>
//<HowToFix>
// Group subsets of fields into smaller types and dispatch the methods logic
// into them.
//
// Estimated Debt: 10 minutes per field added. Severity is **High**: fix
// these issues **now**, before such code gets released in production.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid transforming an immutable type into a mutable one - Code Smells Regression
Runs only in diff mode (baseline defined). Warns when an immutable type (instance fields unmodifiable after construction) became mutable.
Immutable instances can be used concurrently without synchronization, so users often rely on immutability; breaking it may break their code.
How to Fix:
If immutability matters for the matched type, refactor to preserve it.
Estimated Debt: 10 minutes plus 10 minutes per instance field now mutable. Severity is High: fix these issues now, before such code gets released in production.
//<Name>Avoid transforming an immutable type into a mutable one</Name>
// <Id>ND1108:AvoidTransformingAnImmutableTypeIntoAMutableOne</Id>
warnif count > 0
from t in Application.Types where
t.CodeWasChanged() &&
t.OlderVersion().IsImmutable &&
!t.IsImmutable &&
// Don't take account of immutable types transformed into static types (not deemed as immutable)
!t.IsStatic
let culpritFields = t.InstanceFields.Where(f => !f.IsImmutable)
select new {
t,
culpritFields,
Debt = (10 + 10*culpritFields.Count()).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* used to be immutable in the baseline
// and is now mutable because of the *{1}*.
// Avoid breaking immutability because client code
// might rely on this property.
//</Expl>
//<Desc>
// Runs only in *diff mode* (baseline defined). Warns when an *immutable*
// type (instance fields unmodifiable after construction) became mutable.
//
// Immutable instances can be used concurrently without synchronization, so
// users often rely on immutability; breaking it may break their code.
//</Desc>
//<HowToFix>
// If immutability matters for the matched type, refactor to preserve it.
//
// Estimated Debt: 10 minutes plus 10 minutes per instance field now mutable.
// Severity is **High**: fix these issues **now**, before such code gets
// released in production.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid interfaces too big - Object Oriented Design
Matches interfaces with more than 14 methods, properties or events (a property or event counts as one member). Interfaces should have a single responsibility; overly large ones have too many.
The Interface Segregation Principle (ISP) warns that a large interface used by consumers each needing only specific methods couples those consumers unnecessarily. Large interfaces also lead to larger classes handling too many unrelated tasks.
How to Fix:
Refactor into smaller single-responsibility interfaces, e.g. split IFoo handling read and write into IFooReader and IFooWriter. A large public interface with many clients may make splitting impactful or unfeasible.
Estimated Debt: linear from 20 minutes (10 methods) to 7 hours (100+), halved if the interface is not publicly visible (only the current project is impacted).
// <Name>Avoid interfaces too big</Name>
// <Id>ND1200:AvoidInterfacesTooBig</Id>
warnif count > 0
from i in JustMyCode.Types
where i.IsInterface && i.NbMethods >= 14 // Optimization First threshold
// Don't count property or event accessors within the sequence methods
let methods = i.Methods.Where(m => !m.IsPropertyGetter && !m.IsPropertySetter && !m.IsEventAdder && !m.IsEventRemover)
let membersCount = methods.Count() + i.Properties.Count() + i.Events.Count()
where membersCount >= 14
let publicFactor = i.IsPubliclyVisible ? 1 : 0.5
orderby membersCount descending
select new {
i,
Members= methods.Concat<IMember>(i.Properties).Concat(i.Events),
Events = i.Events,
membersCount,
Debt = (publicFactor*membersCount.Linear(10, 20, 100, 7*60)).ToMinutes().ToDebt(),
// The annual interest varies linearly from interest for severity Medium for an interface with 10 methods
// to interest for severity Critical for an interface with 100 methods and more
AnnualInterest = (publicFactor*membersCount .Linear(
10, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes,
100, Severity.Critical.AnnualInterestThreshold().Value.TotalMinutes))
.ToMinutes().ToAnnualInterest()
}
//<Expl> The *{0}* has {3} members. It is too large and it might indicate
// the need to split it into several smaller interfaces that each,
// handles a single responsibility.</Expl>
//<Desc>
// Matches interfaces with more than 14 methods, properties or events (a
// property or event counts as one member). Interfaces should have a single
// responsibility; overly large ones have too many.
//
// The *Interface Segregation Principle* (ISP) warns that a large interface
// used by consumers each needing only specific methods couples those
// consumers unnecessarily. Large interfaces also lead to larger classes
// handling too many unrelated tasks.
//</Desc>
//<HowToFix>
// Refactor into smaller *single-responsibility* interfaces, e.g. split *IFoo*
// handling read and write into *IFooReader* and *IFooWriter*. A large public
// interface with many clients may make splitting impactful or unfeasible.
//
// Estimated Debt: linear from 20 minutes (10 methods) to 7 hours (100+),
// halved if the interface is not publicly visible (only the current project
// is impacted).
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Base class should not use derivatives - Object Oriented Design
The
open/closed principle states:
software entities should be open for extension, but closed for modification.
http://en.wikipedia.org/wiki/Open/closed_principle
Creating or modifying a derived class shouldn't require
modifying the base class. A base class using its derivatives likely will, making extension no longer simple: bad design.
No warning when the derived class used is nested private in the base class (an encapsulated implementation detail).
How to Fix:Understand the need for using derivatives, imagine a new design, refactor. Typically, encapsulate base-class access to derived classes behind an abstract or virtual method. Conditions on
typeof(DerivedClass) need
urgent refactoring the same way. A base class creating derived instances calls for the
factory method pattern http://en.wikipedia.org/wiki/Factory_method_pattern or the
abstract factory pattern http://en.wikipedia.org/wiki/Abstract_factory_pattern
Estimated Debt: 3 minutes per derived class used + 3 minutes per derived class member used.
// <Name>Base class should not use derivatives</Name>
// <Id>ND1201:BaseClassShouldNotUseDerivatives</Id>
warnif count > 0
from baseClass in JustMyCode.Types
where baseClass.IsClass && baseClass.NbChildren > 0 // <-- for optimization!
let derivedClassesUsed = baseClass.DerivedTypes.UsedBy(baseClass)
// Don't warn when a base class is using nested private derived class
.Where(derivedClass =>
!(derivedClass.IsNested &&
derivedClass.Visibility == Visibility.Private &&
derivedClass.ParentType == baseClass))
where derivedClassesUsed.Count() > 0
let derivedClassesMemberUsed = derivedClassesUsed.SelectMany(c => c.Members).UsedBy(baseClass)
orderby derivedClassesMemberUsed.Count() descending
select new {
baseClass,
derivedClassesUsed,
derivedClassesMemberUsed,
Debt = 3*(derivedClassesUsed.Count()+derivedClassesMemberUsed.Count()).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *base {0}* should not rely on its derivative{1?:s}, specifically the *{1}*,
// because it should remain *open* for the creation of new derived
// classes without requiring modifications.
//</Expl>
//<Desc>
// The **open/closed principle** states: *software entities should be open
// for extension, but closed for modification*.
// http://en.wikipedia.org/wiki/Open/closed_principle
//
// Creating or modifying a derived class shouldn't require *modifying* the
// base class. A base class using its derivatives likely will, making
// extension no longer simple: bad design.
//
// No warning when the derived class used is nested private in the base class
// (an encapsulated implementation detail).
//</Desc>
//<HowToFix>
// Understand the need for using derivatives, imagine a new design, refactor.
// Typically, encapsulate base-class access to derived classes behind an
// abstract or virtual method. Conditions on *typeof(DerivedClass)* need
// *urgent refactoring* the same way. A base class creating derived instances
// calls for the *factory method pattern*
// http://en.wikipedia.org/wiki/Factory_method_pattern
// or the *abstract factory pattern*
// http://en.wikipedia.org/wiki/Abstract_factory_pattern
//
// Estimated Debt: 3 minutes per derived class used + 3 minutes per derived
// class member used.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Class shouldn't be too deep in inheritance tree - Object Oriented Design
Warns about classes with 4 or more base classes (third-party base classes not counted: this rule is about your design).
In theory a well-thought
long inheritance chain is fine;
in practice it is more often a sign of confused design, hard to work with and maintain.
How to Fix:Favor Composition over Inheritance: a derived class is bound to the base class implementation, and inheritance breaks encapsulation.
Composition binds to interfaces instead, preserving encapsulation with a clearer, less coupled design — a powerful way to break a
long inheritance chain. See:
http://en.wikipedia.org/wiki/Composition_over_inheritance and
http://stackoverflow.com/questions/49002/prefer-composition-over-inheritance
Estimated Debt: linear in the depth of inheritance.
// <Name>Class shouldn't be too deep in inheritance tree</Name>
// <Id>ND1202:ClassShouldntBeTooDeepInInheritanceTree</Id>
warnif count > 0 from t in JustMyCode.Types
where t.IsClass
let baseClasses = t.BaseClasses.ExceptThirdParty()
where baseClasses.Count() >= 4
orderby baseClasses.Count() descending
select new {
t,
baseClasses,
// The metric value DepthOfInheritance takes account
// of third-party base classes. See its definition here:
// https://www.ndepend.com/docs/code-metrics#DIT
t.DepthOfInheritance,
Debt = (baseClasses.Count() -2)*3.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>The *{0}* derives from *{1}*. A lengthy inheritance chain indicates a confusing design that can be challenging to manage and maintain.</Expl>
//<Desc>
// Warns about classes with 4 or more base classes (third-party base classes
// not counted: this rule is about your design).
//
// *In theory* a well-thought *long inheritance chain* is fine; *in practice*
// it is more often a sign of confused design, hard to work with and maintain.
//</Desc>
//<HowToFix>
// **Favor Composition over Inheritance**: a derived class is bound to the
// base class implementation, and inheritance breaks encapsulation.
// *Composition* binds to interfaces instead, preserving encapsulation with a
// clearer, less coupled design — a powerful way to break a *long inheritance
// chain*. See:
// http://en.wikipedia.org/wiki/Composition_over_inheritance and
// http://stackoverflow.com/questions/49002/prefer-composition-over-inheritance
//
// Estimated Debt: linear in the depth of inheritance.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Class with no descendant should be sealed if possible - Object Oriented Design
A
non-static class not declared
sealed can be subclassed wherever visible. Being a
base class requires significant design effort; subclassing a class not designed for it leads to unanticipated issues. Most classes are
non-sealed by neglect, not intention.
sealed brings performance gains (benchmark:
https://www.meziantou.net/performance-benefits-of-sealed-class.htm) but its real benefit is to
express the intention:
not designed to be a base class, subclassing not allowed.
Public classes are not matched by default (they may be subclassed by library consumers); for an application, uncomment
!t.IsPubliclyVisible.
How to Fix:Assess whether each matched class is really meant to be subclassed; most will end up declared
sealed.
// <Name>Class with no descendant should be sealed if possible</Name>
// <Id>ND1203:ClassWithNoDescendantShouldBeSealedIfPossible</Id>
warnif count > 0
let entryPoints = Application.Methods.Where(m => m.IsEntryPoint)
.ParentTypes().Distinct().ToHashSetEx()
from t in JustMyCode.Types where
t.IsClass &&
t.NbChildren ==0 &&
!t.IsSealed &&
!t.IsStatic &&
!t.IsAbstract &&
!t.IsGeneratedByCompiler && // Top Level Statement Program class is not sealed
!t.IsPubliclyVisible // You might want to comment this condition
// if you are developing an application,
// instead of developing a library
// with public classes that are intended to be
// sub-classed by your clients.
&& !entryPoints.Contains(t) // Don't match un-editable Program types
orderby t.NbLinesOfCode descending
select new {
t,
t.NbLinesOfCode,
Debt = 30.ToSeconds().ToDebt(),
Severity = Severity.Medium
}
//<Expl>No class derives from the *{0}* and it is not publicly visible. It can be marked as *sealed*.</Expl>
//<Desc>
// A *non-static* class not declared *sealed* can be subclassed wherever
// visible. Being a *base class* requires significant design effort;
// subclassing a class not designed for it leads to unanticipated issues.
// Most classes are *non-sealed* by neglect, not intention.
//
// *sealed* brings performance gains (benchmark:
// https://www.meziantou.net/performance-benefits-of-sealed-class.htm)
// but its real benefit is to **express the intention**: *not designed to be
// a base class, subclassing not allowed*.
//
// *Public* classes are not matched by default (they may be subclassed by
// library consumers); for an application, uncomment *!t.IsPubliclyVisible*.
//</Desc>
//<HowToFix>
// Assess whether each matched class is really meant to be subclassed; most
// will end up declared *sealed*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Overrides of Method() should call base.Method() - Object Oriented Design
Overrides should
refine or
complete the base method behavior; not calling the base method
replaces it instead. Violations signal a
design flaw, especially when the design justifies replacing rather than refining.
How to Fix:Investigate whether
inheritance is the right choice, or whether exposing the method polymorphically through an interface would be better. The
template method pattern
http://en.wikipedia.org/wiki/Template_method_pattern might help.
// <Name>Overrides of Method() should call base.Method()</Name>
// <Id>ND1204:OverridesOfMethodShouldCallBaseMethod</Id>
warnif count > 0
from t in Types // Take account of third-party base classes also
// Bother only classes with descendant
where t.IsClass && t.NbChildren > 0
from mBase in t.InstanceMethods
where mBase.IsVirtual &&
!mBase.IsThirdParty &&
!mBase.IsAbstract &&
!mBase.IsExplicitInterfaceImpl &&
!mBase.IsPropertyGetter &&
!mBase.IsPropertySetter &&
!mBase.IsIndexerGetter &&
!mBase.IsIndexerSetter &&
// Don't take account of virtual methods of the Object type.
!mBase.Name.EqualsAny("Equals(Object)","ToString()", "GetHashCode()","Finalize()") &&
mBase.NbLinesOfCode >= 5 && // Need a sufficient behavior to refine
JustMyCode.Contains(mBase)
from mOverride in mBase.OverridesDirectDerived
where !mOverride.IsUsing(mBase) &&
JustMyCode.Contains(mOverride) // Don't warn on generated code
select new {
mOverride,
shouldCall = mBase,
definedInBaseClass = mBase.ParentType,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *override {0}* might need to invoke the base method it overrides to *refine* its base behavior.
//</Expl>
//<Desc>
// Overrides should **refine** or **complete** the base method behavior; not
// calling the base method *replaces* it instead. Violations signal a *design
// flaw*, especially when the design justifies replacing rather than refining.
//</Desc>
//<HowToFix>
// Investigate whether *inheritance* is the right choice, or whether exposing
// the method polymorphically through an interface would be better. The
// **template method** pattern
// http://en.wikipedia.org/wiki/Template_method_pattern might help.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Do not hide base class methods - Object Oriented Design
Method hiding: a base class defines non-virtual
M() and a derived class also defines
M() with the same signature, so
base.M() and
derived.M() behave differently — not
polymorphism, where both invoke the same implementation on a
derived instance. This leads to confusion; this rule warns about all such cases.
How to Fix:Remove or rename the method, or change its parameter signature so it no longer hides the base method. Legitimate hiding (same name, different behavior) is very rare, see:
https://learn.microsoft.com/en-us/archive/blogs/ericlippert/method-hiding-apologia
// <Name>Do not hide base class methods</Name>
// <Id>ND1205:DoNotHideBaseClassMethods</Id>
warnif count > 0
// Define a lookup table indexing methods by their name including parameters signature.
let lookup = Methods.Where(m => !m.IsConstructor && !m.IsStatic && !m.IsGeneratedByCompiler)
.ToLookup(m1 => m1.Name)
from t in Application.Types
where !t.IsStatic && t.IsClass &&
// Discard classes deriving directly from System.Object
t.DepthOfInheritance > 1
where t.BaseClasses.Any()
// For each methods not overriding any methods (new slot),
// let's check if it hides by name some methods defined in base classes.
from m in t.InstanceMethods
where m.IsNewSlot && !m.IsExplicitInterfaceImpl && !m.IsGeneratedByCompiler
// Notice how lookup is used to quickly retrieve methods with same name as m.
// This makes the query 10 times faster than iterating each base methods to check their name.
let baseMethodsHidden = lookup[m.Name].Where(m1 => m1 != m && t.DeriveFrom(m1.ParentType))
where baseMethodsHidden.Count() > 0
select new {
m,
baseMethodsHidden,
baseClasses = baseMethodsHidden.ParentTypes(),
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* hides {2?a:several} method{2?:s} declared in *base {2}* instead of *overriding* {2?it:them}.
// This results in a lack of *polymorphic* behavior at runtime and leads to confusion.
//</Expl>
//<Desc>
// Method hiding: a base class defines non-virtual *M()* and a derived class
// also defines *M()* with the same signature, so *base.M()* and
// *derived.M()* behave differently — not *polymorphism*, where both invoke
// the same implementation on a *derived* instance. This leads to confusion;
// this rule warns about all such cases.
//</Desc>
//<HowToFix>
// Remove or rename the method, or change its parameter signature so it no
// longer hides the base method. Legitimate hiding (same name, different
// behavior) is very rare, see:
// https://learn.microsoft.com/en-us/archive/blogs/ericlippert/method-hiding-apologia
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
A stateless class or structure might be turned into a static type - Object Oriented Design
Matches non-static, non-generic classes/structures with no instance fields, no interface implemented and no base class (other than System.Object): a stateless collection of pure functions, better hosted in a static class so clients needn't create an object.
How to Fix:
Declare all methods static and make the class/structure static. Severity is Low: more an advice than a problem.
// <Name>A stateless class or structure might be turned into a static type</Name>
// <Id>ND1206:AStatelessClassOrStructureMightBeTurnedIntoAStaticType</Id>
warnif count > 0
from t in JustMyCode.Types where
!t.IsStatic &&
!t.IsGeneric &&
!t.IsAbstract &&
t.InstanceFields.Count() == 0 &&
t.SimpleName != "Program" && // Don't warn on Program classes generated by designers
// Don't match:
// --> types that implement some interfaces.
t.NbInterfacesImplemented == 0 &&
// --> or classes that have sub-classes children.
t.NbChildren == 0 &&
// --> or classes that have a base class
((t.IsClass && t.DepthOfDeriveFrom("System.Object".AllowNoMatch()) == 1) ||
t.IsStructure) &&
// Don't warn about classes instantiated by Dependency Injection frameworks
!t.TypesUsingMe.Any(t1 => t1.TypesUsed.ParentNamespaces().Any(n => n.Name.StartsWithAny(
"Microsoft.Extensions.DependencyInjection",
"Autofac", "Microsoft.Practices.Unity", "Ninject",
"StructureMap", "SimpleInjector", "Castle.Windsor",
"LightInject", "Spring", "Lamar", "AutoMapper"
))) &&
!t.BaseClasses.Any(bc => bc.FullName.StartsWith("AutoMapper")) &&
// Don't match marker classes
!(t.IsSealed && t.Members.Count() == 1 &&
t.Members.Single().IsMethod &&
t.Members.Single().AsMethod.IsConstructor &&
t.Members.Single().AsMethod.NbParameters == 0)
let methodsUsingMe = t.TypesUsingMe.ChildMethods().Where(m => m.IsUsing(t))
select new {
t,
methodsUsingMe,
Debt = (1 + methodsUsingMe.Count()).ToMinutes().ToDebt(),
Severity = Severity.Low
}
//<Expl>
// The *{0}* doesn't contain any instance field.
// Since it solely performs processing without managing any state,
// should it be refactored into a *static class*?
//</Expl>
//<Desc>
// Matches non-static, non-generic classes/structures with no instance
// fields, no interface implemented and no base class (other than
// *System.Object*): a *stateless* collection of *pure* functions, better
// hosted in a **static class** so clients needn't create an object.
//</Desc>
//<HowToFix>
// Declare all methods *static* and make the class/structure *static*.
// Severity is **Low**: more an advice than a problem.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Non-static classes should be instantiated or turned to static - Object Oriented Design
A class whose constructors are never called is never instantiated and should be declared static.
Instantiation through reflection is not detected, so plug-in root classes instantiated via IoC frameworks can be false positives. Classes instantiated by ASP.NET infrastructure, view models and Entity Framework ModelSnapshot/DbContext/Migration classes are skipped. So are classes deriving from a third-party abstract class (an endpoint, a hosted service), and classes whose instance members are used by another type: a request DTO or a JSON response, created by a model binder or a serializer. Public classes are matched by default; for a framework whose clients instantiate them, uncomment !t.IsPublic.
How to Fix:
Investigate why the class is never instantiated. If it hosts only static methods, declare it static. If it is instantiated via reflection or by client code, adapt this rule to avoid these matches.
// <Name>Non-static classes should be instantiated or turned to static</Name>
// <Id>ND1207:NonStaticClassesShouldBeInstantiatedOrTurnedToStatic</Id>
warnif count > 0
from t in JustMyCode.Types
where t.IsClass &&
//!t.IsPublic && // if you are developing a framework,
// you might not want to match public classes
!t.IsStatic &&
!t.IsAbstract &&
!t.IsDelegate &&
!t.IsAttributeClass && // Attributes class are never seen as instantiated
// Don't suggest to turn to static, classes that implement interfaces
t.InterfacesImplemented.Count() == 0 &&
// Find the first constructor of t called
// match t if none of its constructors is called.
t.Constructors.FirstOrDefault(ctor => ctor.NbMethodsCallingMe > 0) == null &&
// Don't warn on Program classes generated by designers
t.SimpleName != "Program" &&
// Types instantiated through remoting infrastructure
!t.DeriveFrom("System.MarshalByRefObject".AllowNoMatch()) &&
// JSON, XML and binary serialized types might not be seen as instantiated.
!t.IsUsing("Newtonsoft.Json".MatchNamespace().AllowNoMatch()) &&
!t.IsUsing("System.Text.Json".MatchNamespace().AllowNoMatch()) &&
!t.HasAttribute("System.Xml.Serialization.XmlRootAttribute".AllowNoMatch()) &&
!t.IsUsing("System.Xml.Serialization.XmlElementAttribute".AllowNoMatch()) &&
!t.IsUsing("System.Xml.Serialization.XmlAttributeAttribute".AllowNoMatch()) &&
!t.HasAttribute ("System.SerializableAttribute".AllowNoMatch()) &&
// Serialized type might never be seen as instantiated.
!t.HasAttribute("System.Runtime.Serialization.DataContractAttribute".AllowNoMatch()) &&
!t.IsUsing("System.Runtime.Serialization.DataMemberAttribute".AllowNoMatch()) &&
// ASP.NET Core ViewModel and Repository
!t.SimpleName.EndsWithAny("Model","Repository") &&
!t.IsUsing("System.ComponentModel.DataAnnotations".AllowNoMatch().MatchNamespace()) &&
// ASP.NET Classes that are instantiated by the ASP.NET infrastructure.
!t.BaseClasses.Any(bc => bc.ParentNamespace.Name.StartsWithAny("System.Web", "Microsoft.AspNetCore")) &&
!(t.Constructors.Count() == 1 && t.Constructors.Single().Name.Contains("(IHostingEnvironment)")) &&
// A class deriving from a third-party abstract class is a framework extension point (an endpoint,
// a hosted service, a hub...): the framework instantiates it, and it cannot be declared static.
!t.BaseClasses.Any(bc => bc.IsThirdParty && bc.IsAbstract) &&
// A class whose instance members are used by another type has instances at runtime, created outside
// the analyzed code: by a model binder, a JSON serializer or reflection (a request DTO, a JSON response).
!t.InstanceMethods.Any(m => m.MethodsCallingMe.Any(c => c.ParentType != t)) &&
!t.InstanceFields.Any(f => f.MethodsUsingMe.Any(c => c.ParentType != t)) &&
// Entity Framework ModelSnapshot and DbContext and Migration
!t.DeriveFrom("Microsoft.EntityFrameworkCore.Infrastructure.ModelSnapshot".AllowNoMatch()) &&
!t.DeriveFrom("Microsoft.EntityFrameworkCore.DbContext".AllowNoMatch()) &&
!t.DeriveFrom("Microsoft.EntityFrameworkCore.Migrations.Migration".AllowNoMatch()) &&
// ASP.NET Core Tag Helpers
!t.HasAttribute("Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute".AllowNoMatch()) &&
!t.DeriveFrom("HtmlTags.HtmlTagTagHelper".AllowNoMatch()) &&
// UWP Pages
!t.InstanceMethods.WithName("GetBindingConnector(Int32,Object)").Any() &&
// Validtor classes might not be instantiated
!t.SimpleName.EndsWith("Validator") &&
// Don't warn about classes instantiated by Dependency Injection frameworks
!t.TypesUsingMe.Any(t1 => t1.TypesUsed.ParentNamespaces().Any(n => n.Name.StartsWithAny(
"Microsoft.Extensions.DependencyInjection",
"Autofac", "Microsoft.Practices.Unity", "Ninject",
"StructureMap", "SimpleInjector", "Castle.Windsor",
"LightInject", "Spring", "Lamar", "AutoMapper"
))) &&
!t.BaseClasses.Any(bc => bc.FullName.StartsWith("AutoMapper")) &&
// Don't warn if the type is used by some types using some third-party Json related types.
// The type is potentially instantiated by a Json serializer like the GetFromJsonAsync() extension methods.
!t.TypesUsingMe.SelectMany(t1 => t1.TypesUsed)
.Where(t1 => t1.IsThirdParty && t1.SimpleName.Contains("Json")).Any() &&
// Classes that derive from Unity classes are instantiated implicitly by the Unity engine
!t.BaseClasses.Any(bc => bc.ParentNamespace.Name.StartsWithAny("UnityEngine","UnityEditor")) &&
// Don't match marker classes
!(t.IsSealed && t.Members.Count() == 1 &&
t.Members.Single().IsMethod &&
t.Members.Single().AsMethod.IsConstructor &&
t.Members.Single().AsMethod.NbParameters == 0)
select new {
t,
t.Visibility,
Debt = 2.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
// Notice that classes only instantiated through reflection,
// like plug-in root classes are matched by this rules.
//<Expl>
// The *{0}* is not instantiated in the context of the analyzed code.
// Should it be declared as a *static class* or is it a potential candidate for removal?
//</Expl>
//<Desc>
// A class whose constructors are never called is never instantiated and
// should be declared *static*.
//
// Instantiation through reflection is not detected, so plug-in root classes
// instantiated via *IoC frameworks* can be *false positives*. Classes
// instantiated by ASP.NET infrastructure, view models and Entity Framework
// ModelSnapshot/DbContext/Migration classes are skipped. So are classes
// deriving from a third-party abstract class (an endpoint, a hosted service),
// and classes whose instance members are used by another type: a request DTO
// or a JSON response, created by a model binder or a serializer. *Public*
// classes are matched by default; for a framework whose clients instantiate
// them, uncomment *!t.IsPublic*.
//</Desc>
//<HowToFix>
// Investigate why the class is never instantiated. If it hosts only static
// methods, declare it *static*. If it is instantiated via reflection or by
// client code, adapt this rule to avoid these matches.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Methods should be declared static if possible - Object Oriented Design
An instance method that can be safely declared static should be. The narrower a method's scope, the fewer bugs: static methods can't access instance members, the compiler enforces it, and the signature tells clients the call can't alter object state. It is also a micro performance optimization (no this reference passed).
Beware: declaring a designer-bound event handler static might break the designer generated code if it uses this.Method() syntax.
How to Fix:
Declare matched methods static; consider moving them to a static utility class since they use no instance member of their type or base-types.
// <Name>Methods should be declared static if possible</Name>
// <Id>ND1208:MethodsShouldBeDeclaredStaticIfPossible</Id>
warnif count > 0
from t in JustMyCode.Types.Where(t =>
!t.IsStatic && !t.IsInterface &&
!t.IsEnumeration && !t.IsDelegate &&
!t.IsGeneratedByCompiler &&
// Don't force static call on base class
!t.IsAbstract &&
// Don't advise to declare Global ASP.NET or ApiController methods as static
!t.SimpleName.EndsWith("Controller") &&
!t.DeriveFrom("System.Web.HttpApplication".AllowNoMatch()) &&
!t.DeriveFrom("System.Web.Http.ApiController".AllowNoMatch()) &&
!t.DeriveFrom("Microsoft.AspNetCore.Mvc.Controller".AllowNoMatch()) &&
!t.DeriveFrom("Microsoft.AspNetCore.Mvc.ControllerBase".AllowNoMatch()) &&
!t.HasAttribute("Microsoft.AspNetCore.Mvc.ControllerAttribute".AllowNoMatch()) &&
// Don't set as static methods of JSON serialized type
!t.TypesUsed.Any(t1 => t1.IsAttributeClass &&
(t1.ParentNamespace.Name == "Newtonsoft.Json" || t1.ParentNamespace.Name == "System.Text.Json")
) &&
// Don't set as static methods of XML serialized type
!t.HasAttribute("System.Xml.Serialization.XmlRootAttribute".AllowNoMatch()) &&
!t.IsUsing("System.Xml.Serialization.XmlElementAttribute".AllowNoMatch()) &&
!t.IsUsing("System.Xml.Serialization.XmlAttributeAttribute".AllowNoMatch()) &&
!t.HasAttribute("System.SerializableAttribute".AllowNoMatch()) &&
// Methods of classes that derive from Unity classes are used implicitly by the Unity engine
!t.BaseClasses.Any(bc => bc.ParentNamespace.Name.StartsWithAny("UnityEngine","UnityEditor"))
)
let methodsThatCanBeMadeStatic =
from m in t.InstanceMethods
// An instance method can be turned to static if it is not virtual,
// not using the this reference and also, not using
// any of its class or base classes instance fields or instance methods.
where !m.IsAbstract && !m.IsVirtual &&
!m.AccessThis && !m.IsExplicitInterfaceImpl &&
!m.IsProtected && // Protected method access doesn't match well with static methods
// Don't warn about not yet implemented methods.
!m.CreateA("System.NotImplementedException".AllowNoMatch()) &&
// These Razor methods must not be static
!m.SimpleNameEqualsAny("OnGet","OnPost", "OnGetAsync", "OnPostAsync") &&
// Optimization: Using FirstOrDefault() avoid to check all members,
// as soon as one member is found
// we know the method m cannot be made static.
m.MembersUsed.FirstOrDefault(
mUsed => !mUsed.IsStatic &&
(mUsed.ParentType == t ||
t.DeriveFrom(mUsed.ParentType))
) == null
select m
from m in methodsThatCanBeMadeStatic
let staticFieldsUsed = m.ParentType.StaticFields.UsedBy(m).Where(f => !f.IsGeneratedByCompiler)
let methodsCallingMe = m.MethodsCallingMe
// All callers of the method must be in JustMyCode (or in test code),
// else having a method declared as static would break the call from the code generated
// like when a WPF Connect() method is binding a method to an event.
where methodsCallingMe.All(m1 => JustMyCode.Contains(m1) || m1.IsTestCode)
select new {
m,
staticFieldsUsed,
methodsCallingMe,
Debt = (1 + methodsCallingMe.Count())*30.ToSeconds().ToDebt(),
Severity = Severity.Medium
}
//<Expl>The *{0}* can be declared as static because it doesn't use any instance member through the *this* reference.</Expl>
//<Desc>
// An instance method that can be *safely* declared static should be. The
// narrower a method's scope, the fewer bugs: static methods can't access
// instance members, the compiler enforces it, and the signature tells
// clients the call can't alter object state. It is also a micro performance
// optimization (no *this* reference passed).
//
// Beware: declaring a designer-bound event handler static might break the
// designer generated code if it uses *this.Method()* syntax.
//</Desc>
//<HowToFix>
// Declare matched methods static; consider moving them to a static utility
// class since they use no instance member of their type or base-types.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Constructor should not call a virtual method - Object Oriented Design
Matches constructors of a non-sealed class calling virtual methods.
Constructors run base-first, but virtual calls always execute on the most derived type. A virtual call in a constructor may thus run on a class whose constructor hasn't executed yet — an unsuitable state — making the class error-prone to derive from.
How to Fix:
Re-design object initialization or declare the class sealed if possible. A constructor should initialize its own class's state; extra setup belongs in derived constructors or methods called after construction.
// <Name>Constructor should not call a virtual method</Name>
// <Id>ND1209:ConstructorShouldNotCallAVirtualMethod</Id>
warnif count > 0
from t in JustMyCode.Types where
t.IsClass &&
!t.IsGeneratedByCompiler &&
!t.IsSealed
from ctor in t.Constructors
let virtualMethodsCalled =
from mCalled in ctor.MethodsCalled
where mCalled.IsVirtual && !mCalled.IsFinal &&
// Only take care of just-my-code virtual methods called
JustMyCode.Contains(mCalled) &&
( mCalled.ParentType == t ||
(t.DeriveFrom(mCalled.ParentType) &&
// Don't accept Object methods since they can be called
// from another reference than the 'this' reference.
mCalled.ParentType.FullName != "System.Object")
)
select mCalled
where virtualMethodsCalled.Count() > 0
select new {
ctor ,
virtualMethodsCalled,
// If there is no derived type, it might be
// an opportunity to mark t as sealed.
t.DerivedTypes,
Debt = ((virtualMethodsCalled.Count())*4).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* should not call the *virtual {1}* because doing so
// can potentially corrupt the object initialization process.
//</Expl>
//<Desc>
// Matches constructors of a non-sealed class calling virtual methods.
//
// Constructors run base-first, but virtual calls always execute on the most
// derived type. A virtual call in a constructor may thus run on a class
// whose constructor hasn't executed yet — an unsuitable state — making the
// class *error-prone* to derive from.
//</Desc>
//<HowToFix>
// Re-design object initialization or declare the class *sealed* if possible.
// A constructor should initialize its own class's state; extra setup belongs
// in derived constructors or methods called after construction.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid the Singleton pattern - Object Oriented Design
The
singleton pattern ensures a class has only one runtime instance:
http://en.wikipedia.org/wiki/Singleton_pattern
Avoid singletons: they make code less testable (the same instance is forced across unit tests) and harder to maintain (the static
Instance getter gives uncontrolled access to shared state, degrading the code over time). Only singletons with
mutable instance fields are matched, since the pitfalls come from unregulated access to instance data. More details:
https://blog.ndepend.com/the-proper-usages-of-the-keyword-static-in-c/How to Fix:The problem is the
usual singleton syntax (a static field holding the instance), which hinders testability — not having a single runtime instance. Create the instance at program startup and pass it explicitly to all classes and methods needing it. With multiple singletons, consider consolidating them into a single execution context, easier to propagate.
Estimated Debt: 3 minutes per method relying on the singleton — often hundreds of methods, hence hours of refactoring. Severity is
Critical because a singleton can prevent the whole program from being testable and maintained.
//<Name>Avoid the Singleton pattern</Name>
// <Id>ND1210:AvoidTheSingletonPattern</Id>
warnif count > 0
from t in Application.Types
where !t.IsStatic && !t.IsAbstract && (t.IsClass || t.IsStructure)
// All ctors of a singleton are private
where t.Constructors.Where(ctor => !ctor.IsPrivate).Count() == 0
// Require mutable instance fields to be shared across the several clients of the single object.
let mutableInstanceFields = t.InstanceFields.Where(f => !f.IsImmutable)
where mutableInstanceFields.Any()
// A singleton contains one or several static fields of its parent type,
// or of an interface implemented by its parent type,
// to reference the unique instance
let staticFieldInstances = t.StaticFields.WithFieldTypeIn(t.InterfacesImplemented.Concat(t))
where staticFieldInstances.Count() == 1
let staticFieldInstance = staticFieldInstances.Single()
let methodsUsingField = staticFieldInstance.MethodsUsingMe
let methodsUsingField2 = methodsUsingField.Concat(methodsUsingField.SelectMany(m => m.MethodsCallingMe))
select new {
t,
staticFieldInstance,
methodsUsingField2,
mutableInstanceFields ,
Debt = (3*methodsUsingField2.Count()).ToMinutes().ToDebt(),
AnnualInterest = (10+methodsUsingField2.Count()).ToMinutes().ToAnnualInterest()
}
//<Expl>
// The *{0}* shouldn't rely on this implementation of the *Singleton* pattern.
// While it's fine to have a single instance of a class at runtime,
// using this pattern to enforce a single instance at test-time is problematic,
// as different tests may require their own instances.
//</Expl>
//<Desc>
// The *singleton pattern* ensures a class has only one runtime instance:
// http://en.wikipedia.org/wiki/Singleton_pattern
//
// Avoid singletons: they make code less testable (the same instance is
// forced across unit tests) and harder to maintain (the static *Instance*
// getter gives uncontrolled access to shared state, degrading the code over
// time). Only singletons with *mutable instance fields* are matched, since
// the pitfalls come from unregulated access to instance data. More details:
// https://blog.ndepend.com/the-proper-usages-of-the-keyword-static-in-c/
//</Desc>
//<HowToFix>
// The problem is the *usual singleton syntax* (a static field holding the
// instance), which hinders testability — not having a single runtime
// instance. Create the instance at program startup and pass it explicitly to
// all classes and methods needing it. With multiple singletons, consider
// consolidating them into a single execution context, easier to propagate.
//
// Estimated Debt: 3 minutes per method relying on the singleton — often
// hundreds of methods, hence hours of refactoring. Severity is **Critical**
// because a singleton can prevent the whole program from being testable and
// maintained.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't assign static fields from instance methods - Object Oriented Design
Assigning static fields from instance methods leads to poorly maintainable and non-thread-safe code. More discussion:
https://blog.ndepend.com/the-proper-usages-of-the-keyword-static-in-c/How to Fix:If the
static field is assigned once per program lifetime, declare it
readonly and assign it inline or in the static constructor. Otherwise hold mutable state in
instance fields, the natural OOP artifact for it.
// <Name>Don't assign static fields from instance methods</Name>
// <Id>ND1211:DontAssignStaticFieldsFromInstanceMethods</Id>
warnif count > 0
from f in JustMyCode.Fields where
f.IsStatic &&
!f.IsLiteral &&
!f.IsInitOnly &&
!f.IsGeneratedByCompiler &&
// Contract API define such a insideContractEvaluation static field
f.Name != "insideContractEvaluation"
let assignedBy = f.MethodsAssigningMe.Where(m => !m.IsStatic)
where assignedBy .Count() > 0
select new {
f,
assignedBy,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* is assigned from {1?this:these} instance
// method{1?:s}: *{1}*. This can lead to unexpected *side effects*
// since all instances are sharing the same state.
//</Expl>
//<Desc>
// Assigning static fields from instance methods leads to poorly
// maintainable and non-thread-safe code. More discussion:
// https://blog.ndepend.com/the-proper-usages-of-the-keyword-static-in-c/
//</Desc>
//<HowToFix>
// If the *static* field is assigned once per program lifetime, declare it
// *readonly* and assign it inline or in the static constructor. Otherwise
// hold mutable state in **instance fields**, the natural OOP artifact for it.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid empty interfaces - Object Oriented Design
An empty interface defines no members, hence no contract to implement. It is probably used as a marker to identify a group of types: for run-time identification use a custom attribute instead; for compile-time identification an empty interface is acceptable.
An empty interface implementing at least one other interface is not matched: implementing it requires implementing sub-interfaces members.
How to Fix:
Remove the interface or add members. If used as a marker, replace it with a custom attribute.
Estimated Debt: 10 minutes to discard the interface plus 3 minutes per implementing type.
// <Name>Avoid empty interfaces</Name>
// <Id>ND1212:AvoidEmptyInterfaces</Id>
warnif count > 0 from t in JustMyCode.Types where
t.IsInterface &&
t.NbMethods == 0 &&
!t.InterfacesImplemented.Any()
select new {
t,
t.TypesThatImplementMe,
Debt = (10 + 3*t.TypesThatImplementMe.Count()).ToMinutes().ToDebt(),
Severity = t.TypesThatImplementMe.Any() ? Severity.Medium : Severity.Low
}
//<Expl>The *{0}* is empty and is implemented by *{1}*. If the empty interface is being used to mark a set of classes, replace the interface with a custom attribute.</Expl>
//<Desc>
// An empty interface defines no members, hence no contract to implement. It
// is probably used as a marker to identify a group of types: for run-time
// identification use a custom attribute instead; for compile-time
// identification an empty interface is acceptable.
//
// An empty interface implementing at least one other interface is not
// matched: implementing it requires implementing sub-interfaces members.
//</Desc>
//<HowToFix>
// Remove the interface or add members. If used as a marker, replace it with
// a custom attribute.
//
// Estimated Debt: 10 minutes to discard the interface plus 3 minutes per
// implementing type.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid types initialization cycles - Object Oriented Design
The
class constructor (
static constructor,
cctor in IL) runs the first time a type is used; a static field inline initialization is enough to declare it implicitly.
If the
cctor of
t1 uses
t2 and vice-versa (cycles can span
N types), unexpected hard-to-diagnose initialization bugs can occur. More:
http://codeblog.jonskeet.uk/2012/04/07/type-initializer-circular-dependencies/
This rule enumerates such cycles. Lambdas defined in
cctors (or methods they call) are considered executed at initialization time, which may cause
false positives.
How to Fix:If several states held by several classes must be initialized on first access of any of them, create a dedicated class that initializes and holds all these states.
Estimated Debt: 20 minutes per cycle plus 10 minutes per class constructor involved.
// <Name>Avoid types initialization cycles</Name>
// <Id>ND1213:AvoidTypesInitializationCycles</Id>
warnif count > 0
// Types initialization cycle can only happen between types of an assembly.
from assembly in Application.Assemblies
let cctorSuspects = assembly.ChildMethods.Where(
m => m.IsClassConstructor &&
// Optimization: types involved in a type cycle necessarily don't have type level.
m.ParentType.Level == null)
where cctorSuspects.Count() > 1
let typesSuspects = cctorSuspects.ParentTypes().Where(t0 => !t0.Name.Contains("z__a_")).ToHashSetEx()
//
// dicoTmp associates to each type suspect T, a set of types from typesSuspects
// that contains at least a method or a field used directly or indirectly by the cctor of T.
//
let dicoTmp = cctorSuspects.ToDictionary(
cctor => cctor.ParentType,
cctor => ((IMember)cctor).ToEnumerable().FillIterative(
members => from m in members
from mUsed in (m is IMethod) ? (m as IMethod).MembersUsed : new IMember[0]
where mUsed.ParentAssembly == assembly
select mUsed)
.DefinitionDomain
.Select(m => m.ParentType) // Don't need .Distinct() here, because of ToHashSetEx() below.
.Except(cctor.ParentType)
.Intersect(typesSuspects)
.ToHashSetEx()
)
//
// dico associates to each type suspect T, the set of types initialized (directly or indirectly)
// by the initialization of T. This second step is needed, because if a cctor of a type T1
// calls a member of a type T2, not only the cctor of T1 triggers the initialization of T2,
// but also it triggers the initialization of all types that are initialized by T2 initialization.
//
let dico = typesSuspects.Where(t => dicoTmp[t].Count() > 0).ToDictionary(
typeSuspect => typeSuspect,
typeSuspect => typeSuspect.ToEnumerable().FillIterative(
types => from t in types
from tUsed in dicoTmp[t]
select tUsed)
.DefinitionDomain
.Except(typeSuspect)
.ToHashSetEx()
)
//
// Now that dico is prepared, detect the cctor cycles
//
from t in dico.Keys
// Don't include the types generated by PostSharp
.Where(t0 => !t0.Name.Contains("z__a_"))
// Thanks to the work done to build dico, it is now pretty easy
// to spot types involved in an initialization cycle with t!
let usersAndUseds = from tTmp in dico[t]
where dico.ContainsKey(tTmp) && dico[tTmp].Contains(t)
select tTmp
where usersAndUseds.Count() > 0
// Here we've found type(s) both using and used by the suspect type.
// A cycle involving the type t is found!
// v2017.3.2: don't call Append() as an extension method else ambiguous syntax error
// with the new extension method in .NET Fx v4.7.1 / .NET Standard 2.0: System.Linq.Enumerable.Append()
let typeInitCycle = ExtensionMethodsEnumerable.Append(usersAndUseds,t)
// Compute methodsCalled and fieldsUsed, useful to explore
// how a cctor involved in a type initialization cycle, triggers other type initialization.
let methodsCalledDepth = assembly.ChildMethods.DepthOfIsUsedBy(t.ClassConstructor)
let fieldsUsedDepth = assembly.ChildFields.DepthOfIsUsedBy(t.ClassConstructor)
let methodsCalled = methodsCalledDepth.DefinitionDomain.OrderBy(m => methodsCalledDepth[m]).ToArray()
let fieldsUsed = fieldsUsedDepth.DefinitionDomain.OrderBy(f => fieldsUsedDepth[f]).ToArray()
// Use the tick box to: Group cctors methods By parent types
select new {
t.ClassConstructor,
cctorsCycle= typeInitCycle.Select(tTmp => tTmp.ClassConstructor).Except(t.ClassConstructor),
// methodsCalled and fieldsUsed are members used directly and indirectly by the cctor.
// Export these members to the dependency graph (right click the cell Export/Append … to the Graph)
// and see how the cctor trigger the initialization of other types
methodsCalled,
fieldsUsed,
Debt = (20+10*typeInitCycle.Count()).ToMinutes().ToDebt(),
Severity = Severity.Critical
}
//<Expl>
// The *{0}* and the other{1?:s} *{1}* are involved in a cyclic call,
// resulting in *unpredictable initialization order* and potential unexpected states.
//</Expl>
//<Desc>
// The *class constructor* (*static constructor*, *cctor* in IL) runs the
// first time a type is used; a static field inline initialization is enough
// to declare it implicitly.
//
// If the *cctor* of *t1* uses *t2* and vice-versa (cycles can span *N*
// types), unexpected hard-to-diagnose initialization bugs can occur. More:
// http://codeblog.jonskeet.uk/2012/04/07/type-initializer-circular-dependencies/
//
// This rule enumerates such cycles. Lambdas defined in *cctors* (or methods
// they call) are considered executed at initialization time, which may cause
// *false positives*.
//</Desc>
//<HowToFix>
// If several states held by several classes must be initialized on first
// access of any of them, create a dedicated class that initializes and
// holds all these states.
//
// Estimated Debt: 20 minutes per cycle plus 10 minutes per class constructor
// involved.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid custom delegates - Design
Prefer generic delegates over custom delegates:
- Action<…> for void return type.
- Func<…> for a return type (last generic argument).
- Predicate<T> for a T instance returning a boolean.
- Expression<…> for function definitions compiled/invoked at runtime, also serializable to remote processes.
They clarify code and remove delegate type maintenance. Do not convert delegates consumed by DllImport extern methods (marshalling issues).
How to Fix:
Replace custom delegates with the generic delegates shown in the replaceWith column.
Estimated Debt: 5 minutes per custom delegate plus 3 minutes per method using it.
// <Name>Avoid custom delegates</Name>
// <Id>ND1300:AvoidCustomDelegates</Id>
warnif count > 0
from t in JustMyCode.Types where t.IsDelegate && !t.IsGeneric
let invokeMethod = (from m in t.Methods where m.SimpleNameEquals("Invoke") select m).Single()
let signature1 = invokeMethod.Name.Substring(
invokeMethod.SimpleName.Length,
invokeMethod.Name.Length - invokeMethod.SimpleName.Length)
// 'ref' 'out' and pointers parameters cannot be supported
where !signature1.Contains("&") && !signature1.Contains("*")
let signature2 = signature1.Replace("(","<").Replace(")",">")
let signature3 = signature2 == "<>" ? "" : signature2
let resultTypeName = invokeMethod.ReturnType == null ? "????" :
invokeMethod.ReturnType.FullName == "System.Void" ? "" :
invokeMethod.ReturnType.Name
let replaceWith =
resultTypeName == "Boolean" && invokeMethod.NbParameters == 1 ?
"Predicate" + signature3 : resultTypeName == "" ?
"Action" + signature3 : invokeMethod.NbParameters ==0 ?
"Func<" + resultTypeName + ">" :
"Func" + signature3.Replace(">", "," + resultTypeName + ">")
let methodsUser = t.TypesUsingMe.ChildMethods().Where(m => m.IsUsing(t))
select new {
t,
replaceWith,
methodsUser,
Debt = (5 + 3*methodsUser.Count()).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* should be replaced with *{1}*.
// Generic delegates clarify code and
// reduce delegate type maintenance.
//</Expl>
//<Desc>
// Prefer generic delegates over custom delegates:
//
// - *Action<…>* for *void* return type.
//
// - *Func<…>* for a return type (last generic argument).
//
// - *Predicate<T>* for a *T* instance returning a *boolean*.
//
// - *Expression<…>* for function definitions compiled/invoked at runtime,
// also serializable to remote processes.
//
// They clarify code and remove delegate type maintenance. Do not convert
// delegates consumed by *DllImport* extern methods (marshalling issues).
//</Desc>
//<HowToFix>
// Replace custom delegates with the generic delegates shown in the
// **replaceWith** column.
//
// Estimated Debt: 5 minutes per custom delegate plus 3 minutes per method
// using it.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Types with disposable instance fields must be disposable - Design
Warns when a class has an instance field whose type implements System.IDisposable or System.IAsyncDisposable but does not implement IDisposable itself: such a class indirectly owns an unmanaged resource. If it owns no unmanaged resource directly, it should not have a finalizer.
False positives can occur when the referenced disposable objects outlive the referencing object.
How to Fix:
Implement IDisposable and call the fields' Dispose() from IDisposable.Dispose(). Then ensure every method calling a constructor of the type disposes the objects created.
Estimated Debt: 5 minutes per type plus 2 minutes per disposable field plus 4 minutes per method calling a constructor.
// <Name>Types with disposable instance fields must be disposable</Name>
// <Id>ND1301:TypesWithDisposableInstanceFieldsMustBeDisposable</Id>
warnif count > 0
// Several IDisposable types can be found if several .NET profiles are referenced.
let iDisposables = ThirdParty.Types.WithFullNameIn("System.IDisposable", "System.IAsyncDisposable")
where iDisposables.Any() // in case the code base doesn't use at all System.IDisposable or IAsyncDisposable
from t in JustMyCode.Types.Except(
JustMyCode.Types.ThatImplementAny(iDisposables)
// Don't match ASP.NET types like Page, MasterPage or Control
.Union(JustMyCode.Types.Where(t => t.BaseClasses.Any(bc => bc.ParentNamespace.Name.StartsWith("System.Web.UI")))))
where !t.IsGeneratedByCompiler
let instanceFieldsDisposable =
t.InstanceFields.Where(f => f.FieldType != null &&
f.FieldType.InterfacesImplemented.Intersect(iDisposables).Any() &&
// Impose that disposable types be instantiated by 't' to avoid false positives
f.FieldType.Constructors.Any() &&
t.Constructors.ThatCreateA(f.FieldType).Any())
where instanceFieldsDisposable.Any()
// Don't warn for types that implement the dispose pattern with a Dispose(bool) method.
where t.Methods.FirstOrDefault(m => m.Name == "Dispose(Boolean)") == null
let methodsThatCallsCtor = t.Constructors.SelectMany(ctor => ctor.MethodsCallingMe).Distinct()
select new {
t,
instanceFieldsDisposable,
methodsThatCallsCtor,
Debt = (5 + 2*instanceFieldsDisposable.Count()
+ 4*methodsThatCallsCtor.Count()).ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* should implement *IDisposable* to dispose the disposable
// field{1?:s} it instantiates at construction time:
// the *{1}*
//</Expl>
//<Desc>
// Warns when a class has an instance field whose type implements
// *System.IDisposable* or *System.IAsyncDisposable* but does not implement
// *IDisposable* itself: such a class indirectly owns an unmanaged resource.
// If it owns no unmanaged resource directly, it should not have a finalizer.
//
// False positives can occur when the referenced disposable objects outlive
// the referencing object.
//</Desc>
//<HowToFix>
// Implement *IDisposable* and call the fields' *Dispose()* from
// *IDisposable.Dispose()*. Then ensure every method calling a constructor of
// the type disposes the objects created.
//
// Estimated Debt: 5 minutes per type plus 2 minutes per disposable field
// plus 4 minutes per method calling a constructor.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Disposable types with unmanaged resources should declare finalizer - Design
Reports disposable types (System.IDisposable / IAsyncDisposable) with no finalizer (Object.Finalize()) that contain fields suggesting unmanaged resources:
- System.IntPtr
- System.UIntPtr
- System.Runtime.InteropServices.HandleRef
Disabled by default: it typically reports false positives for classes merely referencing managed resources without the responsibility to dispose them. Uncomment warnif count > 0 to enable.
How to Fix:
Implement a finalizer that calls your Dispose() method.
// <Name>Disposable types with unmanaged resources should declare finalizer</Name>
// <Id>ND1302:DisposableTypesWithUnmanagedResourcesShouldDeclareFinalizer</Id>
// This default rule is disabled by default,
// see in the rule description (below) why.
// warnif count > 0
// Several IDisposable type can be found if several .NET Fx are referenced.
let iDisposables = ThirdParty.Types.WithFullNameIn("System.IDisposable", "System.IAsyncDisposable")
where iDisposables.Any() // in case the code base doesn't use at all System.IDisposable or IAsyncDisposable
let disposableTypes = Application.Types.ThatImplementAny(iDisposables)
let unmanagedResourcesFields = disposableTypes.ChildFields().Where(f =>
!f.IsStatic &&
f.FieldType != null &&
f.FieldType.FullNameEqualsAny(
"System.IntPtr",
"System.UIntPtr",
"System.Runtime.InteropServices.HandleRef")).ToHashSetEx()
let disposableTypesWithUnmanagedResource = unmanagedResourcesFields.ParentTypes()
from t in disposableTypesWithUnmanagedResource
where !t.HasFinalizer
let unmanagedResourcesTypeFields = unmanagedResourcesFields.Intersect(t.InstanceFields)
select new {
t,
unmanagedResourcesTypeFields,
//Debt = 10.ToMinutes().ToDebt(),
//Severity = Severity.Critical
}
//<Expl>
// The *{0}* hold unmanaged resources through *{1}* but doesn't have a finalizer.
//</Expl>
//<Desc>
// Reports disposable types (*System.IDisposable* / *IAsyncDisposable*) with
// no finalizer (*Object.Finalize()*) that contain fields suggesting
// unmanaged resources:
//
// - *System.IntPtr*
//
// - *System.UIntPtr*
//
// - *System.Runtime.InteropServices.HandleRef*
//
// Disabled by default: it typically reports *false positives* for classes
// merely referencing managed resources without the responsibility to dispose
// them. Uncomment *warnif count > 0* to enable.
//</Desc>
//<HowToFix>
// Implement a finalizer that calls your *Dispose()* method.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Methods that create disposable object(s) and that don't call Dispose() - Design
Enumerates methods creating disposable object(s) without calling any Dispose() method. Not a rule: this is acceptable when the objects are disposed elsewhere. Easily refactored to target specific disposable types or caller methods, and turned into a rule.
// <Name>Methods that create disposable object(s) and that don't call Dispose()</Name>
// <Id>ND1303:MethodsThatCreateDisposableObjectAndThatDontCallDispose</Id>
// Just uncomment this to transform this code query into a code rule.
// warnif count > 0
// Several IDisposable types can be found if several .NET Fx are referenced.
let iDisposables = ThirdParty.Types.WithFullName("System.IDisposable")
where iDisposables.Any() // in case the code base doesn't use at all System.IDisposable
// Build sequences of disposableTypes and disposeMethods
let disposableTypes = Types.ThatImplementAny(iDisposables).Concat(iDisposables)
let disposeMethods = disposableTypes.ChildMethods().WithName("Dispose()").ToHashSetEx()
// -> You can refine this code query by assigning to disposableTypesToLookAfter something like:
// disposableTypes.WithFullNameIn("Namespace.TypeName1", "Namespace.TypeName2", ...)
let disposableTypesToLookAfter = disposableTypes
// -> You can refine this code query by assigning to methodsToLookAfter something like:
// Application.Assemblies.WithNameLike("Asm").ChildMethods()
let methodsToLookAfter = Application.Methods
// Enumerate methods that create any disposable type, without calling Dispose()
from m in methodsToLookAfter.ThatCreateAny(disposableTypesToLookAfter )
where !m.MethodsCalled.Intersect(disposeMethods).Any()
select new {
m,
disposableObjectsCreated = disposableTypes.Where(t => m.CreateA(t)),
m.MethodsCalled,
//Debt = 10.ToMinutes().ToDebt(),
//Severity = Severity.Low
}
//<Expl>
// The *{0}* instantiates disposable types without calling any *Dispose()* method.
// Disposable type{1?:s} instantiated: *{1}*
//</Expl>
//<Desc>
// Enumerates methods creating disposable object(s) without calling any
// Dispose() method. Not a rule: this is acceptable when the objects are
// disposed elsewhere. Easily refactored to target specific disposable types
// or caller methods, and turned into a rule.
//</Desc>
./rules/assets/img/StatusBigOk32x32.png
Disposable Types Must Unsubscribe Events - Design
Detects disposable types subscribing to events in constructors without unsubscribing in Dispose() (or a method it calls). Event publisher references can keep instances alive: memory leaks, unexpected callbacks.
How to Fix:
1) Match each constructor subscription with an unsubscription in Dispose() (or a method always called by it). 2) Centralize subscriptions/unsubscriptions in dedicated methods for explicit symmetry. 3) With inheritance, follow the full dispose pattern so derived types can release additional subscriptions. 4) Prefer weak-event patterns or explicit lifetime ownership when the publisher outlives the subscriber.
// <Name>Disposable Types Must Unsubscribe Events</Name>
// <Id>ND1315:DisposableTypesMustUnsubscribeEvents</Id>
warnif count > 0
// Prepare a convenient lookup table first for performance reasons
let lookup = Events
.SelectMany(e => e.MethodsSubscribingToMe
.Where(m => m.IsConstructor)
.Select(m => new { m.ParentType, e}))
.ToLookup(pair => pair.ParentType, pair => pair.e)
from pair in lookup
where pair.Key.Implement("System.IDisposable".AllowNoMatch())
let t = pair.Key
// Events this Disposable type subscribes to
let eventsSubscribedInCtor = pair.ToArray()
// Check if Dispose() deregisters these events
let disposeMethod = t.Methods.FirstOrDefault(m =>
m.SimpleName == "Dispose" && m.NbParameters == 0)
where disposeMethod != null
let notDeregisteredInDispose = eventsSubscribedInCtor.Where(e =>
// No Dispose() or methods called by Dispose() unregister event
!e.MethodsUnsubscribingToMe.Contains(disposeMethod) &&
!e.MethodsUnsubscribingToMe.Any(x => disposeMethod.MethodsCalled.Contains(x))
).ToArray()
where notDeregisteredInDispose.Any()
select new {
t,
eventsSubscribedInCtor,
notDeregisteredInDispose,
disposeMethod,
Debt = (2*notDeregisteredInDispose.Count()).ToMinutes().ToDebt(),
Severity = Severity.High
}
// <Explanation>
// The {0}'s Dispose method() should unregister the {2}.
// </Explanation>
// <Desc>
// Detects disposable types subscribing to events in constructors without
// unsubscribing in Dispose() (or a method it calls). Event publisher
// references can keep instances alive: memory leaks, unexpected callbacks.
// </Desc>
// <HowToFix>
// 1) Match each constructor subscription with an unsubscription in Dispose()
// (or a method always called by it).
// 2) Centralize subscriptions/unsubscriptions in dedicated methods for
// explicit symmetry.
// 3) With inheritance, follow the full dispose pattern so derived types can
// release additional subscriptions.
// 4) Prefer weak-event patterns or explicit lifetime ownership when the
// publisher outlives the subscriber.
// </HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Classes that are candidate to be turned into structures - Design
Structures suit
lightweight values (like
Int32 or
Boolean): as
values, they are not managed by the garbage collector, cheaper for plenty of
short lived instances. This rule matches classes looking like
lightweight values:
- It has instance fields, all typed with value-types.
- It is immutable.
- It implements no interfaces.
- It has no parameterless constructor, is not generic.
- It has no derived classes and derives directly from
System.Object.
- Not an ASP.NETCore ViewModel (name ending with
Model) or Repository.
Whether instances are numerous
short-lived objects is not checked: these criteria are indications, only you can decide if the change is
performance wise.
How to Fix:Use the keyword
struct instead of
class.
CAUTION: first understand the
deep implications explained here:
https://blog.ndepend.com/class-vs-struct-in-c-making-informed-choices/
Estimated Debt: 5 minutes per class plus 1 minute per method using it.
// <Name>Classes that are candidate to be turned into structures</Name>
// <Id>ND1304:ClassesThatAreCandidateToBeTurnedIntoStructures</Id>
warnif count > 0
from t in JustMyCode.Types where
t.IsClass &&
t.IsSealed &&
!t.IsGeneratedByCompiler &&
!t.IsStatic &&
!t.IsGeneric &&
t.NbChildren == 0 && // Must not have children
t.Constructors.All(c => c.NbParameters > 0) && // Must not have parameterless ctor, struct cannot have custom parameterless ctor
t.IsImmutable && // Structures should be immutable type.
// Must have fields and all fields must be of value-type
t.InstanceFields.Count() > 0 &&
t.InstanceFields.All(f => f.FieldType != null) &&
t.InstanceFields.All(f => f.FieldType.IsStructure || f.FieldType.IsEnumeration) &&
// Must not implement interfaces to avoid boxing mismatch
// when structures implements interfaces.
t.InterfacesImplemented.Count() == 0 &&
// Must derive directly from System.Object
t.DepthOfDeriveFrom("System.Object".AllowNoMatch()) == 1 &&
// Must not be a serializable class because a structure should be immutable
// and serialized types are mutable.
!t.TypesUsed.Any(t1 => t1.IsAttributeClass &&
(t1.ParentNamespace.Name == "Newtonsoft.Json" || t1.ParentNamespace.Name == "System.Text.Json")
) &&
!t.HasAttribute("System.Xml.Serialization.XmlRootAttribute".AllowNoMatch()) &&
!t.IsUsing("System.Xml.Serialization.XmlElementAttribute".AllowNoMatch()) &&
!t.IsUsing("System.Xml.Serialization.XmlAttributeAttribute".AllowNoMatch()) &&
!t.HasAttribute("System.Runtime.Serialization.DataContractAttribute".AllowNoMatch()) &&
!t.IsUsing("System.Runtime.Serialization.DataMemberAttribute".AllowNoMatch()) &&
!t.HasAttribute("System.SerializableAttribute".AllowNoMatch()) &&
// ASP.NET Core ViewModel and Repository
!t.SimpleName.EndsWithAny("Model","Repository") &&
!t.IsUsing("System.ComponentModel.DataAnnotations".AllowNoMatch().MatchNamespace()) &&
// Must not be an entry point class like 'Program'
!t.Methods.Any(m => m.IsEntryPoint)
// && !t.IsPubliclyVisible <-- You might want to add this condition if
// you are developing a framework with classes
// that are intended to be sub-classed by
// your clients.
let methodsUser = t.TypesUsingMe.ChildMethods().Where(m => m.IsUsing(t))
select new {
t,
t.SizeOfInst,
t.InstanceFields,
methodsUser,
Debt = (5 + 1*methodsUser.Count()).ToMinutes().ToDebt(),
Severity = Severity.Info
}
//<Expl>
// The *{0}* could be declared as a structure
// because it is immutable, it has simple fields, it is sealed
// and it doesn't have a base class.
//</Expl>
//<Desc>
// Structures suit **lightweight values** (like *Int32* or *Boolean*): as
// *values*, they are not managed by the garbage collector, cheaper for
// plenty of *short lived* instances. This rule matches classes looking like
// *lightweight values*:
//
// - It has instance fields, all typed with value-types.
//
// - It is immutable.
//
// - It implements no interfaces.
//
// - It has no parameterless constructor, is not generic.
//
// - It has no derived classes and derives directly from *System.Object*.
//
// - Not an ASP.NETCore ViewModel (name ending with *Model*) or Repository.
//
// Whether instances are numerous *short-lived* objects is not checked: these
// criteria are indications, only you can decide if the change is
// *performance wise*.
//</Desc>
//<HowToFix>
// Use the keyword *struct* instead of *class*.
//
// **CAUTION:** first understand the **deep implications** explained here:
// https://blog.ndepend.com/class-vs-struct-in-c-making-informed-choices/
//
// Estimated Debt: 5 minutes per class plus 1 minute per method using it.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid namespaces with few types - Design
Warns about non-global namespaces containing fewer than five types. Numerous sparsely populated namespaces require many using clauses in client code. Namespaces should group types used together (separate ones for mutually exclusive uses) and organize features for discoverability.
The rule source contains a completable list of common infrastructure namespace names; namespaces ending with these names are not matched.
How to Fix:
Combine namespaces containing just a few types into a single namespace.
// <Name>Avoid namespaces with few types</Name>
// <Id>ND1305:AvoidNamespacesWithFewTypes</Id>
warnif count > 0
// Common infrastructure namespace names not matched by the rule.
// Complete this list to your need.
let infraNamespaceNames = new HashSet<string>() {
"Services","Exceptions","Logging", "Domain",
"Identity", "Migrations", "Controllers",
"Specifications", "Interfaces", "Components",
"Bus", "Models", "EventHandlers", "Mappings",
"ViewModels", "ViewComponents", "Notifications",
"Configurations", "Extensions", "Events",
"Context", "Data", "EventSourcing", "Repository",
"IoC", "Manage", "Commands", "CommandHandlers",
"Validations", "EventStoreSQL", "Authorization",
"Formatters", "Xml", "Json", "Enums",
"Abstractions", "BaseClasses", "Settings",
"Helpers", "Validators", "Entities", "Pages"
}
// Only warn for namespaces with few types declared in assemblies with more than 15 types.
// This way the rule avoids matching namespace with few types in assemblies with few types.
from a in JustMyCode.Assemblies
where a.NbTypes >= 15
from n in a.ChildNamespaces
where n.Name.Length > 0 // Don't match anonymous namespaces
&& !infraNamespaceNames.Contains(n.SimpleName) // Don't match common infrastructure namespaces
&& !n.ParentNamespaces.Any(pn => infraNamespaceNames.Contains(pn.SimpleName)) // Don't match children of common infrastructure namespaces
&& n.Name != n.ParentAssembly.Name // Don't warn on namespace named as assembly to avoid warning on new VS projects
let types = n.ChildTypes.Where(t => !t.IsGeneratedByCompiler).ToArray()
where
types.Length > 0 && // Don't match namespaces that contain only types GeneratedByCompiler
types.Length < 5 &&
// Only match namespaces that have all types in JustMyCode
types.All(t => JustMyCode.Contains(t))
orderby types.Length ascending
select new {
n,
types,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.Low
}
//<Expl>The *{0}* contains only the *{1}*. To prevent an excessive number of *using* clauses in client code, refrain from segregating classes into numerous small namespaces.</Expl>
//<Desc>
// Warns about non-global namespaces containing fewer than five types.
// Numerous sparsely populated namespaces require many *using* clauses in
// client code. Namespaces should group types used together (separate ones
// for mutually exclusive uses) and organize features for discoverability.
//
// The rule source contains a completable list of common infrastructure
// namespace names; namespaces ending with these names are not matched.
//</Desc>
//<HowToFix>
// Combine namespaces containing just a few types into a single namespace.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Nested types should not be visible - Design
Warns about nested types not declared private. Nested types serve to encapsulate private implementation details of the containing type and should not be externally visible. Do not use them for logical grouping or name collision avoidance — use namespaces. Protected nested types can be used in subclasses.
How to Fix:
Change the type's accessibility if it needn't be externally visible; otherwise make it non-nested. For grouping, use a namespace instead.
Estimated Debt: 2 minutes per nested type plus 4 minutes per outer type using it.
// <Name>Nested types should not be visible</Name>
// <Id>ND1306:NestedTypesShouldNotBeVisible</Id>
warnif count > 0 from t in JustMyCode.Types where
t.IsNested &&
!t.IsGeneratedByCompiler &&
!t.IsPrivate &&
// It is accepted that non-private nested types are declared inside Razor page classes.
!t.OutterTypes.Any(ot => ot.DeriveFrom("Microsoft.AspNetCore.Mvc.RazorPages.PageModel".AllowNoMatch()) ||
ot.DeriveFrom("Microsoft.AspNetCore.Components.ComponentBase".AllowNoMatch()))
let typesUser = t.TypesUsingMe.Where(t1 => t1 != t.ParentType && t1.ParentType != t.ParentType)
select new {
t,
t.Visibility,
typesUser,
Debt = (2 + 4*typesUser.Count()).ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *nested {0}* is declared as *{1}* and is accessed outside
// its parent type by the *{2}*. Typically, types are nested to
// encapsulate private implementation details.
// Consider making it *private* or non-nested.
//</Expl>
//<Desc>
// Warns about nested types not declared private. Nested types serve to
// encapsulate private implementation details of the containing type and
// should not be externally visible. Do not use them for logical grouping or
// name collision avoidance — use namespaces. Protected nested types can be
// used in subclasses.
//</Desc>
//<HowToFix>
// Change the type's accessibility if it needn't be externally visible;
// otherwise make it *non-nested*. For grouping, use a namespace instead.
//
// Estimated Debt: 2 minutes per nested type plus 4 minutes per outer type
// using it.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Declare types in namespaces - Design
Types outside any named namespace live in the global namespace (the anonymous namespace), which cannot be referenced in code: naming collisions, ambiguity, and no encapsulation of related code and data. This rule warns about anonymous namespaces.
How to Fix:
Declare all types of anonymous namespaces in named namespaces.
// <Name>Declare types in namespaces</Name>
// <Id>ND1307:DeclareTypesInNamespaces</Id>
warnif count > 0 from n in Application.Namespaces where
// If an anonymous namespace can be found,
// it means that it contains types outside of namespaces.
n.Name == ""
// Filter anonymous namespaces that contains
// only notmycode types (like generated types)
// and also entry points.
let childTypes = n.ChildTypes.Where(t =>
!t.IsNested &&
!t.Methods.Any(m => m.IsEntryPoint) &&
JustMyCode.Contains(t) &&
!t.IsGeneratedByCompiler &&
// Don't match marker classes
!(t.IsSealed && t.Members.Count() == 1 &&
t.Members.Single().IsMethod &&
t.Members.Single().AsMethod.IsConstructor &&
t.Members.Single().AsMethod.NbParameters == 0)
)
where childTypes.Count() > 0
select new {
n,
childTypes,
n.NbLinesOfCode,
Debt = 2*childTypes.Count().ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{1}* {1?is:are} declared in the global anonymous namespace
// that cannot be imported via a *using* clause.
// This results in naming collisions and ambiguity since
// {1?this type is:these types are} accessible from anywhere.
//</Expl>
//<Desc>
// Types outside any named namespace live in the *global namespace* (the
// *anonymous namespace*), which cannot be referenced in code: naming
// collisions, ambiguity, and no encapsulation of related code and data.
// This rule warns about *anonymous namespaces*.
//</Desc>
//<HowToFix>
// Declare all types of anonymous namespaces in named namespaces.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Empty static constructor can be discarded - Design
Warns about static constructors (cctor in IL, run by the CLR before the type's first use) containing no lines of code: useless, safely removable.
How to Fix:
Remove matched empty static constructors.
// <Name>Empty static constructor can be discarded</Name>
// <Id>ND1308:EmptyStaticConstructorCanBeDiscarded</Id>
warnif count > 0 from m in JustMyCode.Methods where
m.IsClassConstructor &&
m.NbLinesOfCode == 0
select new {
m,
Debt = 2.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *empty {0}* can be safely removed.
//</Expl>
//<Desc>
// Warns about *static constructors* (*cctor* in IL, run by the CLR before
// the type's first use) containing no lines of code: useless, safely
// removable.
//</Desc>
//<HowToFix>
// Remove matched empty *static constructors*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Instances size shouldn't be too big - Design
Types where
SizeOfInst > 128 might degrade performance if many instances are created, and can be hard to maintain. A large
SizeOfInst may come from a base class rather than many own fields.
WPF/WindowsForm forms and controls, Entity Framework classes, and base classes implying large instances (
System.ComponentModel,
System.Xml,
DevExpress…) are not matched; add other vendors as needed.
SizeOfInst metric:
https://www.ndepend.com/docs/code-metrics#SizeOfInstHow to Fix:Group the data held
directly into smaller composable types.
Severity: linear from
Medium (128 bytes) to twice
High (2048 bytes) per instance. Annual interest is 10x higher for structures: each time a large structure
value is passed as parameter it gets copied, so such a structure should be declared as class.
// <Name>Instances size shouldn't be too big</Name>
// <Id>ND1309:InstancesSizeShouldntBeTooBig</Id>
warnif count > 0 from t in JustMyCode.Types where
t.SizeOfInst > 256 &&
// You might want to restrict this rule only on structure, since the cost of copying instance data at each method call might be prohibitive
(t.IsStructure || t.IsClass) &&
// Discard types that represent WPF, WindowsForm and ASP.NET forms and controls and EntityFramwork classes.
t.BaseClasses.All(bc => !bc.ParentNamespace.Name.StartsWithAny(
"Microsoft.AspNetCore", "System.Windows", "System.Web.UI", "System.Web", "Microsoft.EntityFramework",
// Discard types related to these namespaces that typically require large instances size
"System.ComponentModel", "System.Xml",
// Just add more component vendors here if needed
"DevExpress", "Lucene"))
orderby t.SizeOfInst descending
select new {
t,
t.SizeOfInst,
t.InstanceFields,
t.BaseClasses,
instanceKind = t.IsStructure ? "A value" : "An instance",
Debt = t.SizeOfInst.Linear(128,10, 2048, 120).ToMinutes().ToDebt(),
// The annual interest varies linearly from interests for severity Medium for 64 bytes per instance
// to twice interests for severity High for 2048 bytes per instance
AnnualInterest = (t.SizeOfInst.Linear(128, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes,
2048, 2*(Severity.High.AnnualInterestThreshold().Value.TotalMinutes))
)*(t.IsStructure ? 10 : 1) // Multiply interest by 10 for structures
.ToMinutes().ToAnnualInterest()
}
//<Expl>
// {4} of the *{0}* has a memory footprint of at least *{1} bytes*.
// If this type is often instantiated at runtime this can be problematic.
//</Expl>
//<Desc>
// Types where *SizeOfInst > 128* might degrade performance if many instances
// are created, and can be hard to maintain. A large *SizeOfInst* may come
// from a base class rather than many own fields.
//
// WPF/WindowsForm forms and controls, Entity Framework classes, and base
// classes implying large instances (*System.ComponentModel*, *System.Xml*,
// *DevExpress*…) are not matched; add other vendors as needed.
// *SizeOfInst* metric: https://www.ndepend.com/docs/code-metrics#SizeOfInst
//</Desc>
//<HowToFix>
// Group the data held *directly* into smaller composable types.
//
// Severity: linear from **Medium** (128 bytes) to twice **High** (2048
// bytes) per instance. Annual interest is 10x higher for structures: each
// time a large structure *value* is passed as parameter it gets copied, so
// such a structure should be declared as class.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Attribute classes should be sealed - Design
.NET methods fetching custom attributes (like System.Attribute.GetCustomAttribute()) explore the attribute inheritance hierarchy by default. Sealing the attribute removes that search, potentially enhancing performance.
How to Fix:
Seal the attribute type or make it abstract.
// <Name>Attribute classes should be sealed</Name>
// <Id>ND1310:AttributeClassesShouldBeSealed</Id>
warnif count > 0 from t in Application.Types where
t.IsAttributeClass &&
!t.IsSealed &&
!t.IsAbstract &&
t.IsPublic
select new {
t,
t.NbLinesOfCode,
Debt = 2.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// To potentially enhance the performance of .NET
// methods designed for custom attributes, it is
// advisable to seal the attribute *{0}*.
//</Expl>
//<Desc>
// .NET methods fetching custom attributes (like
// *System.Attribute.GetCustomAttribute()*) explore the attribute inheritance
// hierarchy by default. Sealing the attribute removes that search,
// potentially enhancing performance.
//</Desc>
//<HowToFix>
// Seal the attribute type or make it abstract.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't use obsolete types, methods or fields - Design
Warns about methods using a type, method or field tagged with System.ObsoleteAttribute: such API elements will be removed sooner or later and shouldn't be used.
How to Fix:
Follow the workaround message typically provided with System.ObsoleteAttribute to avoid the obsolete element.
Estimated Debt: 5 minutes per type, method or field used. Severity is High: it is important to no longer rely on obsolete code.
// <Name>Don't use obsolete types, methods or fields</Name>
// <Id>ND1311:DontUseObsoleteTypesMethodsOrFields</Id>
warnif count > 0
let obsoleteTypes = Types .Where(t => t.IsObsolete).ToHashSetEx()
let obsoleteMethods = Methods .Where(m => m.IsObsolete).ToHashSetEx()
let obsoleteFields = Fields .Where(f => f.IsObsolete).ToHashSetEx()
let obsoleteProperties = Properties.Where(p => p.IsObsolete).ToHashSetEx()
let obsoleteEvents = Events .Where(e => e.IsObsolete).ToHashSetEx()
// Go from the few obsolete members to their users, instead of scanning every JustMyCode method 5 times
let obsoleteAccessorsAndFields = obsoleteMethods.Cast<IMember>().Concat(obsoleteFields)
.Concat(obsoleteProperties.SelectMany(x => x.AccessorsAndBackingField).Where(x => x.IsMethod))
.Concat(obsoleteEvents.SelectMany(x => x.AccessorsAndBackingField).Where(x => x.IsMethod))
let usersOfObsoleteMembers = obsoleteAccessorsAndFields
.SelectMany(x => x.IsMethod ? x.AsMethod.MethodsCallingMe : x.AsField.MethodsUsingMe)
.Where(x => JustMyCode.Contains(x))
from m in JustMyCode.Methods.UsingAny(obsoleteTypes).Union(usersOfObsoleteMembers)
let obsoleteTypesUsed = obsoleteTypes.UsedBy(m)
// Optimization: MethodsCalled + Intersect() is faster than using obsoleteMethods.UsedBy()
let obsoleteMethodsUsed = m.MethodsCalled.Intersect(obsoleteMethods)
let obsoleteFieldsUsed = obsoleteFields.UsedBy(m)
let obsoletePropertiesUsed = obsoleteProperties.SelectMany(x => x.AccessorsAndBackingField).UsedBy(m)
let obsoleteEventsUsed = obsoleteEvents.SelectMany(x => x.AccessorsAndBackingField).UsedBy(m)
let obsoleteUsage = obsoleteTypesUsed.Except(m.ParentType).Cast<IMember>()
.Concat(obsoleteMethodsUsed)
.Concat(obsoleteFieldsUsed)
.Concat(obsoletePropertiesUsed)
.Concat(obsoleteEventsUsed).ToArray()
where obsoleteUsage.Length > 0
select new {
m,
obsoleteUsage,
Debt = (5*obsoleteUsage.Count()).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* shouldn't use the *{1}* {1?:all }marked as *obsolete*.
// Removing {1?this usage:these usages} will facilitate smoother transitions to newer versions.
//</Expl>
//<Desc>
// Warns about methods using a type, method or field tagged with
// *System.ObsoleteAttribute*: such API elements will be removed sooner or
// later and shouldn't be used.
//</Desc>
//<HowToFix>
// Follow the *workaround message* typically provided with
// *System.ObsoleteAttribute* to avoid the obsolete element.
//
// Estimated Debt: 5 minutes per type, method or field used. Severity is
// **High**: it is important to no longer rely on obsolete code.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Do implement methods that throw NotImplementedException - Design
Warns about methods using NotImplementedException: a temporary facility to declare a method stub deferring implementation (useful in TDD where tests fail until implemented). Before releasing, it shouldn't be used anywhere.
Don't use it permanently to mean this method should be overriden or this facility is unsupported — use abstract methods/classes instead, favoring compile time errors over run-time errors.
How to Fix:
Investigate why NotImplementedException is still thrown.
Severity is High if the method only throws it: the method should be implemented, is bad run-time-error-by-design, or is a removable stub. Severity is Medium if thrown from a method with significant logic: often the fix is throwing another type like InvalidOperationException.
// <Name>Do implement methods that throw NotImplementedException</Name>
// <Id>ND1312:DoImplementMethodsThatThrowNotImplementedException</Id>
warnif count > 0
from m in JustMyCode.Methods
where m.CreateA("System.NotImplementedException".AllowNoMatch())
select new {
m,
m.NbLinesOfCode,
Debt = (m.NbLinesOfCode == 1 ? 10 : 3).ToMinutes().ToDebt(),
Severity = m.NbLinesOfCode == 1 ? Severity.High : Severity.Medium
}
//<Expl>
// The *{0}* throws *NotImplementedException*.
// Implement the missing functionality or refactor to remove
// this method or the usage of this exception class.
//</Expl>
//<Desc>
// Warns about methods using *NotImplementedException*: a *temporary*
// facility to declare a method *stub* deferring implementation (useful in
// **TDD** where tests fail until implemented). Before releasing, it
// shouldn't be used anywhere.
//
// Don't use it permanently to mean *this method should be overriden* or
// *this facility is unsupported* — use *abstract* methods/classes instead,
// favoring *compile time* errors over *run-time* errors.
//</Desc>
//<HowToFix>
// Investigate why *NotImplementedException* is still thrown.
//
// Severity is **High** if the method only throws it: the method should be
// implemented, is bad *run-time*-error-by-design, or is a removable stub.
// Severity is **Medium** if thrown from a method with significant logic:
// often the fix is throwing another type like **InvalidOperationException**.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Override equals and operator equals on value types - Design
For value types, the inherited Equals() uses Reflection (expensive) and compares all instance fields (often unnecessary). If instances get compared, sorted or used as hash table keys, implement Equals(), GetHashCode() and the equality/inequality operators.
How to Fix:
Implement Equals(), GetHashCode() and the equality and inequality operators, or transform the type into a record struct.
Estimated Debt: 15 minutes plus 2 minutes per instance field.
// <Name>Override equals and operator equals on value types</Name>
// <Id>ND1313:OverrideEqualsAndOperatorEqualsOnValueTypes</Id>
warnif count > 0
from t in JustMyCode.Types where
t.IsStructure &&
// Don't warn about C# 15 union types, their struct is generated by the compiler
!t.IsUnion &&
t.InstanceFields.Count() > 0
let equalsMethod = t.InstanceMethods.Where(m0 => m0.Name == "Equals(Object)").SingleOrDefault()
where equalsMethod == null
where
// Don't warn about structure with a fixed field that is not meant to be compared
!t.Fields.Any(f => f.HasAttribute("System.Runtime.CompilerServices.FixedBufferAttribute".AllowNoMatch())) &&
// Don't warn about InlineArrayAttribute's field
!t.HasAttribute("System.Runtime.CompilerServices.InlineArrayAttribute".AllowNoMatch())
select new {
t,
t.InstanceFields,
Debt = (12 + 2*t.InstanceFields.Count()).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* should override the *Equals()* and *GetHashCode()* methods.
// Additionally, consider implementing the *==* and *!=* operators
// if you anticipate users comparing or sorting instances or using
// them as keys in hash tables.
// Alternatively transform it into a *record struct*.
//</Expl>
//<Desc>
// For value types, the inherited *Equals()* uses *Reflection* (expensive)
// and compares all instance fields (often unnecessary). If instances get
// compared, sorted or used as hash table keys, implement *Equals()*,
// *GetHashCode()* and the equality/inequality operators.
//</Desc>
//<HowToFix>
// Implement *Equals()*, *GetHashCode()* and the equality and inequality
// operators, or transform the type into a *record struct*.
//
// Estimated Debt: 15 minutes plus 2 minutes per instance field.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Boxing/unboxing should be avoided - Design
Boxing wraps a value type inside a
System.Object on the managed heap (implicit);
unboxing extracts it (explicit). More:
https://msdn.microsoft.com/en-us/library/yz2be5wk.aspx
Disabled by default to avoid noise; re-activate if boxing/unboxing matters to your team.
How to Fix:With .NET generics (especially generic collections), boxing/unboxing is rarely needed and can usually be refactored away. See:
http://stackoverflow.com/questions/4403055/boxing-unboxing-and-generics
Profile for CPU-heavy methods using boxing/unboxing, especially in a
loop, and refactor them. Severity is
Low: more an advice than a problem.
// <Name>Boxing/unboxing should be avoided</Name>
// <Id>ND1314:BoxingUnboxingShouldBeAvoided</Id>
warnif count > 0 from m in JustMyCode.Methods where
m.IsUsingBoxing ||
m.IsUsingUnboxing
select new {
m,
m.NbLinesOfCode,
m.IsUsingBoxing,
m.IsUsingUnboxing,
relyOn =
m.IsUsingBoxing && m.IsUsingUnboxing ? "boxing and unboxing" :
m.IsUsingBoxing ? "boxing" : "unboxing",
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.Low
}
//<Expl>
// The *{0}* relies on {4} that can negatively impact performances.
//</Expl>
//<Desc>
// *Boxing* wraps a value type inside a *System.Object* on the managed heap
// (implicit); *unboxing* extracts it (explicit). More:
// https://msdn.microsoft.com/en-us/library/yz2be5wk.aspx
//
// **Disabled by default** to avoid noise; re-activate if boxing/unboxing
// matters to your team.
//</Desc>
//<HowToFix>
// With .NET generics (especially generic collections), boxing/unboxing is
// rarely needed and can usually be refactored away. See:
// http://stackoverflow.com/questions/4403055/boxing-unboxing-and-generics
//
// Profile for CPU-heavy methods using boxing/unboxing, especially in a
// **loop**, and refactor them. Severity is **Low**: more an advice than a
// problem.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid namespaces mutually dependent - Architecture
Lists types and methods from a low-level namespace that use types and methods from a higher-level namespace, for each pair of namespaces using each other. Which is low-level is inferred by comparing the couplings [A to B] and [B to A] (types, methods and fields involved): the low-to-high coupling is typically the significantly lower one. Dependencies on the compiler's synthesized types of the global namespace (anonymous types, collection-expression lists) are ignored.
This avoids
namespaces dependency cycles, moving toward a
layered architecture where
low-level code cannot use
high-level code — i.e. getting rid of
spaghetti code: entangled code not properly layered and structured. More in our white books:
https://www.ndepend.com/docs/white-booksHow to Fix:Refactor so that
the low-level namespace doesn't use the high-level namespace. The rule lists each culprit dependency; patterns to remove them:
- Move types between the
low-level and
high-level namespaces.
- Use the
Dependency Inversion Principle (DIP):
https://blog.ndepend.com/solid-design-the-dependency-inversion-principle-dip Create interfaces in the
low-level namespace, implemented in the
high-level one, so low-level code consumes high-level code through interfaces (
callbacks), passed in via higher-level code — a.k.a.
Dependency Injection (DI):
http://en.wikipedia.org/wiki/Dependency_injection
May not apply to libraries with public mutually dependent namespaces, where breaking the API can cost more than the entangled code.
Debt depends on the number of types and methods used. Both namespaces form an unpartitionable
super-component, so
Annual Interest is one hour plus minutes proportional to their size in lines of code, divided by the number of issues listed. It often exceeds the
Debt: a year unfixed costs more than fixing once.
// <Name>Avoid namespaces mutually dependent</Name>
// <Id>ND1400:AvoidNamespacesMutuallyDependent</Id>
warnif count > 0
// Optimization: restraint application assemblies set
// If some namespaces are mutually dependent
// - They must be declared in the same assembly
// - The parent assembly must ContainsNamespaceDependencyCycle
from assembly in Application.Assemblies.Where(a => a.ContainsNamespaceDependencyCycle != null && a.ContainsNamespaceDependencyCycle.Value)
// hashset is used to avoid reporting both A <-> B and B <-> A
let hashset = new HashSet<INamespace>()
// The compiler's own synthesized types live in the global namespace (anonymous types,
// collection-expression lists): a usage of one is not a dependency the developer wrote.
let isSynthesized = new Func<IType, bool>(t => t.IsGeneratedByCompiler && t.ParentNamespace.Name.Length == 0)
// Optimization: restraint namespaces set
let namespacesSuspect = assembly.ChildNamespaces.Where(
// If a namespace doesn't have a Level value, it must be in a dependency cycle
// or it must be using directly or indirectly a dependency cycle.
n => n.Level == null &&
// Also require the namespace to be JustMyCode to avoid generated namespaces like My / My.Resources in VB.NET
JustMyCode.Contains(n))
from nA in namespacesSuspect
// Select namespaces mutually dependent with nA
let unused = hashset.Add(nA) // Populate hashset
let namespacesMutuallyDependentWith_nA = nA.NamespacesUsed.Using(nA)
.Except(hashset) // <-- avoid reporting both A <-> B and B <-> A
where namespacesMutuallyDependentWith_nA.Count() > 0
from nB in namespacesMutuallyDependentWith_nA.Where(
// Don't match namespaces generated by PostSharp
n => !n.Name.StartsWith("PostSharp.ImplementationDetails"))
// nA and nB are mutually dependent
// Infer which one is low level and which one is high level,
// for that we need to compute the coupling from A to B
// and from B to A in terms of number of types and methods
// usages involved in the coupling.
let typesOfBUsedByA = nB.ChildTypes.Where(t => !isSynthesized(t)).UsedBy(nA).ToArray() // Enumerate once
let couplingA2B =
typesOfBUsedByA.Sum(t => t.TypesUsingMe.Count(t1 => t1.ParentNamespace == nA)) +
typesOfBUsedByA.ChildMethods().Sum(m => m.MethodsCallingMe.Count(m1 => m1.ParentNamespace == nA))
let typesOfAUsedByB = nA.ChildTypes.Where(t => !isSynthesized(t)).UsedBy(nB).ToArray() // Enumerate once
let couplingB2A =
typesOfAUsedByB.Sum(t => t.TypesUsingMe.Count(t1 => t1.ParentNamespace == nB)) +
typesOfAUsedByB.ChildMethods().Sum(m => m.MethodsCallingMe.Count(m1 => m1.ParentNamespace == nB))
// The lowLevelNamespace is inferred from the fact that
// [coupling lowLevel -> highLevel] is lower than [coupling highLevel -> lowLevel]
let lowLevelNamespace = (couplingA2B < couplingB2A) ? nA : nB
let highLevelNamespace = (lowLevelNamespace == nA) ? nB : nA
let highLevelTypesUsed = (lowLevelNamespace == nA) ? typesOfBUsedByA : typesOfAUsedByB
let lowLevelTypesUser = lowLevelNamespace.ChildTypes.UsingAny(highLevelTypesUsed)
let lowLevelTypesMethodsUser = lowLevelTypesUser.Cast<IMember>()
.Concat(lowLevelTypesUser.ChildMethods().Using(highLevelNamespace))
.ToArray() // Enumerate once
// Make the rule works also when lines of code is not available
let lowLevelNamespaceLoc = lowLevelNamespace.NbLinesOfCode ?? (lowLevelNamespace.NbILInstructions / 7)
let highLevelNamespaceLoc = highLevelNamespace.NbLinesOfCode ?? (highLevelNamespace.NbILInstructions / 7)
let annualInterestPerIssue =
// Such issue has at least a Severity.Medium, never a Severity.Low
Math.Max(Severity.Medium.AnnualInterestThreshold().Value.TotalSeconds,
((3600 + lowLevelNamespaceLoc + highLevelNamespaceLoc) / Math.Max(1,lowLevelTypesMethodsUser.Length)).Value)
.ToSeconds().ToAnnualInterest()
// Select in details types and methods involved in the coupling lowLevelNamespace using highLevelNamespace
from tmCulprit in lowLevelTypesMethodsUser
let used = (tmCulprit.IsType ? tmCulprit.AsType.TypesUsed.Where(t => t.ParentNamespace == highLevelNamespace && !isSynthesized(t)) :
tmCulprit.AsMethod.MembersUsed.Where(m => m.ParentNamespace == highLevelNamespace &&
!isSynthesized(m.IsType ? m.AsType : m.ParentType)))
.ToArray() // Enumerate once
// A culprit whose only usages of the high-level namespace are synthesized types is not reported
where used.Length > 0
select new {
tmCulprit,
shouldntUse = used,
becauseNamespace = lowLevelNamespace,
shouldntUseNamespace = highLevelNamespace,
Debt = used.Length.Linear(1, 15, 10, 60).ToMinutes().ToDebt(),
AnnualInterest = annualInterestPerIssue
}
//<Expl>The *{0}* should not use the *{1}* because its parent *{2}* is at lower level than the *{3}*
// that contains the element{1?:s} used.
// This situation prevents **clean layered architecture** because both namespaces use each other.</Expl>
//<Desc>
// Lists types and methods from a low-level namespace that use types and
// methods from a higher-level namespace, for each pair of namespaces using
// each other. Which is low-level is inferred by comparing the couplings
// [A to B] and [B to A] (types, methods and fields involved): the low-to-high
// coupling is typically the significantly lower one. Dependencies on the
// compiler's synthesized types of the global namespace (anonymous types,
// collection-expression lists) are ignored.
//
// This avoids **namespaces dependency cycles**, moving toward a *layered
// architecture* where *low-level* code cannot use *high-level* code — i.e.
// getting rid of **spaghetti code: entangled code not properly layered and
// structured**. More in our white books:
// https://www.ndepend.com/docs/white-books
//</Desc>
//<HowToFix>
// Refactor so that **the low-level namespace doesn't use the high-level
// namespace**. The rule lists each culprit dependency; patterns to remove
// them:
//
// - Move types between the *low-level* and *high-level* namespaces.
//
// - Use the *Dependency Inversion Principle (DIP)*:
// https://blog.ndepend.com/solid-design-the-dependency-inversion-principle-dip
// Create interfaces in the *low-level* namespace, implemented in the
// *high-level* one, so low-level code consumes high-level code through
// interfaces (*callbacks*), passed in via higher-level code — a.k.a.
// *Dependency Injection (DI)*:
// http://en.wikipedia.org/wiki/Dependency_injection
//
// May not apply to libraries with public mutually dependent namespaces,
// where breaking the API can cost more than the entangled code.
//
// **Debt** depends on the number of types and methods used. Both namespaces
// form an unpartitionable *super-component*, so **Annual Interest** is one
// hour plus minutes proportional to their size in lines of code, divided by
// the number of issues listed. It often exceeds the *Debt*: a year unfixed
// costs more than fixing once.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid namespaces dependency cycles - Architecture
Lists all
application namespace dependency cycles; each row shows a cycle indexed with one entangled namespace. Avoiding cycles is a
simple yet efficient way to clean a code base architecture; see our white books:
https://www.ndepend.com/docs/white-booksHow to Fix:First fix the rule
Avoid namespaces mutually dependent: this eliminates most cycles. Remaining cycles involve 3+ namespaces, like
A is using B is using C is using A.
Right-click a cycle cell to export the namespaces to the dependency graph or matrix, illustrated here:
https://www.ndepend.com/docs/visual-studio-dependency-graph#Entangled Then decide which namespaces are low-level vs high-level and remove the offending dependencies (e.g. if A is low-level, C shouldn't use A), using the patterns in
Avoid namespaces mutually dependent HowToFix.
The matrix shows cycles as red squares and black cells; use the option
Display Direct and Indirect Dependencies. See:
https://www.ndepend.com/docs/dependency-structure-matrix-dsm#Cycle https://www.ndepend.com/docs/dependency-structure-matrix-dsm#Mutual
Estimated Debt doesn't depend on cycle length: even a long cycle can be broken by removing a few dependencies.
// <Name>Avoid namespaces dependency cycles</Name>
// <Id>ND1401:AvoidNamespacesDependencyCycles</Id>
warnif count > 0
// Optimization: restraint application assemblies set
// If some namespaces are mutually dependent
// - They must be declared in the same assembly
// - The parent assembly must ContainsNamespaceDependencyCycle
from assembly in Application.Assemblies
.Where(a => a.ContainsNamespaceDependencyCycle != null &&
a.ContainsNamespaceDependencyCycle.Value)
// Optimization: restraint namespaces set
let namespacesSuspect = assembly.ChildNamespaces.Where(n =>
// A namespace involved in a cycle necessarily have a null Level.
n.Level == null &&
// Also require the namespace to be JustMyCode to avoid generated namespaces like My / My.Resources in VB.NET
JustMyCode.Contains(n))
// hashset is used to avoid iterating again on namespaces already caught in a cycle.
let hashset = new HashSet<INamespace>()
from suspect in namespacesSuspect
// By commenting in this line, the query matches all namespaces involved in a cycle.
where !hashset.Contains(suspect)
// Define 2 code metrics
// - Namespaces depth of is using indirectly the suspect namespace.
// - Namespaces depth of is used by the suspect namespace indirectly.
// Note: for direct usage the depth is equal to 1.
let namespacesUserDepth = namespacesSuspect.DepthOfIsUsing(suspect)
let namespacesUsedDepth = namespacesSuspect.DepthOfIsUsedBy(suspect)
// Select namespaces that are both using and used by namespaceSuspect
let usersAndUsed = from n in namespacesSuspect where
namespacesUserDepth[n] > 0 &&
namespacesUsedDepth[n] > 0
select n
where usersAndUsed.Count() > 0
// Here we've found namespace(s) both using and used by the suspect namespace.
// A cycle involving the suspect namespace is found!
// v2017.3.2: don't call Append() as an extension method else ambiguous syntax error
// with the new extension method in .NET Fx v4.7.1 / .NET Standard 2.0: System.Linq.Enumerable.Append()
let cycle = ExtensionMethodsEnumerable.Append(usersAndUsed,suspect)
// Fill hashset with namespaces in the cycle.
// .ToArray() is needed to force the iterating process.
let unused1 = (from n in cycle let unused2 = hashset.Add(n) select n).ToArray()
select new {
suspect,
cycle,
Debt = 120.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{1}* form a **dependency cycle**: they all depend on each other.
// This cycle prevents **clean layered architecture** and reduces maintainability.
// To fix it, first address all issues stemming from the rule
// *Avoid mutually dependent namespaces*.
// Then read this rule documentation that explains how to use
// the *Dependency Inversion Principle* to fix cycles involving
// 3 or more namespaces.
//</Expl>
//<Desc>
// Lists all *application namespace dependency cycles*; each row shows a
// cycle indexed with one entangled namespace. Avoiding cycles is a *simple
// yet efficient* way to clean a code base architecture; see our white books:
// https://www.ndepend.com/docs/white-books
//</Desc>
//<HowToFix>
// First fix the rule **Avoid namespaces mutually dependent**: this
// eliminates most cycles. Remaining cycles involve 3+ namespaces, like
// **A is using B is using C is using A**.
//
// Right-click a cycle cell to export the namespaces to the dependency graph
// or matrix, illustrated here:
// https://www.ndepend.com/docs/visual-studio-dependency-graph#Entangled
// Then decide which namespaces are low-level vs high-level and remove the
// offending dependencies (e.g. if A is low-level, C shouldn't use A), using
// the patterns in *Avoid namespaces mutually dependent* HowToFix.
//
// The matrix shows cycles as red squares and black cells; use the option
// **Display Direct and Indirect Dependencies**. See:
// https://www.ndepend.com/docs/dependency-structure-matrix-dsm#Cycle
// https://www.ndepend.com/docs/dependency-structure-matrix-dsm#Mutual
//
// Estimated Debt doesn't depend on cycle length: even a long cycle can be
// broken by removing a few dependencies.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid partitioning the code base through many small library Assemblies - Architecture
Matches assemblies with 3 or fewer types. Each assembly is a DLL and a project to maintain; too many is a symptom of treating
physical projects as
logical components. Prefer fewer, bigger projects with namespaces as logical components: faster compilation and startup, clearer Solution Explorer, easier deployment, fewer assemblies for library clients.
How to Fix:Use the
physical concept of assemblies for physical needs only. Our white book
Partitioning code base through .NET assemblies and Visual Studio projects details valid and invalid reasons:
https://www.ndepend.com/Res/NDependWhiteBook_Assembly.pdf
// <Name>Avoid partitioning the code base through many small library Assemblies</Name>
// <Id>ND1402:AvoidPartitioningTheCodeBaseThroughManySmallLibraryAssemblies</Id>
warnif count > 10
from a in JustMyCode.Assemblies where
// Don't match ASP.NET Core generated assemblies
// whose name is suffixed with '.Views'
!a.Name.EndsWith(".views", StringComparison.OrdinalIgnoreCase) &&
( a.ChildTypes.Count() <= 3) &&
// Don't match executable assemblies that can be small
!a.ChildMethods.Any(t => t.IsEntryPoint) &&
!a.ChildTypes.WithNameIn("Startup","Program").Any() &&
// Don't match DI assembly
!a.IsUsing(@"Microsoft.Extensions.DependencyInjection".MatchNamespace().AllowNoMatch())
select new {
a,
a.ChildTypes,
a.NbLinesOfCode,
a.NbILInstructions,
Debt = 40.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *library {0}* contains only the *{1}*.
// Avoid creating numerous small projects as they usually complicate
// code maintenance and usage.
//</Expl>
//<Desc>
// Matches assemblies with 3 or fewer types. Each assembly is a DLL and a
// project to maintain; too many is a symptom of treating **physical**
// projects as **logical** components. Prefer fewer, bigger projects with
// namespaces as logical components: faster compilation and startup, clearer
// Solution Explorer, easier deployment, fewer assemblies for library clients.
//</Desc>
//<HowToFix>
// Use the *physical* concept of assemblies for physical needs only. Our
// white book **Partitioning code base through .NET assemblies and Visual
// Studio projects** details valid and invalid reasons:
// https://www.ndepend.com/Res/NDependWhiteBook_Assembly.pdf
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Enforcing Clean Architecture - Architecture
Ensures strict inward dependency flow between layers: only Presentation -> Application -> Domain. Flags outward or sideways dependencies (e.g. Domain -> Infrastructure).
How to Fix:
1) Move business rules used by multiple layers into Domain or Application. 2) Replace direct references to outer layers with abstractions defined in an inner layer. 3) Implement those abstractions in Infrastructure/Presentation, injected from the composition root. 4) Move DTOs/view models to their boundary layer; map at boundaries instead of sharing concrete outer-layer types.
// <Name>Enforcing Clean Architecture</Name>
// <Id>ND1412:EnforcingCleanArchitecture</Id>
warnif count > 0
let getLayerTypes = new Func<string, HashSet<IType>>(
names => names.Split('|').SelectMany(name =>
Application.Namespaces.Where(n => n.Name.EndsWith("." + name) || n.Name.Contains("." + name + ".")).ChildTypes()
.Concat(Application.Assemblies.Where(a => a.Name.EndsWith("." + name)).ChildTypes())
).ToHashSetEx()
)
// Define layers
let layers = new[] {
new { Name = "Domain",
Types = getLayerTypes("Domain|Entities") },
new { Name = "Application",
Types = getLayerTypes("Application|UseCases") },
new { Name = "Infrastructure",
Types = getLayerTypes("Infrastructure|Persistence") },
new { Name = "Presentation",
Types = getLayerTypes("Presentation|UI|API|Web") }
}
// Define forbidden dependencies as anonymous types
let forbiddenDeps = new[] {
new { From = "Domain", To = "Application" },
new { From = "Domain", To = "Infrastructure" },
new { From = "Domain", To = "Presentation" },
new { From = "Application", To = "Infrastructure" },
new { From = "Application", To = "Presentation" },
new { From = "Infrastructure", To = "Presentation" }
}
// Search for forbidden dependencies
from dep in forbiddenDeps
let sourceLayer = layers.First(l => l.Name == dep.From)
let targetLayer = layers.First(l => l.Name == dep.To)
from tUser in sourceLayer.Types.UsingAny(targetLayer.Types)
let typesUsed = targetLayer.Types.Intersect(tUser.TypesUsed)
from tUsed in typesUsed
select new {
tUser,
tUsed,
error = dep.From + " must not use " + dep.To,
Debt = 7.ToMinutes().ToDebt(),
Severity = Severity.High
}
// <Explanation>
// The {0} shouldn't use {1} because {2}
// </Explanation>
// <Desc>
// Ensures strict inward dependency flow between layers: only
// Presentation -> Application -> Domain. Flags outward or sideways
// dependencies (e.g. Domain -> Infrastructure).
// </Desc>
// <HowToFix>
// 1) Move business rules used by multiple layers into Domain or Application.
// 2) Replace direct references to outer layers with abstractions defined in
// an inner layer.
// 3) Implement those abstractions in Infrastructure/Presentation, injected
// from the composition root.
// 4) Move DTOs/view models to their boundary layer; map at boundaries
// instead of sharing concrete outer-layer types.
// </HowToFix>
./rules/assets/img/StatusBigOk32x32.png
UI layer shouldn't use directly DB types - Architecture
A sample rule to adapt to your need showing how to define code layers and be warned about dependency violations. It checks if any UI layer type directly uses any DB framework layer type.
- DB framework layer: third-party types of ADO.NET, EntityFramework, NHibernate consumed by the application (easy to adjust).
- UI layer: types using WPF, Windows Form, ASP.NET.
UI using directly DB frameworks is poor design: DB accesses should be hidden from UI in a dedicated Data Access Layer (DAL). Estimated debt is proportional to the UI/DB coupling.
How to Fix:
The rule lists which UI type uses which DB framework type. Rather than fixing matches one by one, first imagine how DB framework accesses could be encapsulated into a dedicated layer.
// <Name>UI layer shouldn't use directly DB types</Name>
// <Id>ND1403:UILayerShouldntUseDirectlyDBTypes</Id>
warnif count > 0
// UI layer is made of types using a UI framework
let uiTypes = Application.Types.UsingAny(Assemblies.WithNameIn(
"Microsoft.AspNetCore", "PresentationFramework", "System.Windows", "System.Windows.Forms", "System.Web"))
// You can easily customize this part to define what are DB types.
let dbTypes = ThirdParty.Assemblies.WithNameIn("System.Data", "EntityFramework", "NHibernate").ChildTypes()
// Ideally even DataSet and associated, usage should be forbidden from UI layer:
// http://stackoverflow.com/questions/1708690/is-list-better-than-dataset-for-ui-layer-in-asp-net
.Except(ThirdParty.Types.WithNameIn("DataSet", "DataTable", "DataRow"))
from uiType in uiTypes.UsingAny(dbTypes)
let dbTypesUsed = dbTypes.Intersect(uiType.TypesUsed)
let dbTypesAndMembersUsed = dbTypesUsed.Union(dbTypesUsed.ChildMembers().UsedBy(uiType))
// Per defaut this rule estimates a technical debt
// proportional to the coupling between the UI and DB types.
let couplingPerUIType = 2 +
uiType.Methods.UsingAny(dbTypesUsed).Count() +
dbTypesAndMembersUsed.Count()
select new {
uiType,
dbTypesAndMembersUsed,
Debt = (4 * couplingPerUIType).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* shouldn't use directly *{1}* because doing so binds the UI layer directly with the DB layer.
//</Expl>
//<Desc>
// A *sample rule to adapt to your need* showing how to define code layers
// and be warned about dependency violations. It checks if any UI layer type
// directly uses any DB framework layer type.
//
// - **DB framework layer**: *third-party* types of *ADO.NET*,
// *EntityFramework*, *NHibernate* consumed by the application (easy to
// adjust).
//
// - **UI layer**: types using *WPF*, *Windows Form*, *ASP.NET*.
//
// *UI using directly DB frameworks* is *poor design*: DB accesses should be
// hidden from UI in a **dedicated Data Access Layer (DAL)**. Estimated debt
// is proportional to the UI/DB coupling.
//</Desc>
//<HowToFix>
// The rule lists which UI type uses which DB framework type. Rather than
// fixing matches one by one, first imagine how DB framework accesses could
// be encapsulated into a dedicated layer.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
UI layer shouldn't use directly DAL layer - Architecture
A
sample rule to adapt to your need showing how to define code layers and be warned about dependency violations. It checks if any UI layer type directly uses any DAL layer type.
-
DAL (Data Access Layer): application types using
ADO.NET,
EntityFramework,
NHibernate types (easy to adjust).
-
UI layer: types using
WPF,
Windows Form,
ASP.NET.
UI using directly DAL is
poor design: DAL accesses should be hidden from UI in an
intermediary domain logic. Estimated debt is proportional to the UI/DAL coupling.
How to Fix:The rule lists which UI type uses which DAL type. More on this topic:
http://www.kenneth-truyers.net/2013/05/12/the-n-layer-myth-and-basic-dependency-injection/
// <Name>UI layer shouldn't use directly DAL layer</Name>
// <Id>ND1404:UILayerShouldntUseDirectlyDALLayer</Id>
warnif count > 0
// UI layer is made of types using a UI framework
let uiTypes = Application.Types.UsingAny(Assemblies.WithNameIn(
"Microsoft.AspNetCore", "PresentationFramework", "System.Windows", "System.Windows.Forms", "System.Web"))
// Exclude commonly used DataSet and associated, from ADO.Net types
// You can easily customize this part to define what are DB types.
let dbTypes = ThirdParty.Assemblies.WithNameIn("System.Data", "EntityFramework", "NHibernate").ChildTypes()
.Except(ThirdParty.Types.WithNameIn("DataSet", "DataTable", "DataRow"))
// DAL layer is made of types using a DB framework
// .ToHashSetEx() results to faster execution of dalTypes.Intersect(uiType.TypesIUse).
let dalTypes = Application.Types.UsingAny(dbTypes).ToHashSetEx()
from uiType in uiTypes.UsingAny(dalTypes)
let dalTypesUsed = dalTypes.Intersect(uiType.TypesUsed)
let dalTypesAndMembersUsed = dalTypesUsed.Union(dalTypesUsed.ChildMembers().UsedBy(uiType))
// Per defaut this rule estimates a technical debt
// proportional to the coupling between the UI with the DAL layer.
let couplingPerUIType = 2 +
uiType.Methods.UsingAny(dalTypesUsed).Count() +
dalTypesAndMembersUsed.Count()
select new {
uiType,
// if dalTypesUsed is empty, it means that the uiType is part of the DAL
dalTypesAndMembersUsed,
Debt = (4 * couplingPerUIType).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* shouldn't use directly *{1}* because doing so binds the UI layer directly with the DAL layer.
//</Expl>
//<Desc>
// A *sample rule to adapt to your need* showing how to define code layers
// and be warned about dependency violations. It checks if any UI layer type
// directly uses any DAL layer type.
//
// - **DAL (Data Access Layer)**: application types using *ADO.NET*,
// *EntityFramework*, *NHibernate* types (easy to adjust).
//
// - **UI layer**: types using *WPF*, *Windows Form*, *ASP.NET*.
//
// *UI using directly DAL* is *poor design*: DAL accesses should be hidden
// from UI in an **intermediary domain logic**. Estimated debt is
// proportional to the UI/DAL coupling.
//</Desc>
//<HowToFix>
// The rule lists which UI type uses which DAL type. More on this topic:
// http://www.kenneth-truyers.net/2013/05/12/the-n-layer-myth-and-basic-dependency-injection/
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Assemblies with poor cohesion (RelationalCohesion) - Architecture
Warns about assemblies with a wrong Relational Cohesion: the number of relationships between the assembly's types divided by the number of types (average types used per type). Classes inside an assembly should be strongly related, so cohesion should be high. Assemblies with fewer than 20 types are ignored.
How to Fix:
Matches reveal either specific coding constraints (like generated code) or design issues; in the latter case plan refactoring to increase overall design and maintainability.
Severity is Low: Relational Cohesion informs about code structure but is not actionable. Fixing actionable Architecture and Code Smells issues will necessarily increase the scores.
// <Name>Assemblies with poor cohesion (RelationalCohesion)</Name>
// <Id>ND1405:AssembliesWithPoorRelationalCohesion</Id>
warnif count > 0 from a in Application.Assemblies
// Build the types list on which we want to check cohesion
// This is the assembly 'a' type, minus enumeration
// and types generated by the compiler.
let types = a.ChildTypes.Where(
t => !t.IsGeneratedByCompiler &&
!t.IsEnumeration &&
JustMyCode.Contains(t))
// Absolutly need ToHashet() to have fast Intersect() calls below.
.ToHashSetEx()
// Relational Cohesion metrics is relevant only if there are enough types
where types.LongCount()> 20
// R is the total number of relationship between types of the assemblies.
let R = types.Sum(t => t.TypesUsed.Intersect(types).Count())
// Relational Cohesion formula
let relationalCohesion = (double)R / types.Count
where relationalCohesion < 0.8
select new {
a,
a.ChildTypes,
relationalCohesion,
a.RelationalCohesion,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.Low
}
//<Expl>
// The *{0}* has a *Relational Cohesion* value of {2}.
// This suggests that the types within this assembly are loosely coupled,
// potentially signaling an architectural concern.
//</Expl>
//<Desc>
// Warns about assemblies with a wrong *Relational Cohesion*: the number of
// relationships between the assembly's types divided by the number of types
// (average types used per type). Classes inside an assembly should be
// strongly related, so cohesion should be high. Assemblies with fewer than
// 20 types are ignored.
//</Desc>
//<HowToFix>
// Matches reveal either specific coding constraints (like generated code) or
// design issues; in the latter case plan refactoring to increase overall
// design and maintainability.
//
// Severity is **Low**: *Relational Cohesion* informs about code structure
// but **is not actionable**. Fixing actionable **Architecture** and
// **Code Smells** issues will necessarily increase the scores.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Namespaces with poor cohesion (RelationalCohesion) - Architecture
Warns about namespaces with a wrong Relational Cohesion: the number of relationships between the namespace's types divided by the number of types (average types used per type). Classes inside a namespace should be strongly related, so cohesion should be high. Namespaces with fewer than 20 types are ignored.
How to Fix:
Matches reveal either specific coding constraints (like generated code) or design issues; in the latter case plan refactoring sessions. Right-click the ChildTypes cells to export them to the graph and get a class coupling overview.
Severity is Low: Relational Cohesion informs about code structure but is not actionable. Fixing actionable Architecture and Code Smells issues will necessarily increase the scores.
// <Name>Namespaces with poor cohesion (RelationalCohesion)</Name>
// <Id>ND1406:NamespacesWithPoorRelationalCohesion</Id>
warnif count > 0 from n in Application.Namespaces
// Build the types list on which we want to check cohesion
// This is the namespace children types, minus enumerations
// and types generated by the compiler.
let types = n.ChildTypes.Where(
t => !t.IsEnumeration && JustMyCode.Contains(t))
// Absolutly need ToHashet() to have fast Intersect() calls below.
.ToHashSetEx()
// Relational Cohesion metrics is relevant only if there are enough types
where types.LongCount() > 20
// R is the total number of relationship between types of the namespaces.
let R = types.Sum(t => t.TypesUsed.Intersect(types).Count())
// Relational Cohesion formula
let relationalCohesion = (double)R / types.Count
where relationalCohesion < 0.8
select new {
n,
n.ChildTypes,
relationalCohesion,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.Low
}
//<Expl>
// The *{0}* has a *Relational Cohesion* value of {2}.
// This suggests that the types within this namespace are loosely coupled,
// potentially signaling an architectural concern.
//</Expl>
//<Desc>
// Warns about namespaces with a wrong *Relational Cohesion*: the number of
// relationships between the namespace's types divided by the number of types
// (average types used per type). Classes inside a namespace should be
// strongly related, so cohesion should be high. Namespaces with fewer than
// 20 types are ignored.
//</Desc>
//<HowToFix>
// Matches reveal either specific coding constraints (like generated code) or
// design issues; in the latter case plan refactoring sessions. Right-click
// the *ChildTypes* cells to export them to the graph and get a class
// coupling overview.
//
// Severity is **Low**: *Relational Cohesion* informs about code structure
// but **is not actionable**. Fixing actionable **Architecture** and
// **Code Smells** issues will necessarily increase the scores.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Assemblies that don't satisfy the Abstractness/Instability principle - Architecture
The
Abstractness versus Instability Diagram identifies assemblies
potentially painful to maintain (concrete and stable) and
potentially useless (abstract and instable).
-
Abstractness: many abstract types (interfaces, abstract classes), few concrete ones.
-
Stability: types used by many types from other assemblies (
painful to modify).
The
normalized distance ([0.0 … 1.0]) from the idealized line
A + I = 1 (
main sequence) indicates the abstractness/stability balance; this rule warns above 0.7. Metric explained here:
https://www.ndepend.com/docs/code-metrics#DitFromMainSeq Concepts introduced by
Robert C. Martin in 1994:
http://www.objectmentor.com/resources/articles/oodmetrc.pdfHow to Fix:The assembly is either
potentially painful to maintain (massively used, mostly concrete: create abstractions to reduce coupling with concrete implementations) or
potentially useless (mostly abstract, not used enough: review the design).
Severity is
Low: this principle informs about code structure but
is not actionable. Fixing actionable
Architecture and
Code Smells issues will push the scores in the right direction.
// <Name>Assemblies that don't satisfy the Abstractness/Instability principle</Name>
// <Id>ND1407:AssembliesThatDontSatisfyTheAbstractnessInstabilityPrinciple</Id>
warnif count > 0 from a in Application.Assemblies
where a.NormDistFromMainSeq > 0.7
orderby a.NormDistFromMainSeq descending
select new {
a,
a.NormDistFromMainSeq,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.Low
}
//<Expl>
// The *{0}* has a *Normalized Distance from the Main Sequence* metric equals to {1}.
// To understand this metric, its implications, and the necessary corrective actions,
// please read this rule description.
//</Expl>
//<Desc>
// The **Abstractness versus Instability Diagram** identifies assemblies
// **potentially painful to maintain** (concrete and stable) and
// **potentially useless** (abstract and instable).
//
// - **Abstractness**: many abstract types (interfaces, abstract classes),
// few concrete ones.
//
// - **Stability**: types used by many types from other assemblies (*painful
// to modify*).
//
// The *normalized distance* ([0.0 … 1.0]) from the idealized line
// **A + I = 1** (*main sequence*) indicates the abstractness/stability
// balance; this rule warns above 0.7. Metric explained here:
// https://www.ndepend.com/docs/code-metrics#DitFromMainSeq
// Concepts introduced by *Robert C. Martin* in 1994:
// http://www.objectmentor.com/resources/articles/oodmetrc.pdf
//</Desc>
//<HowToFix>
// The assembly is either *potentially painful to maintain* (massively used,
// mostly concrete: create abstractions to reduce coupling with concrete
// implementations) or *potentially useless* (mostly abstract, not used
// enough: review the design).
//
// Severity is **Low**: this principle informs about code structure but
// **is not actionable**. Fixing actionable **Architecture** and
// **Code Smells** issues will push the scores in the right direction.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid excessive class coupling - Architecture
Assesses class coupling for types and methods by counting the unique application types consumed and their namespaces. High coupling suggests too many responsibilities, violating the Single Responsibility Principle:
https://blog.ndepend.com/solid-design-the-single-responsibility-principle-srp/How to Fix:Break the type or method into smaller, more focused components each following a single responsibility.
// <Name>Avoid excessive class coupling</Name>
// <Id>ND1410:AvoidExcessiveClassCoupling</Id>
warnif count > 0
from x in JustMyCode.Types.Concat(JustMyCode.Methods.Cast<ICodeContainer>())
// Don't match too linear method or type,
// hence require a minimum of cyclomatic complexity
let cyclomaticComplexity = x.IsMethod ?
x.AsMethod.CyclomaticComplexity :
x.AsType.CyclomaticComplexity
where cyclomaticComplexity >= (x.IsMethod ? 10 : 25)
// Optimization before really counting the number of types used
where x.IsType ? x.AsType.NbTypesUsed >= 90 :
x.AsMethod.NbMethodsCalled + x.AsMethod.FieldsUsed.Count() >= 40
let typesUsed =
(x.IsMethod ?
x.AsMethod.MembersUsed.Where(m1 => !m1.IsThirdParty).Select(m1 => m1.ParentType).Distinct() :
x.AsType.TypesUsed.Where(t1 => !t1.IsThirdParty)).ToArray()
where typesUsed.Length >= (x.IsMethod ? 40 : 90)
// Require that not all types used are in the same namespace
// to enforce several responsibilities
let namespacesUsed = typesUsed.ParentNamespaces().ToArray()
where namespacesUsed.Length >= (x.IsMethod ? 5 : 10)
orderby typesUsed.Length descending
select new {
x,
typesUsed,
namespacesUsed,
x.NbLinesOfCode,
cyclomaticComplexity,
x.MaintainabilityIndex,
nbTypes = typesUsed.Length,
nbNamespaces = namespacesUsed.Length,
Debt = (x.IsMethod ?
typesUsed.Length.Linear(40, 8, 200, 60) :
typesUsed.Length.Linear(90, 15, 500, 120)
).ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* makes use of *{6}* application types spread across *{7}* namespaces.
// This suggests that it may be handling too many responsibilities.
//</Expl>
//<Desc>
// Assesses class coupling for types and methods by counting the unique
// application types consumed and their namespaces. High coupling suggests
// too many responsibilities, violating the Single Responsibility Principle:
// https://blog.ndepend.com/solid-design-the-single-responsibility-principle-srp/
//</Desc>
//<HowToFix>
// Break the type or method into smaller, more focused components each
// following a single responsibility.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Higher cohesion - lower coupling - Architecture
Good architecture separates abstract namespaces (only interfaces, enumerations, delegates) from concrete ones (classes, structures). The more concrete namespaces rely only on abstract ones, the more Decoupled the architecture and the more Cohesive the classes.
This query shows, for each concrete namespace, which concrete and abstract namespaces are used.
How to Fix:
Transform this query into a rule if you wish to constrain your coupling / cohesion ratio.
// <Name>Higher cohesion - lower coupling</Name>
// <Id>ND1408:HigherCohesionLowerCoupling</Id>
// warnif count > 0
let abstractNamespaces = JustMyCode.Namespaces.Where(
n => n.ChildTypes.Where(t => !t.IsInterface && !t.IsEnumeration && !t.IsDelegate).Count() < 3 &&
n.NbLinesOfCode <= 20
).ToHashSetEx()
let concreteNamespaces = JustMyCode.Namespaces.Except(abstractNamespaces).ToHashSetEx()
from n in concreteNamespaces
let namespacesUsed = n.NamespacesUsed.ExceptThirdParty()
let concreteNamespacesUsed = namespacesUsed.Except(abstractNamespaces)
let abstractNamespacesUsed = namespacesUsed.Except(concreteNamespaces)
orderby concreteNamespacesUsed.Count() descending
select new {
n,
concreteNamespacesUsed ,
abstractNamespacesUsed,
// Debt = 50.ToMinutes().ToDebt(),
// Severity = Severity.High
}
//<Expl>
// The *{0}* is deemed as *concrete* because it contains implementations
// and it shouldn't rely on other *concrete {1}*.
// Instead, it should depend on namespaces that contain mostly abstractions.
//</Expl>
//<Desc>
// Good architecture separates *abstract* namespaces (only interfaces,
// enumerations, delegates) from *concrete* ones (classes, structures). The
// more concrete namespaces rely *only* on abstract ones, the more
// **Decoupled** the architecture and the more **Cohesive** the classes.
//
// This query shows, for each concrete namespace, which concrete and
// abstract namespaces are used.
//</Desc>
//<HowToFix>
// Transform this query into a rule if you wish to constrain your
// *coupling / cohesion* ratio.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid mutually-dependent types - Architecture
Matches pairs of classes or structures using each other: neither can be reviewed, refactored or tested without the other.
Disabled by default: mutual dependency is legitimate in common situations like recursive data structures, complex serialization or the visitor pattern. Enable it if you wish to fix most of its issues.
How to Fix:Identify the unwanted dependency (
t1 using
t2, or the reverse) and remove it, often via the
dependency inversion principle — interfaces abstracting one implementation from the other:
https://blog.ndepend.com/solid-design-the-dependency-inversion-principle-dip
// <Name>Avoid mutually-dependent types</Name>
// <Id>ND1409:AvoidMutuallyDependentTypes</Id>
warnif count > 0
from t1 in Application.Types
where t1.NbLinesOfCode > 10 &&
(t1.IsClass || t1.IsStructure)
from t2 in t1.TypesUsingMe
where t2.NbLinesOfCode > 10 &&
(t2.IsClass || t2.IsStructure) &&
// The mutual check — this is what keeps test types out of the pairs.
t1.IsUsing(t2)
// To avoid reporting twice the same pair
// use lexicographically sort of full names of types
// to only take the first one.
where new string[] {t1.FullName, t2.FullName}.OrderBy(x => x).First() == t1.FullName
// So far we only keep dependencies that involve method call.
// It is possible to comment the two 'where' clauses below to get also
// others kind of dependencies, like usage of typeof(Class).
let t2MethodsUsingT1 = t2.Methods.Where(m => m.IsUsing(t1)).ToArray()
where t2MethodsUsingT1.Length > 0
let t1MethodsUsingT2 = t1.Methods.Where(m => m.IsUsing(t2)).ToArray()
where t1MethodsUsingT2.Length > 0
select new {
t1, t2,
t2MethodsUsingT1,
t1MethodsUsingT2,
Debt = 20.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* and the *{1}* are mutually dependent.
// To address this, identify the direction in which to apply
// the *dependency inversion principle* and make the necessary adjustments.
//</Expl>
//<Desc>
// Matches pairs of classes or structures using each other: neither can be
// reviewed, refactored or tested without the other.
//
// Disabled by default: mutual dependency is legitimate in common situations
// like recursive data structures, complex serialization or the visitor
// pattern. Enable it if you wish to fix most of its issues.
//</Desc>
//<HowToFix>
// Identify the unwanted dependency (*t1* using *t2*, or the reverse) and
// remove it, often via the **dependency inversion principle** — interfaces
// abstracting one implementation from the other:
// https://blog.ndepend.com/solid-design-the-dependency-inversion-principle-dip
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Example of custom rule to check for dependency - Architecture
A sample rule showing how to check if a dependency exists from a code element A to B (each being an Assembly, Namespace, Type, Method or Field, not necessarily of the same kind).
Generate such a rule by right-clicking the Dependency Matrix cell (B in row, A in column) or the Dependency Graph arrow from A to B, then clicking Generate a code rule that warns if this dependency exists. Adapt the generated rule to your needs.
How to Fix:
This is a sample rule, there is nothing to fix as is.
// <Name>Example of custom rule to check for dependency</Name>
// <Id>ND9900:ExplicitId9900</Id>
warnif count > 0 from a in Application.Assemblies
where
a.IsUsing("Foo1.Foo2".AllowNoMatch().MatchNamespace()) &&
(a.Name == @"Foo3")
select new {
a,
a.NbLinesOfCode,
// Debt and Severity / Annual Interest can be modified to estimate well the
// effort to fix the issue and the annual cost to leave the issue unfixed.
Debt = 30.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* shouldn't use directly the namespace *Foo3.Foo4*
// because (TODO insert your reason).
//</Expl>
//<Desc>
// A **sample rule** showing how to check if a dependency exists from a code
// element **A** to **B** (each being an *Assembly*, *Namespace*, *Type*,
// *Method* or *Field*, not necessarily of the same kind).
//
// Generate such a rule by right-clicking the *Dependency Matrix* cell (**B**
// in row, **A** in column) or the *Dependency Graph* arrow from **A** to
// **B**, then clicking
// **Generate a code rule that warns if this dependency exists**.
// Adapt the generated rule to your needs.
//</Desc>
//<HowToFix>
// This is a *sample rule*, there is nothing to fix *as is*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
API Breaking Changes: Types - API Breaking Changes
Runs only in diff mode (baseline defined). Warns if a type publicly visible in the baseline is no longer public or has been removed: client code using it will be broken.
How to Fix:
Keep public types public, now and in the future. If a public type must be removed, tag it with System.ObsoleteAttribute and a workaround message for a few releases first (obsolete-tagged removals are not matched).
Severity is High: an API Breaking change can provoke significant friction with API consumers.
// <Name>API Breaking Changes: Types</Name>
// <Id>ND1500:APIBreakingChangesTypes</Id>
warnif count > 0 from t in codeBase.OlderVersion().Application.Types
where t.IsPubliclyVisible
let reason =
// The type has been removed, it was not tagged as obsolete
// and its parent assembly hasn't been removed …
( t.WasRemoved() &&
!t.ParentAssembly.WasRemoved() &&
!t.IsObsolete) ? "was public and has been removed" :
// … or the type is not publicly visible anymore
(!t.WasRemoved() && !t.NewerVersion().IsPubliclyVisible) ?
"used to be publicly visible but not anymore" : null
where reason != null
select new {
t,
NewVisibility =
(t.WasRemoved() ? " " :
t.NewerVersion().Visibility.ToString()),
reason,
Debt = 20.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>The *{0}* {2}. This breaks client code using it.</Expl>
//<Desc>
// Runs only in *diff mode* (baseline defined). Warns if a type publicly
// visible in the *baseline* is no longer public or has been removed:
// client code using it will be broken.
//</Desc>
//<HowToFix>
// Keep public types public, now and in the future. If a public type must be
// removed, tag it with *System.ObsoleteAttribute* and a *workaround message*
// for a few releases first (obsolete-tagged removals are not matched).
//
// Severity is **High**: an API Breaking change can provoke significant
// friction with API consumers.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
API Breaking Changes: Methods - API Breaking Changes
Runs only in diff mode (baseline defined). Warns if a method publicly visible in the baseline is no longer public or has been removed: client code using it will be broken.
How to Fix:
Keep public methods public, now and in the future. If a public method must be removed, tag it with System.ObsoleteAttribute and a workaround message for a few releases first (obsolete-tagged removals are not matched).
Severity is High: an API Breaking change can provoke significant friction with API consumers.
// <Name>API Breaking Changes: Methods</Name>
// <Id>ND1501:APIBreakingChangesMethods</Id>
warnif count > 0 from m in codeBase.OlderVersion().Application.Methods
where m.IsPubliclyVisible
// The method has been removed, it was not tagged as obsolete
// and its parent type hasn't been removed …
let reason =
( m.WasRemoved() &&
!m.IsVirtual &&
!m.ParentType.WasRemoved() &&
!m.IsObsolete ) ? (
m.ParentType.NewerVersion().MethodsAndConstructors.Any(m1 => m1.WasAdded() && m1.SimpleNameEquals(m.SimpleName)) ?
"is public and has different parameters than in baseline" :
"was public and has been removed"
) :
// … or the method is not publicly visible anymore
// but the parent type is still publicly visible
(!m.WasRemoved() && !m.NewerVersion().IsPubliclyVisible &&
m.ParentType.NewerVersion().IsPubliclyVisible) ?
"used to be publicly visible but not anymore" :
// … or the method return type has changed
(!m.WasRemoved() && m.ReturnType != null && m.NewerVersion().ReturnType != null
&& m.ReturnType.FullName != m.NewerVersion().ReturnType.FullName)
? "is public and has a different return type" : null
where reason != null
//--------------------------------------
// Handle special case: if between two versions a regular property becomes
// an auto-property (or vice-versa) the property getter/setter method have
// a different value for IMethod.IsGeneratedByCompiler
// since auto-property getter/setter are marked as generated by the compiler.
//
// If a method IsGeneratedByCompiler value changes between two versions,
// NDepend doesn't pair the newer/older occurences of the method.
//
// Hence in such situation, a public method is seen as added
// and a public method is seen as removed, but the API is not broken!
// The equivalentMethod-check below avoids reporting such
// API Breaking Change false-positive.
let equivalentMethod = m.WasRemoved() && m.ParentType.IsPresentInBothBuilds() ?
m.ParentType.NewerVersion().Methods
.FirstOrDefault(m1 =>
m1.IsPubliclyVisible &&
m1.Name == m.Name &&
m1.IsGeneratedByCompiler != m.IsGeneratedByCompiler &&
(m1.ReturnType == null || m.ReturnType == null || m1.ReturnType.FullName == m.ReturnType.FullName)
)
: null
where equivalentMethod == null
//--------------------------------------
select new {
m,
NewVisibility =
(m.WasRemoved() ? " " :
m.NewerVersion().Visibility.ToString()),
reason,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>The *{0}* {2}. This breaks client code using it.</Expl>
//<Desc>
// Runs only in *diff mode* (baseline defined). Warns if a method publicly
// visible in the *baseline* is no longer public or has been removed:
// client code using it will be broken.
//</Desc>
//<HowToFix>
// Keep public methods public, now and in the future. If a public method must
// be removed, tag it with *System.ObsoleteAttribute* and a *workaround
// message* for a few releases first (obsolete-tagged removals are not
// matched).
//
// Severity is **High**: an API Breaking change can provoke significant
// friction with API consumers.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
API Breaking Changes: Fields - API Breaking Changes
Runs only in diff mode (baseline defined). Warns if a field publicly visible in the baseline is no longer public or has been removed: client code using it will be broken.
How to Fix:
Keep public fields public, now and in the future. If a public field must be removed, tag it with System.ObsoleteAttribute and a workaround message for a few releases first (obsolete-tagged removals are not matched).
Severity is High: an API Breaking change can provoke significant friction with API consumers.
// <Name>API Breaking Changes: Fields</Name>
// <Id>ND1502:APIBreakingChangesFields</Id>
warnif count > 0 from f in codeBase.OlderVersion().Application.Fields
where f.IsPubliclyVisible
// The field has been removed, it was not tagged as obsolete
// and its parent type hasn't been removed …
let reason =
( ( f.WasRemoved() &&
!f.ParentType.WasRemoved() &&
!f.IsObsolete) ?
"was public and has been removed" :
// … or the field is not publicly visible anymore
// but the parent type is still publicly visible
(!f.WasRemoved() && !f.NewerVersion().IsPubliclyVisible &&
f.ParentType.NewerVersion().IsPubliclyVisible) ?
"used to be publicly visible but not anymore" :
// … or the field type has changed
(!f.WasRemoved() && f.FieldType != null && f.NewerVersion().FieldType != null
&& f.FieldType.FullName != f.NewerVersion().FieldType.FullName)
? "is public and has a different field type" : null
)
where reason != null
select new {
f,
NewVisibility =
(f.WasRemoved() ? " " :
f.NewerVersion().Visibility.ToString()),
reason,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>The *{0}* {2}. This breaks client code using it.</Expl>
//<Desc>
// Runs only in *diff mode* (baseline defined). Warns if a field publicly
// visible in the *baseline* is no longer public or has been removed:
// client code using it will be broken.
//</Desc>
//<HowToFix>
// Keep public fields public, now and in the future. If a public field must
// be removed, tag it with *System.ObsoleteAttribute* and a *workaround
// message* for a few releases first (obsolete-tagged removals are not
// matched).
//
// Severity is **High**: an API Breaking change can provoke significant
// friction with API consumers.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
API Breaking Changes: Interfaces and Abstract Classes - API Breaking Changes
Runs only in diff mode (baseline defined). Warns if a publicly visible interface or abstract class got abstract methods added or removed: client code implementing or deriving from it will be broken.
How to Fix:
Keep public contracts of interfaces and abstract classes stable, now and in the future. If a contract must change, tag abstract methods to be removed with System.ObsoleteAttribute and a workaround message for a few releases first.
Severity is High (not Critical, since an interface is not necessarily meant to be implemented by API consumers).
// <Name>API Breaking Changes: Interfaces and Abstract Classes</Name>
// <Id>ND1503:APIBreakingChangesInterfacesAndAbstractClasses</Id>
warnif count > 0 from tNewer in Application.Types where
(tNewer.IsInterface || tNewer.IsClass && tNewer.IsAbstract) &&
tNewer.IsPubliclyVisible &&
tNewer.IsPresentInBothBuilds()
let tOlder = tNewer.OlderVersion() where tOlder.IsPubliclyVisible
let methodsRemoved = tOlder.Methods.Where(m => m.IsAbstract && m.WasRemoved())
let methodsAdded = tNewer.Methods.Where(m => m.IsAbstract && m.WasAdded())
where methodsAdded.Count() > 0 || methodsRemoved.Count() > 0
select new {
tNewer,
methodsAdded,
methodsRemoved,
usage = tNewer.IsInterface ? "implementing" : "deriving from",
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* is *public* and its member set have been
// changed since the baseline.
// This breaks client classes {3} it.
// New methods: *{1}* Removed methods: *{2}*
//</Expl>
//<Desc>
// Runs only in *diff mode* (baseline defined). Warns if a publicly visible
// interface or abstract class got abstract methods added or removed: client
// code implementing or deriving from it will be broken.
//</Desc>
//<HowToFix>
// Keep public contracts of interfaces and abstract classes stable, now and
// in the future. If a contract must change, tag abstract methods to be
// removed with *System.ObsoleteAttribute* and a *workaround message* for a
// few releases first.
//
// Severity is **High** (not **Critical**, since an interface is not
// necessarily meant to be implemented by API consumers).
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Broken serializable types - API Breaking Changes
Runs only in
diff mode (baseline defined). Warns about types tagged
SerializableAttribute whose serializable instance fields were added or removed (fields tagged
NonSerializedAttribute are ignored).
From
http://msdn.microsoft.com/library/system.serializableattribute.aspx : all fields of a
SerializableAttribute type are serialized by default, unless the type implements
ISerializable or fields are marked
NonSerializedAttribute.
How to Fix:Keep the serialization process of serializable types stable, now and in the future; else deal with
Version Tolerant Serialization:
https://msdn.microsoft.com/en-us/library/ms229752(v=vs.110).aspx
Severity is
High: an API Breaking change can provoke significant friction with API consumers.
// <Name>Broken serializable types</Name>
// <Id>ND1504:BrokenSerializableTypes</Id>
warnif count > 0
from t in Application.Types where
// Collect types tagged with SerializableAttribute
t.HasAttribute("System.SerializableAttribute".AllowNoMatch()) &&
!t.IsDelegate &&
t.IsPresentInBothBuilds()
// Find newer and older versions of NonSerializedAttribute
let newNonSerializedAttribute = ThirdParty.Types.WithFullName("System.NonSerializedAttribute").SingleOrDefault()
let oldNonSerializedAttribute = newNonSerializedAttribute == null ? null : newNonSerializedAttribute.OlderVersion()
// Find added or removed fields not marked with NonSerializedAttribute
let addedInstanceField = from f in t.InstanceFields where
f.WasAdded() &&
(newNonSerializedAttribute == null || !f.HasAttribute(newNonSerializedAttribute))
select f
let removedInstanceField = from f in t.OlderVersion().InstanceFields where
f.WasRemoved() &&
(oldNonSerializedAttribute == null || !f.HasAttribute(oldNonSerializedAttribute))
select f
where addedInstanceField.Count() > 0 || removedInstanceField.Count() > 0
select new {
t,
addedInstanceField,
removedInstanceField,
Debt = 20.ToMinutes().ToDebt(),
Severity = Severity.Critical
}
//<Expl>
// The field set of the serializable *{0}* changed since the baseline.
// This can break client code using it.
// New field{1?:s}: *{1}* Removed field{2?:s}: *{2}*
//</Expl>
//<Desc>
// Runs only in *diff mode* (baseline defined). Warns about types tagged
// *SerializableAttribute* whose serializable instance fields were added or
// removed (fields tagged *NonSerializedAttribute* are ignored).
//
// From http://msdn.microsoft.com/library/system.serializableattribute.aspx :
// all fields of a *SerializableAttribute* type are serialized by default,
// unless the type implements *ISerializable* or fields are marked
// *NonSerializedAttribute*.
//</Desc>
//<HowToFix>
// Keep the serialization process of serializable types stable, now and in
// the future; else deal with **Version Tolerant Serialization**:
// https://msdn.microsoft.com/en-us/library/ms229752(v=vs.110).aspx
//
// Severity is **High**: an API Breaking change can provoke significant
// friction with API consumers.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid changing enumerations Flags status - API Breaking Changes
Runs only in diff mode (baseline defined). Matches enumerations whose FlagsAttribute tagging changed since the baseline (added or removed).
FlagsAttribute is a strong property, less for behavior (only enum.ToString() changes) than for meaning: range of values or range of flags? Changing it can significantly impact clients.
How to Fix:
Keep the FlagsAttribute status of each enumeration stable, now and in the future.
// <Name>Avoid changing enumerations Flags status</Name>
// <Id>ND1505:AvoidChangingEnumerationsFlagsStatus</Id>
warnif count > 0
let oldFlags = codeBase.OlderVersion().ThirdParty.Types.WithFullName("System.FlagsAttribute").FirstOrDefault()
let newFlags = ThirdParty.Types.WithFullName("System.FlagsAttribute").FirstOrDefault()
where oldFlags != null || newFlags != null
from t in Application.Types where
t.IsEnumeration &&
t.IsPresentInBothBuilds()
let hasFlagsAttributeNow = newFlags != null && t.HasAttribute(newFlags)
let usedToHaveFlagsAttribute = oldFlags != null && t.OlderVersion().HasAttribute(oldFlags)
where hasFlagsAttributeNow != usedToHaveFlagsAttribute
select new {
t,
expl = hasFlagsAttributeNow ?
"is now tagged with the *FlagsAttribute* but wasn't in baseline" :
"used to be tagged with the *FlagsAttribute* in baseline but not anymore",
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The public *{0}* {1}. This breaks client code using it.
//</Expl>
//<Desc>
// Runs only in *diff mode* (baseline defined). Matches enumerations whose
// *FlagsAttribute* tagging changed since the *baseline* (added or removed).
//
// *FlagsAttribute* is a strong property, less for *behavior* (only
// *enum.ToString()* changes) than for *meaning*: **range of values** or
// **range of flags**? Changing it can significantly impact clients.
//</Desc>
//<HowToFix>
// Keep the *FlagsAttribute* status of each enumeration stable, now and in
// the future.
//</HowToFix>
./rules/assets/img/Info32x32.png
API: New publicly visible types - API Breaking Changes
Runs only in diff mode (baseline defined). Lists types new in the public surface of the analyzed assemblies.
// <Name>API: New publicly visible types</Name>
from t in Application.Types
where t.IsPubliclyVisible &&
// The type has been removed and its parent assembly hasn't been removed …
( (t.WasAdded() && !t.ParentAssembly.WasAdded()) ||
// … or the type existed but was not publicly visible
!t.WasAdded() && !t.OlderVersion().IsPubliclyVisible)
select new {
t,
OldVisibility =
(t.WasAdded() ? " " :
t.OlderVersion().Visibility.ToString()),
}
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists types new in the
// public surface of the analyzed assemblies.
//</Desc>
./rules/assets/img/Info32x32.png
API: New publicly visible methods - API Breaking Changes
Runs only in diff mode (baseline defined). Lists methods new in the public surface of the analyzed assemblies.
// <Name>API: New publicly visible methods</Name>
from m in Application.Methods
where m.IsPubliclyVisible &&
// The method has been removed and its parent assembly hasn't been removed …
( (m.WasAdded() && !m.ParentType.WasAdded()) ||
// … or the method existed but was not publicly visible
!m.WasAdded() && !m.OlderVersion().IsPubliclyVisible)
//--------------------------------------
// Handle special case: if between two versions a regular property becomes
// an auto-property (or vice-versa) the property getter/setter method have
// a different value for IMethod.IsGeneratedByCompiler
// since auto-property getter/setter are marked as generated by the compiler.
//
// If a method IsGeneratedByCompiler value changes between two versions,
// NDepend doesn't pair the newer/older occurences of the method.
//
// Hence in such situation, a public method is seen as added
// and a public method is seen as removed, but the API is not broken!
// The equivalentMethod-check below avoids reporting such
// API Breaking Change false-positive.
let equivalentMethod = m.WasAdded() && m.ParentType.IsPresentInBothBuilds() ?
m.ParentType.OlderVersion().Methods
.FirstOrDefault(m1 =>
m1.IsPubliclyVisible &&
m1.Name == m.Name &&
m1.IsGeneratedByCompiler != m.IsGeneratedByCompiler)
: null
where equivalentMethod == null
//--------------------------------------
select new {
m,
OldVisibility =
(m.WasAdded() ? " " :
m.OlderVersion().Visibility.ToString())
}
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists methods new in the
// public surface of the analyzed assemblies.
//</Desc>
./rules/assets/img/Info32x32.png
API: New publicly visible fields - API Breaking Changes
Runs only in diff mode (baseline defined). Lists fields new in the public surface of the analyzed assemblies.
// <Name>API: New publicly visible fields</Name>
from f in Application.Fields
where f.IsPubliclyVisible &&
// The field has been removed and its parent assembly hasn't been removed …
( (f.WasAdded() && !f.ParentType.WasAdded()) ||
// … or the t existed but was not publicly visible
!f.WasAdded() && !f.OlderVersion().IsPubliclyVisible)
select new {
f,
OldVisibility =
(f.WasAdded() ? " " :
f.OlderVersion().Visibility.ToString())
}
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists fields new in the
// public surface of the analyzed assemblies.
//</Desc>
./rules/assets/img/StatusBigOk32x32.png
Code should be tested - Code Coverage
Lists the methods not covered or partially covered by tests, one issue per method — also in a type no test reaches yet: writing the first tests of a type fixes issues, it never turns one issue into several.
The technical debt (effort to write tests) is the effort to develop the code element multiplied by factors in ]0,1.3] based on:
- the method code size and complexity
- the actual percentage coverage
- the abstractness of types used (classes are harder to test than interfaces)
- the method visibility (private/protected harder to test)
- reads of mutable static fields (state shared across tests executions)
- JustMyCode or not (NotMyCode is often generated, with generatable tests)
This rule is a large debt source: untested code is by definition part of the technical debt. The annual interest (annual cost of leaving code uncovered) is based on the effort to develop times usage factors.
The Testability column (0 to 100, higher = easier to unit test) and the TestHurdle column help to pick the quick wins first. Testability decreases with the number of methods called (the strongest sign), with a hurdle reached (hard: a process, the network, a database or console key input, up to 3 calls deep; soft: the file system, HttpClient, the environment, a sleep or console line input, called directly), with the method complexity, when tests cannot call the method (private, or internal without InternalsVisibleTo) and when no test reaches its type yet; it increases for a static method without mutable state. TestHurdle names the hurdle reached and, deeper than one call, the first call of the chain.
How to Fix:
Write unit tests covering the matched methods.
// <Name>Code should be tested</Name>
// <Id>ND1600:CodeShouldBeTested</Id>
warnif count > 0
// This lambda infers a factor in the range [0,1] from a sequence of distinct types used,
// based on how many of these types are actually concrete (i.e are not interfaces or enumeration).
// Primitive types (int, bool...) are eliminated from the sequence of types used
// by filtering types declared in the namespace System.
// Namespaces are per assembly: several "System" namespaces, compared by reference in a HashSet
let systemNamespaces = Namespaces.WithName("System").ToHashSetEx()
let abstractionUsageFactorFormula = new Func<IEnumerable<IType>,double>(typesUsed =>
typesUsed.Count(t => !systemNamespaces.Contains(t.ParentNamespace) && !t.IsInterface && !t.IsEnumeration)
/ (1 + typesUsed.Count(t => !systemNamespaces.Contains(t.ParentNamespace))))
// methodsUntested is all methods not 100% covered by tests
let methodsUntested = (from method in JustMyCode.Methods
where !method.IsExcludedFromCoverage &&
method.NbLinesOfCode >= 0 &&
method.NbILInstructions > 0 &&
method.PercentageCoverage < 100
// Factor in case method is partially covered
let uncoverageFactor = ((100 - method.PercentageCoverage) / 100).Value
// Complexity factor
let complexityFactor = ((method.CyclomaticComplexity ?? method.ILCyclomaticComplexity) + method.ILNestingDepth).Linear(0, 0.1, 10, 1).Value
// Not my code is often generated code and is in general easier to get tested since test can be generated as well.
let justMyCodeFactor = (JustMyCode.Contains(method) ? 1 : 0.4)
// abstractionUsageFactor reflects the fact that code that relies on interfaces
// is easier to test that code that relies on concrete classes.
let abstractionUsageFactor = 0.7 + 0.3 *abstractionUsageFactorFormula(method.MembersUsed.ParentTypes())
// The usageFactor depends on the method 'rank' that is a value
// indicating if the method is often used or not
let usageFactor = (method.Rank / (method.Rank + 4)).Value
// It is more complicated to write tests for non publicly visible methods
let visibilityFactor = method.Visibility.EqualsAny(Visibility.Public, Visibility.Internal) ? 1 :
method.Visibility != Visibility.Private ? 1.1 : 1.2
// Is is more complicated to write tests for methods that read mutable static fields
// whose changing state is shared across tests executions.
let staticFieldUsageFactor = method.ReadsMutableTypeState ? 1.3 : 1.0
// Both "effort to write tests" and "annual cost to not test" for a method
// is determined by several factors in the range [0,1] that multiplies the effortToDevelop
let effortToDevelopInMinutes = method.EffortToDevelop().Value.TotalMinutes
let effortToWriteTests = Math.Max(2, // Minimum 2 minutes per method not tested
effortToDevelopInMinutes *
uncoverageFactor *
complexityFactor *
justMyCodeFactor *
abstractionUsageFactor *
visibilityFactor *
staticFieldUsageFactor).ToMinutes().ToDebt()
let annualCostToNotFix = Math.Max(2, // Minimum 2 minutes per method not tested
effortToDevelopInMinutes *
usageFactor *
uncoverageFactor *
justMyCodeFactor).ToMinutes().ToAnnualInterest()
select new {
elem = method,
debt = effortToWriteTests,
ai = annualCostToNotFix
// Uncomment the line below to tinker with various factors
// uncoverageFactor, complexityFactor , justMyCodeFactor , abstractionUsageFactor, visibilityFactor, staticFieldUsageFactor
}).ToArray() // enumerate once
// TESTABILITY: the columns Testability (0-100, higher = easier to unit test) and TestHurdle, to pick
// the quick wins first. Computed for the issues only, from lookups built once per query: it changes
// neither the debt, nor the annual interest, nor the order. Few 'let' on purpose: each one, even
// unused, costs at every record of the query.
//
// The hurdles: framework APIs that tie a unit test to infrastructure. HARD (a process, the network,
// a database, console key input): their callers up to 3 calls deep are hard to test. SOFT (the file
// system, HttpClient - fakeable through its message handler -, the environment, sleeps, console line
// input - fed through Console.SetIn -): direct callers only.
// A call through an interface stops the chain: an interface is a seam a test can fake.
// Members: the hurdle members of the type, all of them when empty. Value: 1 hard type, 2 hard member,
// 3 soft type, 4 soft member.
let hurdleApis = ThirdParty.Types.Join(new[] {
new { Type = "System.Diagnostics.Process", Hard = true, Members = "" },
new { Type = "System.Net.HttpListener", Hard = true, Members = "" },
new { Type = "System.Net.WebClient", Hard = true, Members = "" },
new { Type = "System.Net.WebRequest", Hard = true, Members = "" },
new { Type = "System.Net.HttpWebRequest", Hard = true, Members = "" },
new { Type = "System.Net.Sockets.Socket", Hard = true, Members = "" },
new { Type = "System.Net.Sockets.TcpClient", Hard = true, Members = "" },
new { Type = "System.Net.Sockets.TcpListener", Hard = true, Members = "" },
new { Type = "System.Net.Sockets.UdpClient", Hard = true, Members = "" },
new { Type = "System.Data.Common.DbConnection", Hard = true, Members = "" },
new { Type = "System.Data.Common.DbCommand", Hard = true, Members = "" },
new { Type = "System.Data.SqlClient.SqlConnection", Hard = true, Members = "" },
new { Type = "System.Data.SqlClient.SqlCommand", Hard = true, Members = "" },
new { Type = "Microsoft.Data.SqlClient.SqlConnection", Hard = true, Members = "" },
new { Type = "Microsoft.Data.SqlClient.SqlCommand", Hard = true, Members = "" },
new { Type = "System.Console", Hard = true, Members = " ReadKey " },
new { Type = "System.Console", Hard = false, Members = " ReadLine Read " },
new { Type = "System.IO.File", Hard = false, Members = "" },
new { Type = "System.IO.Directory", Hard = false, Members = "" },
new { Type = "System.IO.FileInfo", Hard = false, Members = "" },
new { Type = "System.IO.DirectoryInfo", Hard = false, Members = "" },
new { Type = "System.IO.FileStream", Hard = false, Members = "" },
new { Type = "System.Net.Http.HttpClient", Hard = false, Members = "" },
new { Type = "System.Threading.Thread", Hard = false, Members = " Sleep " },
new { Type = "System.Threading.Tasks.Task", Hard = false, Members = " Delay " },
new { Type = "System.Environment", Hard = false, Members = " GetEnvironmentVariable SetEnvironmentVariable Exit " } },
t => t.FullName, h => h.Type,
(t, h) => t.MethodsAndConstructors.Where(m => h.Members.Length == 0 || h.Members.Contains(" " + m.SimpleName + " "))
.Select(m => new { m, Value = (h.Hard ? 1 : 3) + (h.Members.Length == 0 ? 0 : 1) }))
.SelectMany(ms => ms)
.ToDictionary(x => x.m, x => x.Value)
// The hurdle depth of the methods calling them: 1 to 3 for the hard hurdles (0 = the API), then 11
// for a direct caller of a soft one; the hard depth wins.
let hurdleDepth = Enumerable.Range(1, 4).Aggregate(
hurdleApis.Where(api => api.Value <= 2).ToDictionary(api => api.Key, api => 0),
(depthOf, step) => depthOf
.Concat((step <= 3 ? depthOf.Where(e => e.Value == step - 1).SelectMany(e => e.Key.MethodsCallingMe)
: hurdleApis.Where(api => api.Value >= 3).SelectMany(api => api.Key.MethodsCallingMe))
.Where(m => !depthOf.ContainsKey(m)).Distinct()
.Select(m => new KeyValuePair<IMethod, int>(m, step <= 3 ? step : 11)))
.ToDictionary(e => e.Key, e => e.Value))
// Testability of a method: simpler, no hurdle, calling few methods, callable by tests (public, or
// internal in an assembly with InternalsVisibleTo), in a type tests already reach, static without
// mutable state = easier. The weights were calibrated on 173 test-writing outcomes of an AI agent
// (methods it covered vs. methods it declared integration-only): fewer calls out is the strongest sign.
let testabilityOf = new Func<HashSet<IAssembly>, Func<IMethod, int>>(assembliesOpenToTests => m =>
(int)Math.Round(100 * Math.Min(1.0,
1 / (1.0 + 0.03 * ((m.CyclomaticComplexity ?? m.ILCyclomaticComplexity ?? 0) + (m.ILNestingDepth ?? 0))) *
1 / (1.0 + 0.05 * (m.NbMethodsCalled ?? 0)) *
(!hurdleDepth.ContainsKey(m) ? 1 : hurdleDepth[m] == 1 ? 0.15 : hurdleDepth[m] == 2 ? 0.5 : hurdleDepth[m] == 3 ? 0.7 : 0.8) *
(m.IsPubliclyVisible ||
(m.Visibility.EqualsAny(Visibility.Public, Visibility.Internal, Visibility.ProtectedOrInternal) &&
m.ParentType.Visibility.EqualsAny(Visibility.Public, Visibility.Internal, Visibility.ProtectedOrInternal) &&
assembliesOpenToTests.Contains(m.ParentAssembly) &&
m.ParentType.OutterTypes.All(t => t.Visibility.EqualsAny(Visibility.Public, Visibility.Internal, Visibility.ProtectedOrInternal)))
? 1 : 0.8) *
(m.ParentType.PercentageCoverage > 0 ? 1 : 0.75) *
(m.IsStatic && !m.ReadsMutableTypeState ? 1.1 : 1))))
(Application.Assemblies.Where(a => a.HasAttribute("System.Runtime.CompilerServices.InternalsVisibleToAttribute".AllowNoMatch())).ToHashSetEx())
// The hurdle as text: the API reached and, deeper than 1, the first call of the chain.
let hurdleText = new Func<Func<IMethod, int, IMethod>, Func<IMethod, string>, Func<IMethod, string>>((calleeAt, apiText) => m =>
!hurdleDepth.ContainsKey(m) ? "" :
hurdleDepth[m] == 1 ? apiText(calleeAt(m, 0)) :
hurdleDepth[m] == 2 ? apiText(calleeAt(calleeAt(m, 1), 0)) + " via " + calleeAt(m, 1).ParentType.Name + "." + calleeAt(m, 1).SimpleName + "() (2 calls deep)" :
hurdleDepth[m] == 3 ? apiText(calleeAt(calleeAt(calleeAt(m, 2), 1), 0)) + " via " + calleeAt(m, 2).ParentType.Name + "." + calleeAt(m, 2).SimpleName + "() (3 calls deep)" :
apiText(m.MethodsCalled.FirstOrDefault(c => hurdleApis.ContainsKey(c) && hurdleApis[c] >= 3)))
// the callee of m at that hurdle depth
((m, depth) => m == null ? null : m.MethodsCalled.FirstOrDefault(c => hurdleDepth.ContainsKey(c) && hurdleDepth[c] == depth),
// a hurdle type in full (any of its members), a hurdle member with its name
api => api == null ? "" : hurdleApis[api] == 1 || hurdleApis[api] == 3 ? api.ParentType.FullName : api.ParentType.FullName + "." + api.SimpleName + "()")
// One issue per method, also in a type no test reaches yet: the first tests of a type fix issues,
// they never turn one type issue into one issue per method still uncovered.
from x in methodsUntested
orderby x.ai.Value descending
select new {
x.elem,
x.elem.PercentageCoverage,
x.elem.NbLinesOfCode,
x.elem.NbLinesOfCodeNotCovered,
x.elem.CyclomaticComplexity,
Debt = x.debt,
AnnualInterest = x.ai,
Testability = testabilityOf(x.elem),
TestHurdle = hurdleText(x.elem)
}
//<Expl>The *{0}* is {1}% covered by tests. {3} statement{3?:s} {3?is:are} not executed at test-time.</Expl>
//<Desc>
// Lists the methods not covered or partially covered by tests, one issue
// per method — also in a type no test reaches yet: writing the first tests
// of a type fixes issues, it never turns one issue into several.
//
// The **technical debt** (effort to write tests) is the effort to develop
// the code element multiplied by factors in ]0,1.3] based on:
//
// - the method code size and complexity
//
// - the actual percentage coverage
//
// - the abstractness of types used (classes are harder to test than
// interfaces)
//
// - the method visibility (private/protected harder to test)
//
// - reads of mutable static fields (state shared across tests executions)
//
// - *JustMyCode* or not (*NotMyCode* is often generated, with generatable
// tests)
//
// This rule is a large debt source: untested code is by definition part of
// the technical debt. The **annual interest** (annual cost of leaving code
// uncovered) is based on the effort to develop times usage factors.
//
// The **Testability** column (0 to 100, higher = easier to unit test) and
// the **TestHurdle** column help to pick the quick wins first. Testability
// decreases with the number of methods called (the strongest sign), with a
// hurdle reached (*hard*: a process, the network, a database or console
// key input, up to 3 calls deep; *soft*: the file system, HttpClient, the
// environment, a sleep or console line input, called directly), with the
// method complexity, when tests cannot call the method (private, or
// internal without InternalsVisibleTo) and when no test reaches its type
// yet; it increases for a static method without mutable state. TestHurdle
// names the hurdle reached and, deeper than one call, the first call of
// the chain.
//</Desc>
//<HowToFix>
// Write unit tests covering the matched methods.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
New Types and Methods should be tested - Code Coverage
Runs only in diff mode (baseline defined) and when coverage data is imported, on types and methods added since baseline.
Covered code is maintainable and non-error-prone. Since stopping development to test a poorly covered code base is impracticable, each time a method or type gets added, write associated unit-tests. Maintainability increases, refactoring becomes driven by testability, and the overall design improves.
Severity is High: these issues reflect an actual trend of not writing tests for new code.
How to Fix:
Write unit-tests covering most methods and types added since the baseline.
// <Name>New Types and Methods should be tested</Name>
// <Id>ND1601:NewMethodsShouldBeTested</Id>
warnif count > 0
// Gather typesMoved, types whose parent namespaces changed since baseline
let typesRemoved = context.CompareContext.OlderCodeBase.Application.Types
.Where(t => t.WasRemoved()).Select(t => t.Name).ToHashSetEx()
let typesMoved = Application.Types
.Where(t => t.WasAdded() && typesRemoved.Contains(t.Name)).ToHashSetEx()
// Gather methodsMoved keys: a method removed from a type of the baseline and added, with the
// same name (signature embedded), lines of code and IL size, to another type is relocated
// code (a class split into helpers): its coverage debt pre-dates the baseline, it is not new.
let methodsRemovedKeys = context.CompareContext.OlderCodeBase.Application.Methods
.Where(m => m.WasRemoved() && m.NbILInstructions >= 10)
.Select(m => m.Name + "|" + m.NbLinesOfCode.ToString() + "|" + m.NbILInstructions.ToString()).ToHashSetEx()
let newMethodsUntested = (from m in JustMyCode.Methods where
m.NbLinesOfCode > 0 &&
m.PercentageCoverage < 30 &&
m.WasAdded() &&
// Don't match methods considered as new because its parameters changed
(!m.ParentType.IsPresentInBothBuilds() ||
!m.ParentType.OlderVersion().MethodsAndConstructors.Any(m1 => m1.WasRemoved() && m1.SimpleNameEquals(m.SimpleName))) &&
// Don't match methods in types moved, nor methods relocated to another type
!typesMoved.Contains(m.ParentType) &&
!methodsRemovedKeys.Contains(m.Name + "|" + m.NbLinesOfCode.ToString() + "|" + m.NbILInstructions.ToString())
select new {
elem = (ICodeContainer)m,
// Simplistic Debt estimation, because the effort to write tests for a method not 100% tested
// is already estimated properly with the rule "Code should be tested".
debt = m.NbLinesOfCodeNotCovered.Linear(1,2, 10,10).ToMinutes().ToDebt(),
}).ToArray() // enumerate once
// newMethodsUntestedInNewTypeTested is all new methods not 100% covered with parent type more than 0% covered
let newMethodsUntestedInNewTypeTested= from x in newMethodsUntested where
x.elem.AsMethod.ParentType.PercentageCoverage > 0 ||
!x.elem.AsMethod.ParentType.WasAdded()
select x
// newTypesUntested contains all type 0% covered whose methods are in methodsUntested
let newTypesUntested = newMethodsUntested
.Where(x => x.elem.AsMethod.ParentType.PercentageCoverage == 0 &&
x.elem.AsMethod.ParentType.WasAdded())
.ToLookup(x => x.elem.AsMethod.ParentType)
.Select(@group =>
new {
elem = (ICodeContainer)@group.Key,
debt = @group.Sum(m => m.debt) })
//from x in newMethodsUntested // use this clause to list all methods instead of types & methods
from x in newMethodsUntestedInNewTypeTested.Concat(newTypesUntested)
orderby x.elem.NbLinesOfCode descending,
x.elem.NbLinesOfCodeNotCovered ,
x.elem.PercentageCoverage
select new {
x.elem,
x.elem.PercentageCoverage,
x.elem.NbLinesOfCode,
x.elem.NbLinesOfCodeNotCovered,
CyclomaticComplexity = x.elem.IsMethod ? x.elem.AsMethod.CyclomaticComplexity : x.elem.AsType.CyclomaticComplexity,
Debt = x.debt,
Severity = Severity.High
}
//<Expl>The new *{0}* is {1}% covered by tests.
// It is important to direct the testing endeavor primarily towards newly developed code.</Expl>
//<Desc>
// Runs only in *diff mode* (baseline defined) and when coverage data is
// imported, on types and methods added since baseline.
//
// Covered code is *maintainable* and *non-error-prone*. Since stopping
// development to test a poorly covered code base is impracticable, each time
// a method or type gets added, write associated unit-tests. Maintainability
// increases, refactoring becomes driven by testability, and the overall
// design improves.
//
// Severity is **High**: these issues reflect an actual trend of not writing
// tests for new code.
//</Desc>
//<HowToFix>
// Write unit-tests covering most methods and types added since the baseline.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Methods refactored should be tested - Code Coverage
Runs only in diff mode (baseline defined) and when coverage data is imported, on methods refactored since baseline.
Covered code is maintainable and non-error-prone. Since stopping development to test a poorly covered code base is impracticable, each time a method or type gets refactored, write associated unit-tests. Maintainability increases, refactoring becomes driven by testability, and the overall design improves.
Severity is High: these issues reflect an actual trend of not writing tests for refactored code.
How to Fix:
Write unit-tests covering most refactored methods and classes.
// <Name>Methods refactored should be tested</Name>
// <Id>ND1602:MethodsRefactoredShouldBeTested</Id>
warnif count > 0
from m in JustMyCode.Methods where
m.PercentageCoverage < 30 &&
m.CodeWasChanged() &&
!m.IsClassConstructor &&
// Make sure the source file really changed, and it is not just a IL change
m.SourceFileDeclAvailable &&
context.CompareContext.WasChanged(m.SourceDecls.First().SourceFile.FilePath)
// A change that keeps the method's shape — same lines of code, same IL size, same
// cyclomatic complexity — is the ripple of a rename, a namespace move or a retyped
// dependency (a type reference or a call/callvirt swap in the IL), not a refactoring
// of this method: nothing new to test here.
let older = m.OlderVersion()
where older == null ||
m.NbLinesOfCode != older.NbLinesOfCode ||
m.NbILInstructions != older.NbILInstructions ||
m.CyclomaticComplexity != older.CyclomaticComplexity
orderby m.NbLinesOfCode descending,
m.NbLinesOfCodeNotCovered ,
m.PercentageCoverage
select new {
m,
m.PercentageCoverage,
m.NbLinesOfCode,
m.NbLinesOfCodeNotCovered,
// Simplistic Debt estimation, because the effort to write tests for a method not 100% tested
// is already estimated properly with the rule "Code should be tested".
Debt = m.NbLinesOfCodeNotCovered.Linear(1,2, 10,10).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>The refactored *{0}* is {1}% covered by tests.
// When engaging in code refactoring, it is important to write tests to safeguard against the introduction of regression bugs.</Expl>
//<Desc>
// Runs only in *diff mode* (baseline defined) and when coverage data is
// imported, on methods refactored since baseline.
//
// Covered code is *maintainable* and *non-error-prone*. Since stopping
// development to test a poorly covered code base is impracticable, each time
// a method or type gets refactored, write associated unit-tests.
// Maintainability increases, refactoring becomes driven by testability, and
// the overall design improves.
//
// Severity is **High**: these issues reflect an actual trend of not writing
// tests for refactored code.
//</Desc>
//<HowToFix>
// Write unit-tests covering most refactored methods and classes.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Assemblies and Namespaces should be tested - Code Coverage
Lists assemblies and namespaces not covered at all by unit tests (a matched assembly's children namespaces are not matched).
Its goal is to inform of large untested code elements — not to collide with Code should be tested, which details uncovered code, Debt and Annual Interest per method. Hence Debt is only 4 minutes per issue and severity is Low.
How to Fix:
Write unit and integration tests to cover, even partially, the matched elements. Then use Code should be tested, New Types and Methods should be tested and Methods refactored should be tested to write tests where they matter most, refactoring for testability if needed.
// <Name>Assemblies and Namespaces should be tested</Name>
// <Id>ND1603:AssembliesNamespacesAndTypesShouldBeTested</Id>
warnif count > 0
from elem in JustMyCode.Assemblies.Concat(JustMyCode.Namespaces.Cast<ICodeElementParent>())
where elem.NbLinesOfCode > 0 &&
elem.PercentageCoverage == 0 &&
elem.Parent.PercentageCoverage != 0
orderby elem.NbLinesOfCode descending
select new {
elem,
elem.NbLinesOfCodeNotCovered,
Debt = 4.ToMinutes().ToDebt(),
Severity = Severity.Low
}
//<Expl>
//None of the {1} statement{1?:s} of the *{0}* {1?is:are} covered by a test.
//This issue will be considered as fixed when a first test will be written.
//Issues of the rule *Code should be tested* provide the details of statements to cover for each method.
//</Expl>
//<Desc>
// Lists assemblies and namespaces not covered at all by unit tests (a
// matched assembly's children namespaces are not matched).
//
// Its goal is to inform of large untested code elements — not to collide
// with **Code should be tested**, which details uncovered code, *Debt* and
// *Annual Interest* per method. Hence *Debt* is only 4 minutes per issue and
// severity is *Low*.
//</Desc>
//
//<HowToFix>
// Write unit and integration tests to cover, even partially, the matched
// elements. Then use **Code should be tested**, **New Types and Methods
// should be tested** and **Methods refactored should be tested** to write
// tests where they matter most, refactoring for testability if needed.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Types almost 100% tested should be 100% tested - Code Coverage
Runs only when coverage data is imported.
Covering the last few percents of a class is as hard as the first 90%, so teams often settle for 90%. But untestable code usually means poorly written, error prone code: worth covering because most tricky bugs might come from this small portion of hard-to-test code. Not all classes need 100% coverage (e.g. UI), but most of the application logic should live in easy-to-test classes 100% covered.
Severity is High: such situation is bug-prone.
How to Fix:
Write more unit-tests for uncovered code. Hard-to-test code signals a design needing refactoring.
// <Name>Types almost 100% tested should be 100% tested</Name>
// <Id>ND1604:TypesAlmost100PercentTestedShouldBe100PercentTested</Id>
warnif count > 0
from t in JustMyCode.Types where
t.PercentageCoverage >= 95 &&
t.PercentageCoverage <= 99 &&
!t.IsGeneratedByCompiler
let methodsCulprit = t.Methods.Where(m => m.PercentageCoverage < 100)
orderby t.NbLinesOfCode descending ,
t.NbLinesOfCodeNotCovered ,
t.PercentageCoverage
select new {
t,
t.PercentageCoverage,
t.NbLinesOfCode,
t.NbLinesOfCodeNotCovered,
methodsCulprit,
// Simplistic Debt estimation, because the effort to write tests for a type not 100% tested
// is already estimated properly with the rule "Code should be tested".
Debt = t.NbLinesOfCodeNotCovered.Linear(1,2, 20,20).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* is {1}% covered by test. There are only {3} statement{3?:s} on {2} that remain untested.
// {3?This:These few} *hard-to-test* statement{3?:s} may indicate *poor design*
// and potential *error-prone* behavior.
// Write test and achieve 100% coverage helps ensure that rules
// *Types that used to be 100% covered by tests should still be 100% covered*
// can identify any coverage regressions that could potentially introduce bugs.
//</Expl>
//<Desc>
// Runs only when coverage data is imported.
//
// Covering the last few percents of a class is as hard as the first 90%, so
// teams often settle for 90%. But *untestable code* usually means *poorly
// written*, *error prone* code: worth covering **because most tricky bugs
// might come from this small portion of hard-to-test code**. Not all classes
// need 100% coverage (e.g. UI), but most of the application logic should
// live in *easy-to-test classes* 100% covered.
//
// Severity is **High**: such situation is *bug-prone*.
//</Desc>
//<HowToFix>
// Write more unit-tests for uncovered code. *Hard-to-test code* signals a
// design needing refactoring.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Namespaces almost 100% tested should be 100% tested - Code Coverage
Runs only when coverage data is imported.
Covering the last few percents of a namespace's classes is as hard as the first 90%, so teams often settle for 90%. But untestable code usually means poorly written, error prone code: worth covering because most tricky bugs might come from this small portion of hard-to-test code. Not all classes need 100% coverage (e.g. UI), but most of the application logic should live in easy-to-test classes 100% covered.
Severity is High: such situation is bug-prone.
How to Fix:
Write more unit-tests for uncovered code in the namespace. Hard-to-test code signals a design needing refactoring.
// <Name>Namespaces almost 100% tested should be 100% tested</Name>
// <Id>ND1605:NamespacesAlmost100PercentTestedShouldBe100PercentTested</Id>
warnif count > 0
from n in JustMyCode.Namespaces where
n.PercentageCoverage >= 95 &&
n.PercentageCoverage <= 99
let methodsCulprit = n.ChildMethods.Where(m => m.PercentageCoverage < 100)
orderby n.NbLinesOfCode descending ,
n.NbLinesOfCodeNotCovered ,
n.PercentageCoverage
select new {
n,
n.PercentageCoverage,
n.NbLinesOfCode,
n.NbLinesOfCodeNotCovered,
methodsCulprit,
// Simplistic Debt estimation, because the effort to write tests for a type not 100% tested
// is already estimated properly with the rule "Code should be tested".
Debt = n.NbLinesOfCodeNotCovered.Linear(1,2, 50,60).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* is {1}% covered by test. There are only {3} statement{3?:s} on {2} that remain untested.
// {3?This:These few} *hard-to-test* statement{3?:s} may indicate *poor design*
// and potential *error-prone* behavior.
// Write test and achieve 100% coverage helps ensure that rules
// *Types that used to be 100% covered by tests should still be 100% covered*
// can identify any coverage regressions that could potentially introduce bugs.
//</Expl>
//<Desc>
// Runs only when coverage data is imported.
//
// Covering the last few percents of a namespace's classes is as hard as the
// first 90%, so teams often settle for 90%. But *untestable code* usually
// means *poorly written*, *error prone* code: worth covering **because most
// tricky bugs might come from this small portion of hard-to-test code**. Not
// all classes need 100% coverage (e.g. UI), but most of the application
// logic should live in *easy-to-test classes* 100% covered.
//
// Severity is **High**: such situation is *bug-prone*.
//</Desc>
//<HowToFix>
// Write more unit-tests for uncovered code in the namespace. *Hard-to-test
// code* signals a design needing refactoring.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Types that used to be 100% covered by tests should still be 100% covered - Code Coverage
Runs only in diff mode (baseline defined) and when coverage data is imported. Warns when a type fully covered by tests in the baseline is now only partially covered.
The last ~10% of a class is as hard to cover as the first 90%, but untestable code usually means poorly written, error prone code: most tricky bugs might come from this small portion of hard-to-test code. Most application logic should live in easy-to-test classes 100% covered.
Severity is High: a type that lost 100% coverage is a bug-prone situation to handle carefully.
How to Fix:
Write more unit-tests for code no longer covered. Hard-to-test code signals a design needing refactoring. For code impossible to cover (like MessageBox.Show()), define an infrastructure to mock it at test-time.
// <Name>Types that used to be 100% covered by tests should still be 100% covered</Name>
// <Id>ND1606:TypesThatUsedToBe100PercentCoveredByTestsShouldStillBe100PercentCovered</Id>
warnif count > 0
from t in JustMyCode.Types where
t.IsPresentInBothBuilds() &&
t.OlderVersion().PercentageCoverage == 100 &&
t.PercentageCoverage < 100
from m in t .MethodsAndConstructors where
m.NbLinesOfCode> 0 &&
m.PercentageCoverage < 100 &&
!m.IsExcludedFromCoverage
select new {
m,
m.PercentageCoverage,
// Simplistic Debt estimation, because the effort to write tests for a method not 100% tested
// is already estimated properly with the rule "Code should be tested".
Debt = t.NbLinesOfCodeNotCovered.Linear(1,2, 10,10).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* was previously 100% covered by tests in the baseline, but now it stands at {1}% coverage.
// It is important to create new tests to potentially detect any regressions in coverage that might introduce bugs.
//</Expl>
//<Desc>
// Runs only in *diff mode* (baseline defined) and when coverage data is
// imported. Warns when a type fully covered by tests in the baseline is now
// only partially covered.
//
// The last ~10% of a class is as hard to cover as the first 90%, but
// *untestable code* usually means *poorly written*, *error prone* code:
// **most tricky bugs might come from this small portion of hard-to-test
// code**. Most application logic should live in *easy-to-test classes* 100%
// covered.
//
// Severity is **High**: a type that lost 100% coverage is a bug-prone
// situation to handle carefully.
//</Desc>
//<HowToFix>
// Write more unit-tests for code no longer covered. *Hard-to-test code*
// signals a design needing refactoring. For code impossible to cover (like
// *MessageBox.Show()*), define an infrastructure to *mock* it at test-time.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Types tagged with FullCoveredAttribute should be 100% covered - Code Coverage
Lists partially covered methods of types tagged FullCoveredAttribute. Runs only when coverage data is imported.
FullCoveredAttribute expresses in source code the intention that a class is and should remain 100% covered — an intention continuously checked by this rule. To avoid linking NDepend.API.dll, use your own attribute and adapt the rule.
The last ~10% of a class is as hard to cover as the first 90%, but untestable code usually means poorly written, error prone code: most tricky bugs might come from this small portion of hard-to-test code.
Severity is High: a type that lost 100% coverage is a bug-prone situation to handle carefully.
How to Fix:
Write more unit-tests for uncovered code of matched classes. Hard-to-test code signals a design needing refactoring.
// <Name>Types tagged with FullCoveredAttribute should be 100% covered</Name>
// <Id>ND1607:TypesTaggedWithFullCoveredAttributeShouldBe100PercentCovered</Id>
warnif count > 0
let covAttrs = Types.WithName("FullCoveredAttribute").Where(t => t.IsAttributeClass).ToArray()
where covAttrs.Any()
from t in JustMyCode.Types where
t.AttributeClassesThatTagMe.Intersect(covAttrs).Any() &&
t.PercentageCoverage < 100
from m in t.MethodsAndConstructors where
m.NbLinesOfCode> 0 &&
m.PercentageCoverage < 100 &&
!m.IsExcludedFromCoverage
select new {
m,
m.PercentageCoverage,
m.NbLinesOfCodeNotCovered,
m.NbLinesOfCode,
// Simplistic Debt estimation, because the effort to write tests for a method not 100% tested
// is already estimated properly with the rule "Code should be tested".
Debt = m.NbLinesOfCodeNotCovered.Linear(1,2, 10,10).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* is tagged with *FullCoveredAttribute*, yet its current coverage ratio stands at {1}%.
// This attribute indicates a commitment to keep this type fully tested.
// It is important to craft additional tests that align with this commitment.
//</Expl>
//<Desc>
// Lists partially covered methods of types tagged **FullCoveredAttribute**.
// Runs only when coverage data is imported.
//
// **FullCoveredAttribute** expresses in source code the intention that a
// class is and should remain 100% covered — an intention continuously
// checked by this rule. To avoid linking *NDepend.API.dll*, use your own
// attribute and adapt the rule.
//
// The last ~10% of a class is as hard to cover as the first 90%, but
// *untestable code* usually means *poorly written*, *error prone* code:
// **most tricky bugs might come from this small portion of hard-to-test
// code**.
//
// Severity is **High**: a type that lost 100% coverage is a bug-prone
// situation to handle carefully.
//</Desc>
//<HowToFix>
// Write more unit-tests for uncovered code of matched classes.
// *Hard-to-test code* signals a design needing refactoring.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Types 100% covered should be tagged with FullCoveredAttribute - Code Coverage
Runs only when coverage data is imported.
An attribute class named FullCoveredAttribute expresses in source code the intention that a class is and should remain 100% covered — an intention continuously checked by the rule Types tagged with FullCoveredAttribute should be 100% covered.
Severity is Low: an advice for improvement, not a problem.
How to Fix:
Tag 100% covered types with the FullCoveredAttribute class from NDepend.API.dll, or your own attribute with this name in any namespace.
// <Name>Types 100% covered should be tagged with FullCoveredAttribute</Name>
// <Id>ND1608:Types100PercentCoveredShouldBeTaggedWithFullCoveredAttribute</Id>
warnif count > 0
let covAttrs = Types.WithName("FullCoveredAttribute").Where(t => t.IsAttributeClass).ToArray()
from t in JustMyCode.Types where
t.PercentageCoverage == 100 &&
t.NbILInstructions > 0 &&
!t.IsGeneratedByCompiler &&
!t.AttributeClassesThatTagMe.Intersect(covAttrs).Any()
select new {
t,
t.NbLinesOfCode,
Debt = 3.ToMinutes().ToDebt(), // It is fast to add such attribute to a type.
Severity = Severity.Low
}
//<Expl>
// The *{0}* is 100% covered by tests, great job!
// Create an attribute class named *FullCoveredAttribute*
// and tag this type with it.
// This way the intent to keep this type 100% covered is clearly
// expressed and regularly checked through the rule
// *Types tagged with FullCoveredAttribute should be 100% covered*.
// This attribute will motivate developers that will refactor this type
// to write tests to exercise their changes.
//</Expl>
//<Desc>
// Runs only when coverage data is imported.
//
// An attribute class named **FullCoveredAttribute** expresses in source code
// the intention that a class is and should remain 100% covered — an
// intention continuously checked by the rule *Types tagged with
// FullCoveredAttribute should be 100% covered*.
//
// Severity is **Low**: an advice for improvement, not a problem.
//</Desc>
//<HowToFix>
// Tag 100% covered types with the **FullCoveredAttribute** class from
// *NDepend.API.dll*, or your own attribute with this name in any namespace.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Methods should have a low C.R.A.P score - Code Coverage
Runs only when coverage data is imported.
Change Risk Analyzer and Predictor (CRAP) pinpoints code both complex and untested. First defined here:
http://www.artima.com/weblogs/viewpost.jsp?thread=215899
Formula:
CRAP(m) = CC(m)^2 * (1 – cov(m)/100)^3 + CC(m), where
CC(m) is the
cyclomatic complexity and
cov(m) the
percentage coverage.
Matched methods cumulate two
error prone smells: complexity (hard to maintain) and incomplete coverage (hard to refactor without regression). The threshold of 30 is the one suggested by the inventors; no amount of testing keeps methods with CC > 30 out of CRAP territory. Methods with fewer than 10 lines of code are not matched.
How to Fix:Refactor the complex logic into smaller, less complex methods (possibly in new types) and write unit-tests to fully cover it. For code impossible to cover (like
MessageBox.Show()), define an infrastructure to
mock it at test-time.
// <Name>Methods should have a low C.R.A.P score</Name>
// <Id>ND1609:MethodsShouldHaveALowCRAPScore</Id>
warnif count > 0
from m in JustMyCode.Methods
// Don't match too short methods
where m.NbLinesOfCode > 10 && m.CoverageDataAvailable && m.CyclomaticComplexity != null
let CC = m.CyclomaticComplexity
let uncov = (100 - m.PercentageCoverage) / 100f
let CRAP = (double)(CC * CC * uncov * uncov * uncov) + CC
where CRAP != null && CRAP > 60
orderby CRAP descending, m.NbLinesOfCode descending
select new {
m,
CRAP,
CC,
m.PercentageCoverage, m.NbLinesOfCode,
// CRAP score equals 30 => 10 minutes debt
// CRAP score equals 3000 => 3 hours to write tests
Debt = CRAP.Linear(30,10, 3000, 3*60).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* has a an insufficient CRAP score of *{1}*.
// The *Change Risk Analyzer and Predictor* code metric
// is specifically crafted to identify code plagued by both
// excessive complexity and a low testing coverage ratio.
//</Expl>
//<Desc>
// Runs only when coverage data is imported.
//
// **Change Risk Analyzer and Predictor** (CRAP) pinpoints code both complex
// and untested. First defined here:
// http://www.artima.com/weblogs/viewpost.jsp?thread=215899
//
// Formula: **CRAP(m) = CC(m)^2 * (1 – cov(m)/100)^3 + CC(m)**, where *CC(m)*
// is the *cyclomatic complexity* and *cov(m)* the *percentage coverage*.
//
// Matched methods cumulate two *error prone* smells: complexity (hard to
// maintain) and incomplete coverage (hard to refactor without regression).
// The threshold of 30 is the one suggested by the inventors; no amount of
// testing keeps methods with CC > 30 out of CRAP territory. Methods with
// fewer than 10 lines of code are not matched.
//</Desc>
//<HowToFix>
// Refactor the complex logic into smaller, less complex methods (possibly in
// new types) and write unit-tests to fully cover it. For code impossible to
// cover (like *MessageBox.Show()*), define an infrastructure to *mock* it at
// test-time.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Complex Methods should be 100% tested - Code Coverage
Detects methods with a low maintainability index and insufficient test coverage: hard to understand, error prone and risky to modify. Maintainability index definition:
https://www.ndepend.com/docs/code-metrics#MaintainabilityIndexHow to Fix:1. Break large or complex methods into smaller, focused units.
2. Improve readability: clear naming, consistent formatting, less nesting.
3. Write unit tests validating both common and edge cases.
4. Ensure refactored code is fully covered by tests before merging.
// <Name>Complex Methods should be 100% tested</Name>
// <Id>ND1610:ComplexMethodsShouldBeFullyTested</Id>
warnif count > 0
from m in JustMyCode.Methods
where m.NbLinesOfCode >= 30 && m.MaintainabilityIndex <= 40 &&
m.CoverageDataAvailable && m.PercentageCoverage < 100 &&
m.CyclomaticComplexity != null && m.CyclomaticComplexity >= 10
orderby m.MaintainabilityIndex ascending
select new {
m,
m.MaintainabilityIndex,
m.CyclomaticComplexity,
m.NbLinesOfCode,
m.PercentageCoverage,
Debt =
(((60 - m.MaintainabilityIndex)/25f)*(100 - m.PercentageCoverage))
.Linear(10, 6, 150, 120).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* has a low maintainability index *{1}* and is highly complex.
// Current test coverage is only *{4}%*.
// Refactor the code and add sufficient tests to ensure full coverage
// and improved maintainability.
//</Expl>
//<Desc>
// Detects methods with a low maintainability index and insufficient test
// coverage: hard to understand, error prone and risky to modify.
// Maintainability index definition:
// https://www.ndepend.com/docs/code-metrics#MaintainabilityIndex
//</Desc>
//<HowToFix>
// 1. Break large or complex methods into smaller, focused units.
//
// 2. Improve readability: clear naming, consistent formatting, less nesting.
//
// 3. Write unit tests validating both common and edge cases.
//
// 4. Ensure refactored code is fully covered by tests before merging.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Test methods should assert something - Code Coverage
Matches test methods (methods tagged with a test attribute like [Test], [Fact] or [TestMethod]) that call no assertion API, directly or through helper methods. Such a test covers code without checking what this code does: coverage figures overstate how well the code is tested.
Assertion APIs recognized: the Assert classes of NUnit, xUnit, MSTest and TUnit, FluentAssertions, Shouldly, NFluent, Verify, ApprovalTests, mock verifications (Moq Verify(), NSubstitute Received(), FakeItEasy MustHaveHappened()), BDD runners (SpecFlow, Reqnroll, LightBDD), Roslyn analyzer and code fix tests and any third-party method named Assert... or declared in a type named ...Assert.... Assert.Pass(), Assert.Ignore(), Assert.Inconclusive(), Assume, Warn and Debug.Assert() check nothing.
Not matched: tests whose returned value is checked by the test framework (NUnit ExpectedResult, FsCheck properties), tests tagged with [ExpectedException] and tests invoking a member dynamically (a call with a dynamic argument is bound at run-time and might assert).
Unlike other default rules, this rule reports issues on test code.
How to Fix:
Assert the observable outcome of the code exercised: returned values, state changes or expected interactions. A test meant to only check that no exception is thrown can say so, like with NUnit Assert.DoesNotThrow().
If your tests check through your own helpers that throw an exception instead of calling an assertion API, add these helpers to the list at the top of the rule source.
Debt is 5 minutes per test: understand what the test exercises and assert it.
// <Name>Test methods should assert something</Name>
// <Id>ND1611:TestMethodsShouldAssertSomething</Id>
warnif count > 0
// Assertion APIs: adapt this list to your own assertion library
let assertionApis = ThirdParty.Methods.Where(m =>
// Test frameworks: Assert, ClassicAssert, CollectionAssert, AssertionHelper...
// Pass(), Ignore(), Inconclusive(), Skip(), Assume and Warn check nothing.
m.ParentNamespace.Name.StartsWithAny("NUnit.Framework", "Xunit", "Microsoft.VisualStudio.TestTools.UnitTesting") ?
m.ParentType.SimpleName.Contains("Assert") &&
!m.SimpleName.StartsWithAny("Pass", "Ignore", "Inconclusive", "Skip") :
// Assertion and snapshot libraries, BDD runners (steps are bound at run-time)
m.ParentNamespace.Name.StartsWithAny(
"TUnit.Assertions", "FluentAssertions", "AwesomeAssertions", "Shouldly", "NFluent",
"Verify", "ApprovalTests", "Snapshooter", "TechTalk.SpecFlow", "Reqnroll", "LightBDD") ||
// Mock verifications, property-based checks, Roslyn analyzer and code fix tests
(m.ParentNamespace.Name.StartsWith("Moq") && m.SimpleName.StartsWithAny("Verify", "InSequence")) ||
(m.ParentNamespace.Name.StartsWith("NSubstitute") && m.SimpleName.StartsWithAny("Received", "DidNotReceive")) ||
(m.ParentNamespace.Name.StartsWith("FakeItEasy") && m.SimpleName.StartsWith("Must")) ||
(m.ParentNamespace.Name.StartsWith("FsCheck") && m.SimpleName.Contains("ThrowOnFailure")) ||
(m.ParentNamespace.Name.StartsWith("Microsoft.CodeAnalysis") && m.ParentNamespace.Name.Contains("Testing") &&
m.SimpleName.StartsWithAny("RunAsync", "Verify")) ||
// Any other assertion API: AssertEx.Equal(), Mock.Assert(), AssertWasCalled()...
// Debug.Assert() is removed from release builds.
((m.SimpleName.StartsWith("Assert") || m.ParentType.SimpleName.Contains("Assert")) &&
!m.ParentNamespace.Name.StartsWith("System")))
// Your own assertion helpers that throw instead of calling an assertion API:
// .Concat(Methods.Where(m => m.ParentType.FullName == "MyTests.Check"))
// A dynamic call like Assert.Equal(expected, (dynamic)x) is bound at run-time:
// a method invoking a member dynamically might assert.
let assertingDynamically = ThirdParty.Methods.Where(m =>
m.SimpleName == "InvokeMember" && m.ParentType.FullName == "Microsoft.CSharp.RuntimeBinder.Binder")
.SelectMany(m => m.MethodsCallingMe)
// Test code asserting directly, through helpers (lambdas and async bodies
// included) or through overrides: a base fixture calling an abstract
// CheckResult() implemented by derived fixtures.
let assertingMetric = assertionApis.SelectMany(api => api.MethodsCallingMe).Where(m => m.IsTestCode)
.Concat(assertingDynamically).Distinct()
.FillIterative(methods => methods.SelectMany(m => m.MethodsCallingMe.Concat(m.OverriddensBase)).Where(m => m.IsTestCode))
let asserting = assertingMetric.DefinitionDomain.ToHashSetEx()
from m in TestCode.Methods
where m.IsTestMethod && !m.IsAbstract && !asserting.Contains(m) &&
// The test framework checks the value returned: NUnit ExpectedResult, FsCheck properties...
m.ReturnType != null &&
m.ReturnType.FullNameEqualsAny("System.Void", "System.Threading.Tasks.Task", "System.Threading.Tasks.ValueTask") &&
// The exception expected is the assertion
!m.HasAttribute("Microsoft.VisualStudio.TestTools.UnitTesting.ExpectedExceptionAttribute".AllowNoMatch()) &&
!m.HasAttribute("NUnit.Framework.ExpectedExceptionAttribute".AllowNoMatch())
orderby m.NbLinesOfCode descending
select new {
m,
exercised = m.MethodsCalled.Where(mc => !mc.IsThirdParty && !mc.IsTestCode),
m.NbLinesOfCode,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The test method *{0}* calls no assertion API: it passes as long as no
// exception is thrown, whatever the code it exercises does.
//</Expl>
//<Desc>
// Matches **test methods** (methods tagged with a test attribute like
// *[Test]*, *[Fact]* or *[TestMethod]*) that call no assertion API, directly
// or through helper methods. Such a test covers code without checking what
// this code does: coverage figures overstate how well the code is tested.
//
// Assertion APIs recognized: the *Assert* classes of NUnit, xUnit, MSTest and
// TUnit, FluentAssertions, Shouldly, NFluent, Verify, ApprovalTests, mock
// verifications (Moq *Verify()*, NSubstitute *Received()*, FakeItEasy
// *MustHaveHappened()*), BDD runners (SpecFlow, Reqnroll, LightBDD), Roslyn
// analyzer and code fix tests and any third-party method named *Assert...*
// or declared in a type named *...Assert...*. *Assert.Pass()*, *Assert.Ignore()*, *Assert.Inconclusive()*,
// *Assume*, *Warn* and *Debug.Assert()* check nothing.
//
// Not matched: tests whose returned value is checked by the test framework
// (NUnit *ExpectedResult*, FsCheck properties), tests tagged with
// *[ExpectedException]* and tests invoking a member dynamically (a call
// with a *dynamic* argument is bound at run-time and might assert).
//
// Unlike other default rules, this rule reports issues on **test code**.
//</Desc>
//<HowToFix>
// Assert the observable outcome of the code exercised: returned values,
// state changes or expected interactions. A test meant to only check that
// no exception is thrown can say so, like with NUnit *Assert.DoesNotThrow()*.
//
// If your tests check through your own helpers that throw an exception
// instead of calling an assertion API, add these helpers to the list at the
// top of the rule source.
//
// **Debt** is 5 minutes per test: understand what the test exercises and
// assert it.
//</HowToFix>
./rules/assets/img/Info32x32.png
Test Methods - Code Coverage
Lists test methods: methods tagged with a test attribute of a supported test framework (xUnit, NUnit, MSTest, TUnit…) declared in test assemblies.
Test assemblies (assemblies referencing a test framework) belong to the TestCode view, disjoint from Application: rules don't report issues on test code (except Test methods should assert something), while dependencies between test and application code remain available, as in this query.
// <Name>Test Methods</Name>
from m in TestCode.Methods
where m.IsTestMethod
select new { m, m.ParentType, m.NbLinesOfCode }
//<Desc>
// Lists test methods: methods tagged with a test attribute of a supported
// test framework (xUnit, NUnit, MSTest, TUnit…) declared in test assemblies.
//
// Test assemblies (assemblies referencing a test framework) belong to the
// **TestCode** view, disjoint from *Application*: rules don't report issues
// on test code (except *Test methods should assert something*), while
// dependencies between test and application code remain available, as in
// this query.
//</Desc>
./rules/assets/img/Info32x32.png
Methods directly called by test Methods - Code Coverage
Lists all application methods directly called by test methods (methods of the TestCode view tagged with a test attribute). Not listed: overrides called through polymorphism, methods solely invoked through a delegate or through reflection.
// <Name>Methods directly called by test Methods</Name>
let testMethods = TestCode.Methods.Where(m => m.IsTestMethod).ToHashSetEx()
from m in Application.Methods.UsedByAny(testMethods)
select new { m ,
calledByTests = m.MethodsCallingMe.Intersect(testMethods),
// --- Uncomment this line if your project import some coverage data ---
// m.PercentageCoverage
}
//<Desc>
// Lists all application methods directly called by test methods (methods of
// the **TestCode** view tagged with a test attribute). Not listed: overrides
// called through polymorphism, methods solely invoked through a delegate or
// through reflection.
//</Desc>
./rules/assets/img/Info32x32.png
Methods directly and indirectly called by test Methods - Code Coverage
Lists all application methods directly or indirectly called by test methods (methods of the TestCode view tagged with a test attribute). The recursion yields the depthOfCalledByTests metric: 1 = directly called by a test, 2 = called by a method called by a test… Not listed: overrides called through polymorphism, methods solely invoked through a delegate or through reflection.
// <Name>Methods directly and indirectly called by test Methods</Name>
let testMethods = TestCode.Methods.Where(m => m.IsTestMethod)
let depthOfCalledByTest = Application.Methods.DepthOfIsUsedByAny(testMethods)
from pair in depthOfCalledByTest
where pair.Value > 0
orderby pair.Value ascending
select new {
method = pair.CodeElement,
// (depthOfCalledByTests == 1) means that the method is directly called by tests
// (depthOfCalledByTests == 2) means that the method is directly called by a method directly called by tests
// …
depthOfCalledByTests = pair.Value,
nbLinesOfCode = pair.CodeElement.NbLinesOfCode,
// --- Uncomment this line if your project import some coverage data ---
// m.PercentageCoverage
}
//<Desc>
// Lists all application methods *directly or indirectly* called by test
// methods (methods of the **TestCode** view tagged with a test attribute).
// The recursion yields the *depthOfCalledByTests* metric: *1* = directly
// called by a test, *2* = called by a method called by a test… Not listed:
// overrides called through polymorphism, methods solely invoked through a
// delegate or through reflection.
//</Desc>
./rules/assets/img/StatusBigOk32x32.png
Potentially Dead Types - Dead Code
Lists potentially dead types: removable because never used by the program. It matches types not used anywhere and types used only by such types — hence the TypesUsingMe column and the depth metric (0 = not used, 1 = used only by unused types, etc…).
Public types are not matched by default (client code not analyzed by NDepend might use them); easily changed. Entity Framework ModelSnapshot classes (used only by the EF infrastructure) are not matched.
Usages from test code (the TestCode view) keep a type alive: a type used only from tests is not matched here but by the rule Code used only by tests, disabled by default.
How to Fix:
Static analysis cannot list dead types exactly: types can be used dynamically (like through reflection). Investigate each match; if truly unused, remove it to avoid maintaining useless code, or comment it out with a note on future intentions.
If a used type is still matched, tag it with IsNotDeadCodeAttribute from NDepend.API.dll (or your own attribute, adapting the rule).
Debt is 15 minutes (quick investigation). Annual Interest is proportional to the type's #lines of code: the bigger, the more it slows down maintenance.
// <Name>Potentially Dead Types</Name>
// <Id>ND1700:PotentiallyDeadTypes</Id>
warnif count > 0
// Filter procedure for types that shouldn't be considered as dead
let canTypeBeConsideredAsDeadProc = new Func<IType, bool>(
t => !t.IsPublic && // Public types might be used by client applications of your assemblies.
t.Name != "Program" &&
!t.IsGeneratedByCompiler &&
!t.IsUsedFromAssemblyAttributes &&
// If you don't want to link NDepend.API.dll, you can use your own
// IsNotDeadCodeAttribute or UsedImplicitlyAttribute
// and adapt the source code of this rule.
!t.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) &&
!t.HasAttribute("JetBrains.Annotations.UsedImplicitlyAttribute".AllowNoMatch()) &&
!t.Methods.Any(m => m.HasAttribute("JetBrains.Annotations.UsedImplicitlyAttribute".AllowNoMatch())) &&
!t.Properties.Any(p => p.HasAttribute("JetBrains.Annotations.UsedImplicitlyAttribute".AllowNoMatch())) &&
// Exclude static types that define only const fields
// because they cannot be seen as used in IL code.
!(t.IsStatic && t.NbMethods == 0 && !t.Fields.Where(f => !f.IsLiteral).Any()) &&
// Entity Framework ModelSnapshot classes are used only by the EF infrastructure.
!t.DeriveFrom("Microsoft.EntityFrameworkCore.Infrastructure.ModelSnapshot".AllowNoMatch()) &&
// ASP.NET MVC Controller classes are used only by the infrastructure.
!t.SimpleName.EndsWith("Controller") &&
!t.DeriveFrom("Microsoft.AspNetCore.Mvc.Controller".AllowNoMatch()) &&
!t.DeriveFrom("Microsoft.AspNetCore.Mvc.ControllerBase".AllowNoMatch()) &&
!t.HasAttribute("Microsoft.AspNetCore.Mvc.ControllerAttribute".AllowNoMatch()) &&
// Types holding a module initializer are used by the runtime at assembly load
// (.NET 5+ [ModuleInitializer]): a registration hook and everything it reaches is alive.
!t.Methods.Any(m => m.HasAttribute("System.Runtime.CompilerServices.ModuleInitializerAttribute".AllowNoMatch())) &&
// TypeConverter classes are instantiated by the BCL from the [TypeConverter] attribute,
// often through its string form ("Ns.X, Asm") that IL analysis cannot see.
!t.DeriveFrom("System.ComponentModel.TypeConverter".AllowNoMatch()) &&
// Types used by the Unity engine
!t.HasAttribute("UnityEngine.RuntimeInitializeOnLoadAttribute".AllowNoMatch()) &&
!t.HasAttribute("UnityEngine.InitializeOnLoadAttribute".AllowNoMatch()) &&
!t.DeriveFrom("UnityEditor.AssetPostprocessor".AllowNoMatch()) &&
!(t.IsStatic && t.StaticMethods.Any(m => m.HasAttribute("UnityEditor.MenuItemAttribute".AllowNoMatch()))) &&
!t.Methods.Any(m => m.HasAttribute("UnityEngine.RuntimeInitializeOnLoadMethodAttribute".AllowNoMatch()) ||
m.HasAttribute("UnityEngine.InitializeOnLoadMethodAttribute".AllowNoMatch()))
)
// Select types unused (usages from test code count: a type used only from tests
// is not dead, see the rule 'Code used only by tests')
let typesUnused =
from t in JustMyCode.Types where
t.NbTypesUsingMe == 0 && canTypeBeConsideredAsDeadProc(t)
select t
// Dead types = types used only by unused types (recursive)
let deadTypesMetric = typesUnused.FillIterative(
types => // Unique loop, just to let a chance to build the hashset.
from o in (new object()).ToEnumerable()
let hashset = types.ToHashSetEx()
from t in codeBase.Application.Types.UsedByAny(types).Except(types)
where canTypeBeConsideredAsDeadProc(t) &&
// Select types used only by types already considered as dead
t.TypesUsingMe.All(tUser => hashset.Contains(tUser))
select t)
from t in deadTypesMetric.DefinitionDomain
let depth = deadTypesMetric[t]
// A type of depth > 0 is used, but only by types themselves dead: the explanation names them
let nbUsers = t.TypesUsingMe.Count()
let deadUsers = t.TypesUsingMe.Take(3).Select(tUser => "*" + tUser.Name + "*").Aggregate(", ") +
(nbUsers > 3 ? " and " + (nbUsers - 3).ToString() + " other" + (nbUsers > 4 ? "s" : "") : "")
select new {
t,
depth,
t.TypesUsingMe,
expl = depth == 0 ?
"is not used within the context of analyzed code and may be a candidate for removal" :
"is used only by the type" + (nbUsers > 1 ? "s " : " ") + deadUsers + ", potentially dead too, " +
"and may be a candidate for removal along with " + (nbUsers > 1 ? "them" : "it"),
Debt = 15.ToMinutes().ToDebt(),
AnnualInterest = (10 + (t.NbLinesOfCode ?? 1)).ToMinutes().ToAnnualInterest()
}
//<Expl>
// The *{0}* {3}.
//</Expl>
//<Desc>
// Lists *potentially* **dead types**: removable because never used by the
// program. It matches types not used anywhere and types used only by such
// types — hence the *TypesUsingMe* column and the *depth* metric (*0* = not
// used, *1* = used only by unused types, etc…).
//
// *Public* types are not matched by default (client code not analyzed by
// NDepend might use them); easily changed. Entity Framework ModelSnapshot
// classes (used only by the EF infrastructure) are not matched.
//
// Usages from **test code** (the *TestCode* view) keep a type alive: a type
// used only from tests is not matched here but by the rule *Code used only
// by tests*, disabled by default.
//</Desc>
//<HowToFix>
// *Static analysis* cannot list dead types *exactly*: types can be used
// *dynamically* (like through reflection). Investigate each match; if truly
// unused, remove it to avoid maintaining useless code, or comment it out
// with a note on future intentions.
//
// If a used type is still matched, tag it with **IsNotDeadCodeAttribute**
// from *NDepend.API.dll* (or your own attribute, adapting the rule).
//
// **Debt** is 15 minutes (quick investigation). **Annual Interest** is
// proportional to the type's #lines of code: the bigger, the more it slows
// down maintenance.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Potentially Dead Methods - Dead Code
Lists potentially dead methods: removable because never called by the program. It matches methods not called anywhere and methods called only by such methods — hence the MethodsCallingMe column and the depth metric (0 = not called, 1 = called only by uncalled methods, etc…).
Public methods are not matched by default (client code not analyzed by NDepend might call them); easily changed.
Calls from test code (the TestCode view) keep a method alive: a method called only from tests is not matched here but by the rule Code used only by tests, disabled by default.
How to Fix:
Static analysis cannot list dead methods exactly: methods can be invoked dynamically (like through reflection). Investigate each match; if truly never invoked, remove it to avoid maintaining useless code, or comment it out with a note on future intentions.
If an invoked method is still matched, tag it with IsNotDeadCodeAttribute from NDepend.API.dll (or your own attribute, adapting the rule).
Debt is 10 minutes (quick investigation) plus 3 minutes per unit of depth: dead methods only called by dead code take longer to investigate. Annual Interest is proportional to the method's #lines of code: the bigger, the more it slows down maintenance.
// <Name>Potentially Dead Methods</Name>
// <Id>ND1701:PotentiallyDeadMethods</Id>
warnif count > 0
// Filter procedure for methods that shouldn't be considered as dead
let canMethodBeConsideredAsDeadProc = new Func<IMethod, bool>(
m => !m.IsPubliclyVisible && // Public methods might be used by client applications of your assemblies.
!m.IsEntryPoint && // Main() method is not used by-design.
!m.IsExplicitInterfaceImpl && // The IL code never explicitly calls explicit interface methods implementation.
!m.IsClassConstructor && // The IL code never explicitly calls class constructors.
!m.IsFinalizer && // The IL code never explicitly calls finalizers.
!m.IsVirtual && // Only check for non virtual method that are not seen as used in IL.
!(m.IsConstructor && // Don't take account of protected ctor that might be call by a derived ctors.
m.IsProtected) &&
!m.IsEventAdder && // The IL code never explicitly calls events adder/remover.
!m.IsEventRemover &&
!m.IsGeneratedByCompiler &&
!m.ParentType.IsDelegate &&
// Don't consider Global ASP.NET methods as unused
!m.ParentType.DeriveFrom("System.Web.HttpApplication".AllowNoMatch()) &&
// Methods tagged with these attributes are called by the serialization infrastructure.
!m.HasAttribute("System.Runtime.Serialization.OnSerializingAttribute".AllowNoMatch()) &&
!m.HasAttribute("System.Runtime.Serialization.OnDeserializingAttribute".AllowNoMatch()) &&
!m.HasAttribute("System.Runtime.Serialization.OnSerializedAttribute".AllowNoMatch()) &&
!m.HasAttribute("System.Runtime.Serialization.OnDeserializedAttribute".AllowNoMatch()) &&
// If you don't want to link NDepend.API.dll, you can use your own
// IsNotDeadCodeAttribute or UsedImplicitlyAttribute
// and adapt the source code of this rule.
!m.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) &&
!m.HasAttribute("JetBrains.Annotations.UsedImplicitlyAttribute".AllowNoMatch()) &&
// Module initializers are called by the runtime at assembly load (.NET 5+),
// never from IL: a registration hook and everything it calls is alive.
!m.HasAttribute("System.Runtime.CompilerServices.ModuleInitializerAttribute".AllowNoMatch()) &&
// Methods called by the Unity engine
!m.HasAttribute("UnityEngine.RuntimeInitializeOnLoadMethodAttribute".AllowNoMatch()) &&
!m.HasAttribute("UnityEngine.InitializeOnLoadMethodAttribute".AllowNoMatch()) &&
!m.HasAttribute("UnityEditor.MenuItemAttribute".AllowNoMatch()) &&
// Don't consider public getters/setters
// of classes that implement INotifyPropertyChanged
// as dead code.
!(m.IsPublic && (m.IsPropertyGetter || m.IsPropertySetter) &&
m.ParentType.Implement("System.ComponentModel.INotifyPropertyChanged".AllowNoMatch())) &&
// ASP.NET MVC Controller classes and methods are used only by the infrastructure
// except methods tagged by NonAction.
!( (m.ParentType.SimpleName.EndsWith("Controller") ||
m.ParentType.DeriveFrom("Microsoft.AspNetCore.Mvc.Controller".AllowNoMatch()) ||
m.ParentType.DeriveFrom("Microsoft.AspNetCore.Mvc.ControllerBase".AllowNoMatch()) ||
m.ParentType.HasAttribute("Microsoft.AspNetCore.Mvc.ControllerAttribute".AllowNoMatch())) &&
!m.HasAttribute("System.Web.Mvc.NonActionAttribute".AllowNoMatch())) &&
// Methods of classes that derive from Unity classes are used implicitly by the Unity engine
!m.ParentType.BaseClasses.Any(bc => bc.ParentNamespace.Name.StartsWithAny("UnityEngine","UnityEditor")) &&
// Method might be called by expression in DebuggerDisplayAttribute
!m.ParentType.HasAttribute("System.Diagnostics.DebuggerDisplayAttribute".AllowNoMatch()) &&
// Instance ctor in class implementing interface(s) might be used through Dependency Injection
!(m.IsConstructor && (m.IsPublic || m.IsInternal) && !m.IsStatic &&
(m.ParentType.NbInterfacesImplemented > 0 ||
// The instance ctor is not used, and its parent type is used by a type
// that does not use any of its members.
// This certainly shows the service injection pattern.
m.ParentType.TypesUsingMe.Any(t => !t.Methods.Any(m1 => m1.MembersUsed.Any(m2 => m2.ParentType == m.ParentType)))
))
)
// Get methods unused (calls from test code count: a method called only from tests
// is not dead, see the rule 'Code used only by tests')
let methodsUnused =
from m in JustMyCode.Methods where
m.NbMethodsCallingMe == 0 &&
canMethodBeConsideredAsDeadProc(m)
select m
// Dead methods = methods used only by unused methods (recursive)
let deadMethodsMetric = methodsUnused.FillIterative(
methods => // Unique loop, just to let a chance to build the hashset.
from o in (new object()).ToEnumerable()
// Use a hashet to make Intersect calls much faster!
let hashset = methods.ToHashSetEx()
from m in codeBase.Application.Methods.UsedByAny(methods).Except(methods)
where canMethodBeConsideredAsDeadProc(m) &&
// Select methods called only by methods already considered as dead
m.MethodsCallingMe.All(mCaller => hashset.Contains(mCaller))
select m)
from m in JustMyCode.Methods.Intersect(deadMethodsMetric.DefinitionDomain)
let depth = deadMethodsMetric[m]
// A method of depth > 0 is called, but only by methods themselves dead: the explanation names them
let nbCallers = m.MethodsCallingMe.Count()
let deadCallers = m.MethodsCallingMe.Take(3).Select(mCaller => "*" + mCaller.ParentType.Name + "." + mCaller.Name + "*").Aggregate(", ") +
(nbCallers > 3 ? " and " + (nbCallers - 3).ToString() + " other" + (nbCallers > 4 ? "s" : "") : "")
select new {
m,
depth,
m.MethodsCallingMe,
expl = depth == 0 ?
"is not invoked within the context of analyzed code and may be a candidate for removal" :
"is called only by the method" + (nbCallers > 1 ? "s " : " ") + deadCallers + ", potentially dead too, " +
"and may be a candidate for removal along with " + (nbCallers > 1 ? "them" : "it"),
Debt = (10 + 3*depth).ToMinutes().ToDebt(),
AnnualInterest = (8 + (m.NbLinesOfCode ?? 1)).ToMinutes().ToAnnualInterest()
}
//<Expl>
// The *{0}* {3}.
//</Expl>
//<Desc>
// Lists *potentially* **dead methods**: removable because never called by
// the program. It matches methods not called anywhere and methods called
// only by such methods — hence the *MethodsCallingMe* column and the *depth*
// metric (*0* = not called, *1* = called only by uncalled methods, etc…).
//
// *Public* methods are not matched by default (client code not analyzed by
// NDepend might call them); easily changed.
//
// Calls from **test code** (the *TestCode* view) keep a method alive: a
// method called only from tests is not matched here but by the rule *Code
// used only by tests*, disabled by default.
//</Desc>
//<HowToFix>
// *Static analysis* cannot list dead methods *exactly*: methods can be
// invoked *dynamically* (like through reflection). Investigate each match;
// if truly never invoked, remove it to avoid maintaining useless code, or
// comment it out with a note on future intentions.
//
// If an invoked method is still matched, tag it with
// **IsNotDeadCodeAttribute** from *NDepend.API.dll* (or your own attribute,
// adapting the rule).
//
// **Debt** is 10 minutes (quick investigation) plus 3 minutes per unit of
// depth: dead methods only called by dead code take longer to investigate.
// **Annual Interest** is proportional to the method's #lines of code: the
// bigger, the more it slows down maintenance.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Potentially Dead Fields - Dead Code
Lists potentially dead fields: removable because never used by the program.
Public fields are not matched by default (client code not analyzed by NDepend might use them); easily changed — rules in the Visibility group warn about public fields. Enumeration values and literal constants are also not matched: their usage is lost in the compiled IL code NDepend analyzes.
Usages from test code (the TestCode view) keep a field alive: a field used only from tests is not matched here but by the rule Code used only by tests, disabled by default.
How to Fix:
Static analysis cannot list dead fields exactly: fields can be read or assigned dynamically (like through reflection). Investigate each match; if truly unused, remove it to avoid maintaining a useless code element.
If a used field is still matched, tag it with IsNotDeadCodeAttribute from NDepend.API.dll (or your own attribute, adapting the rule).
Debt is 10 minutes (quick investigation); Annual Interest is 8 minutes per unused field matched.
// <Name>Potentially Dead Fields</Name>
// <Id>ND1702:PotentiallyDeadFields</Id>
warnif count > 0
from f in JustMyCode.Fields where
// Usages from test code count: a field used only from tests is not dead, see the rule 'Code used only by tests'
f.NbMethodsUsingMe == 0 &&
!f.IsPublic && // Although not recommended, public fields might be used by client applications of your assemblies.
!f.IsLiteral && // The IL code never explicitly uses literal fields.
!f.IsEnumValue && // The IL code never explicitly uses enumeration value.
f.Name != "value__" && // Field named 'value__' are relative to enumerations and the IL code never explicitly uses them.
!f.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) &&
!f.HasAttribute("JetBrains.Annotations.UsedImplicitlyAttribute".AllowNoMatch()) &&
!f.IsGeneratedByCompiler &&
// If you don't want to link NDepend.API.dll, you can use your own IsNotDeadCodeAttribute
// and adapt the source code of this rule.
// Don't warn for fields tagged with these Unity specific attribute
!f.HasAttribute("UnityEngine.SerializeField".AllowNoMatch()) &&
!f.HasAttribute("UnityEngine.SerializeReference".AllowNoMatch()) &&
// Don't warn about InlineArrayAttribute's field
!f.ParentType.HasAttribute("System.Runtime.CompilerServices.InlineArrayAttribute".AllowNoMatch())
select new {
f,
Debt = 10.ToMinutes().ToDebt(),
AnnualInterest = 8.ToMinutes().ToAnnualInterest()
}
//<Expl>
// The *{0}* is not read not assigned within the context of analyzed code and may be a candidate for removal.
//</Expl>
//<Desc>
// Lists *potentially* **dead fields**: removable because never used by the
// program.
//
// *Public* fields are not matched by default (client code not analyzed by
// NDepend might use them); easily changed — rules in the *Visibility* group
// warn about public fields. Enumeration values and literal constants are
// also not matched: their usage is lost in the compiled IL code NDepend
// analyzes.
//
// Usages from **test code** (the *TestCode* view) keep a field alive: a
// field used only from tests is not matched here but by the rule *Code used
// only by tests*, disabled by default.
//</Desc>
//<HowToFix>
// *Static analysis* cannot list dead fields *exactly*: fields can be read or
// assigned *dynamically* (like through reflection). Investigate each match;
// if truly unused, remove it to avoid maintaining a useless code element.
//
// If a used field is still matched, tag it with **IsNotDeadCodeAttribute**
// from *NDepend.API.dll* (or your own attribute, adapting the rule).
//
// **Debt** is 10 minutes (quick investigation); **Annual Interest** is 8
// minutes per unused field matched.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Wrong usage of IsNotDeadCodeAttribute - Dead Code
NDepend.Attributes.IsNotDeadCodeAttribute (from NDepend.API.dll, replaceable by your own attribute) marks elements not used directly but used somehow (like through reflection), for the Potentially dead Types/Methods/Fields rules.
This rule matches elements tagged with this attribute that are nevertheless used directly somewhere in the code.
How to Fix:
Remove the now-useless IsNotDeadCodeAttribute tagging of matched elements.
// <Name>Wrong usage of IsNotDeadCodeAttribute</Name>
// <Id>ND1703:WrongUsageOfIsNotDeadCodeAttribute</Id>
warnif count > 0
let tAttr = Types.WithFullName("NDepend.Attributes.IsNotDeadCodeAttribute").FirstOrDefault()
where tAttr != null
// Get types that do a wrong usage of IsNotDeadCodeAttribute
let types = from t in Application.Types where
t.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) &&
( // types used don't need to be tagged with IsNotDeadCodeAttribute!
t.TypesUsingMe.Count(t1 =>
!t.NestedTypes.Contains(t1) &&
!t1.IsTestCode && // test users don't count: the attribute also exempts from the rule 'Code used only by tests'
!t1.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) ) > 0 ||
// Static types that define only const fields cannot be seen as used in IL code.
// They don't need to be tagged with IsNotDeadCodeAttribute.
(t.IsStatic && t.NbMethods == 0 && !t.Fields.Where(f => !f.IsLiteral).Any())
)
select t
// Get methods that do a wrong usage of IsNotDeadCodeAttribute
let methods = from m in Application.Methods where
m.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) &&
m.MethodsCallingMe.Count(m1 => !m1.IsTestCode && !m1.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch())) > 0
select m
// Get fields that do a wrong usage of IsNotDeadCodeAttribute
let fields = from f in Application.Fields where
f.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) &&
f.MethodsUsingMe.Count(m1 => !m1.IsTestCode && !m1.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch())) > 0
select f
from member in types.Cast<IMember>().Concat(methods).Concat(fields)
select new {
member,
Debt = 4.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* shouldn't be tagged with *IsNotDeadCodeAttribute* because it is used in the context of analyzed code.
//</Expl>
//<Desc>
// **NDepend.Attributes.IsNotDeadCodeAttribute** (from *NDepend.API.dll*,
// replaceable by your own attribute) marks elements not used directly but
// used somehow (like through reflection), for the *Potentially dead
// Types/Methods/Fields* rules.
//
// This rule matches elements tagged with this attribute that are
// nevertheless used directly somewhere in the code.
//</Desc>
//<HowToFix>
// Remove the now-useless *IsNotDeadCodeAttribute* tagging of matched
// elements.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Code used only by tests - Dead Code
Lists application types, methods and fields used only from test code (the TestCode view): the program itself never uses them, only tests keep them alive. The testUsers column lists these test code users.
Such code is either dead production code whose tests were left behind, or a test helper living in the application code.
When a type is matched, its members and nested types are not matched. Elements exempted by the Potentially Dead Types/Methods/Fields rules (public API, infrastructure entry points, IsNotDeadCodeAttribute…) are not matched either.
How to Fix:
If the element is dead production code, remove it together with the tests that exercise it.
If it is a test helper, move it into the test code: a method that only uses members visible from the tests can become an extension method there, so its call sites stay unchanged.
If it is a deliberate test hook, or is used in ways IL analysis cannot see, tag it with IsNotDeadCodeAttribute from NDepend.API.dll.
Debt is 25 minutes for a type, 20 for a method and 15 for a field: the tests must be updated too. Annual Interest is proportional to the #lines of code and divided by 4 (2 minutes for a field), since such code is still exercised by the tests.
// <Name>Code used only by tests</Name>
// <Id>ND1704:CodeUsedOnlyByTests</Id>
warnif count > 0
// Same exemptions as the rules Potentially Dead Types/Methods/Fields: an element
// they exempt (public API, infrastructure entry points, IsNotDeadCodeAttribute...)
// is used in ways IL analysis cannot see, not only from tests.
// Shared procedures: a CQLinq query cannot hold more than 32 attribute/base-class name predicates.
// If you don't want to link NDepend.API.dll, you can use your own
// IsNotDeadCodeAttribute or UsedImplicitlyAttribute
// and adapt the source code of this rule.
let hasExemptionAttributeProc = new Func<IAttributeTarget, bool>(
x => x.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) ||
x.HasAttribute("JetBrains.Annotations.UsedImplicitlyAttribute".AllowNoMatch()))
// ASP.NET MVC Controller classes and their methods are used by the infrastructure.
let isControllerProc = new Func<IType, bool>(
t => t.SimpleName.EndsWith("Controller") ||
t.DeriveFrom("Microsoft.AspNetCore.Mvc.Controller".AllowNoMatch()) ||
t.DeriveFrom("Microsoft.AspNetCore.Mvc.ControllerBase".AllowNoMatch()) ||
t.HasAttribute("Microsoft.AspNetCore.Mvc.ControllerAttribute".AllowNoMatch()))
// Methods called by the runtime (.NET 5+ module initializers) or by the Unity engine,
// never from IL: such a method and its parent type are alive.
let isCalledByRuntimeProc = new Func<IMethod, bool>(
m => m.HasAttribute("System.Runtime.CompilerServices.ModuleInitializerAttribute".AllowNoMatch()) ||
m.HasAttribute("UnityEngine.RuntimeInitializeOnLoadMethodAttribute".AllowNoMatch()) ||
m.HasAttribute("UnityEngine.InitializeOnLoadMethodAttribute".AllowNoMatch()) ||
m.HasAttribute("UnityEditor.MenuItemAttribute".AllowNoMatch()))
let canTypeBeMatchedProc = new Func<IType, bool>(
t => !t.IsPublic && // Public types might be used by client applications of your assemblies.
t.Name != "Program" &&
!t.IsGeneratedByCompiler &&
!t.IsUsedFromAssemblyAttributes &&
!hasExemptionAttributeProc(t) &&
!t.Methods.Any(m => hasExemptionAttributeProc(m) || isCalledByRuntimeProc(m)) &&
!t.Properties.Any(p => hasExemptionAttributeProc(p)) &&
// Exclude static types that define only const fields
// because they cannot be seen as used in IL code.
!(t.IsStatic && t.NbMethods == 0 && !t.Fields.Where(f => !f.IsLiteral).Any()) &&
// Entity Framework ModelSnapshot classes are used only by the EF infrastructure.
!t.DeriveFrom("Microsoft.EntityFrameworkCore.Infrastructure.ModelSnapshot".AllowNoMatch()) &&
!isControllerProc(t) &&
// TypeConverter classes are instantiated by the BCL from the [TypeConverter] attribute,
// often through its string form ("Ns.X, Asm") that IL analysis cannot see.
!t.DeriveFrom("System.ComponentModel.TypeConverter".AllowNoMatch()) &&
// Types used by the Unity engine
!t.HasAttribute("UnityEngine.RuntimeInitializeOnLoadAttribute".AllowNoMatch()) &&
!t.HasAttribute("UnityEngine.InitializeOnLoadAttribute".AllowNoMatch()) &&
!t.DeriveFrom("UnityEditor.AssetPostprocessor".AllowNoMatch())
)
let canMethodBeMatchedProc = new Func<IMethod, bool>(
m => !m.IsPubliclyVisible && // Public methods might be used by client applications of your assemblies.
!m.IsEntryPoint && // Main() method is not used by-design.
!m.IsExplicitInterfaceImpl && // The IL code never explicitly calls explicit interface methods implementation.
!m.IsClassConstructor && // The IL code never explicitly calls class constructors.
!m.IsFinalizer && // The IL code never explicitly calls finalizers.
!m.IsVirtual && // Only check for non virtual method that are not seen as used in IL.
!(m.IsConstructor && // Don't take account of protected ctor that might be call by a derived ctors.
m.IsProtected) &&
!m.IsEventAdder && // The IL code never explicitly calls events adder/remover.
!m.IsEventRemover &&
!m.IsGeneratedByCompiler &&
!m.ParentType.IsDelegate &&
!hasExemptionAttributeProc(m) &&
!isCalledByRuntimeProc(m) &&
// Don't consider Global ASP.NET methods as unused
!m.ParentType.DeriveFrom("System.Web.HttpApplication".AllowNoMatch()) &&
// Methods tagged with these attributes are called by the serialization infrastructure.
!m.HasAttribute("System.Runtime.Serialization.OnSerializingAttribute".AllowNoMatch()) &&
!m.HasAttribute("System.Runtime.Serialization.OnDeserializingAttribute".AllowNoMatch()) &&
!m.HasAttribute("System.Runtime.Serialization.OnSerializedAttribute".AllowNoMatch()) &&
!m.HasAttribute("System.Runtime.Serialization.OnDeserializedAttribute".AllowNoMatch()) &&
// Don't consider public getters/setters
// of classes that implement INotifyPropertyChanged
// as dead code.
!(m.IsPublic && (m.IsPropertyGetter || m.IsPropertySetter) &&
m.ParentType.Implement("System.ComponentModel.INotifyPropertyChanged".AllowNoMatch())) &&
// Controller methods are used only by the infrastructure except methods tagged by NonAction.
!(isControllerProc(m.ParentType) &&
!m.HasAttribute("System.Web.Mvc.NonActionAttribute".AllowNoMatch())) &&
// Methods of classes that derive from Unity classes are used implicitly by the Unity engine
!m.ParentType.BaseClasses.Any(bc => bc.ParentNamespace.Name.StartsWithAny("UnityEngine","UnityEditor")) &&
// Method might be called by expression in DebuggerDisplayAttribute
!m.ParentType.HasAttribute("System.Diagnostics.DebuggerDisplayAttribute".AllowNoMatch()) &&
// Instance ctor in class implementing interface(s) might be used through Dependency Injection
!(m.IsConstructor && (m.IsPublic || m.IsInternal) && !m.IsStatic &&
(m.ParentType.NbInterfacesImplemented > 0 ||
// The instance ctor is not used, and its parent type is used by a type
// that does not use any of its members.
// This certainly shows the service injection pattern.
m.ParentType.TypesUsingMe.Any(t => !t.Methods.Any(m1 => m1.MembersUsed.Any(m2 => m2.ParentType == m.ParentType)))
))
)
let canFieldBeMatchedProc = new Func<IField, bool>(
f => !f.IsPublic && // Although not recommended, public fields might be used by client applications of your assemblies.
!f.IsLiteral && // The IL code never explicitly uses literal fields.
!f.IsEnumValue && // The IL code never explicitly uses enumeration value.
f.Name != "value__" && // Field named 'value__' are relative to enumerations and the IL code never explicitly uses them.
!f.IsGeneratedByCompiler &&
!hasExemptionAttributeProc(f) &&
// Don't warn for fields tagged with these Unity specific attribute
!f.HasAttribute("UnityEngine.SerializeField".AllowNoMatch()) &&
!f.HasAttribute("UnityEngine.SerializeReference".AllowNoMatch()) &&
// Don't warn about InlineArrayAttribute's field
!f.ParentType.HasAttribute("System.Runtime.CompilerServices.InlineArrayAttribute".AllowNoMatch())
)
// Types used, but only from test code (the TestCode view).
// Usages from the type's own nested types don't count.
let typesUsedOnlyByTests = (
from t in JustMyCode.Types
let users = t.TypesUsingMe.Where(tUser => !t.NestedTypes.Contains(tUser))
where users.Any() && users.All(tUser => tUser.IsTestCode) &&
canTypeBeMatchedProc(t)
select t).ToHashSetEx()
// A matched type hides its members and nested types: removing or moving the type fixes them too.
let isInMatchedTypeProc = new Func<IType, bool>(
t => typesUsedOnlyByTests.Contains(t) || t.OutterTypes.Any(tOuter => typesUsedOnlyByTests.Contains(tOuter)))
let types = typesUsedOnlyByTests.Where(t => !t.OutterTypes.Any(tOuter => typesUsedOnlyByTests.Contains(tOuter)))
// Methods called, but only from test code
let methods =
from m in JustMyCode.Methods where
m.NbMethodsCallingMe > 0 && m.MethodsCallingMe.All(mCaller => mCaller.IsTestCode) &&
!isInMatchedTypeProc(m.ParentType) &&
canMethodBeMatchedProc(m)
select m
// Fields used, but only from test code
let fields =
from f in JustMyCode.Fields where
f.NbMethodsUsingMe > 0 && f.MethodsUsingMe.All(mUser => mUser.IsTestCode) &&
!isInMatchedTypeProc(f.ParentType) &&
canFieldBeMatchedProc(f)
select f
from member in types.Cast<IMember>().Concat(methods).Concat(fields)
let testUsers = member.IsType ? member.AsType.TypesUsingMe.Where(tUser => tUser.IsTestCode).Cast<ICodeElement>() :
member.IsMethod ? member.AsMethod.MethodsCallingMe.Cast<ICodeElement>() :
member.AsField.MethodsUsingMe.Cast<ICodeElement>()
let loc = member.IsType ? member.AsType.NbLinesOfCode : member.IsMethod ? member.AsMethod.NbLinesOfCode : null
select new {
member,
testUsers,
Debt = (member.IsType ? 25 : member.IsMethod ? 20 : 15).ToMinutes().ToDebt(),
AnnualInterest = (member.IsType ? (10 + (loc ?? 1)) / 4 : member.IsMethod ? (8 + (loc ?? 1)) / 4 : 2).ToMinutes().ToAnnualInterest()
}
//<Expl>
// The *{0}* is used only from test code: the program never uses it, only tests keep it alive.
//</Expl>
//<Desc>
// Lists application **types, methods and fields used only from test code**
// (the *TestCode* view): the program itself never uses them, only tests
// keep them alive. The *testUsers* column lists these test code users.
//
// Such code is either dead production code whose tests were left behind,
// or a test helper living in the application code.
//
// When a type is matched, its members and nested types are not matched.
// Elements exempted by the *Potentially Dead Types/Methods/Fields* rules
// (public API, infrastructure entry points, *IsNotDeadCodeAttribute*…) are
// not matched either.
//</Desc>
//<HowToFix>
// If the element is dead production code, remove it together with the
// tests that exercise it.
//
// If it is a test helper, move it into the test code: a method that only
// uses members visible from the tests can become an extension method there,
// so its call sites stay unchanged.
//
// If it is a deliberate test hook, or is used in ways IL analysis cannot
// see, tag it with **IsNotDeadCodeAttribute** from *NDepend.API.dll*.
//
// **Debt** is 25 minutes for a type, 20 for a method and 15 for a field:
// the tests must be updated too. **Annual Interest** is proportional to the
// #lines of code and divided by 4 (2 minutes for a field), since such code
// is still exercised by the tests.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Software Composition Analysis (SCA) - Security
Raises an issue per consumed NuGet package version with known security advisories listed on GitHub:
https://github.com/advisories?query=+ecosystem%3Anuget The issue is reported on the first type (alphabetically) using a third-party assembly from the affected package.
Flagged Code<!-- MyApp.csproj: Newtonsoft.Json 12.0.1 has the advisory GHSA-5crp-9r3c-p9vr -->
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
</ItemGroup>
Fixed Code<!-- 13.0.1 is the patched version that resolves all known advisories of this package -->
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
How to Fix:Update the affected package to at least the patched version recommended, which resolves all known advisories for the package. It is not always the latest version (e.g. an issue in 4.0.2 might be fixed in 4.1.0 while 7.0.8 exists).
// <Name>Software Composition Analysis (SCA)</Name>
// <Id>ND3120:SoftwareCompositionAnalysis</Id>
warnif count > 0
// Use this hashset to avoid to have several SAC issues on a type
let hashset = new HashSet<IType>()
// Gather third-party assemblies with one or several GitHub Security Advisories
let thirdPartyAsmWithAdvisories = from asm in ThirdParty.Assemblies
// Assemblies from Microsoft.NetCore.App.Runtime pkg reference version
// are always X.0.0.0 (like 8.0.0.0 or 9.0.0.0)
// Thus this rule cannot detect if patched version is referenced.
where !asm.Name.StartsWithAny("System.","Microsoft.") &&
// If the asm is not resolved at analysis time, the file-version
// remains unknow and this can lead to false positive
!asm.IsThirdPartyInferredFromRef
let advisories = asm.GetSecurityAdvisories()
where advisories.Count > 0
select new {
asm,
advisories,
PackageName = (string)advisories.First().PackageName
}
// Merge third-party assemblies within the same NuGet Package
from x in thirdPartyAsmWithAdvisories.ToLookup(a1 => a1.PackageName)
// Gather data useful to describe the advisories and how to fix
let HighestSeverity = x.SelectMany(r => r.advisories).Max(ad => ad.Severity)
let AdvisoryWithHighestSeverity = x.SelectMany(r => r.advisories).First(ad => ad.Severity == HighestSeverity)
let ThirdPartyAssembliesImpacted = x.Select(x1 => x1.asm).ToArray()
// Application AND test assemblies: a vulnerable package referenced only by tests is still a vulnerability
let AppAssembliesUser = ApplicationAndTestCode.Assemblies.UsingAny(ThirdPartyAssembliesImpacted)
// Discard when consummers are only .NET Fx v4.x, this provoke some false positive with .NET Core version range matching
where AppAssembliesUser.Any(a1 => !a1.TargetFrameworkName.StartsWith("net4"))
// The issue appears on the first type using one culprit third-party assembly
let typeUsers = ApplicationAndTestCode.Types.UsingAny(ThirdPartyAssembliesImpacted)
.Where(t => !t.IsGeneratedByCompiler || t.Name.StartsWith("Program+")).ToArray()
let FirstAppTypeUser = (typeUsers.Length == 0 ? ApplicationAndTestCode.Types : typeUsers ) // Make sure at least one type else we get an exception!
.Aggregate((minElement, nextElement) =>
string.Compare(nextElement.Name, minElement.Name, StringComparison.Ordinal) < 0 && !hashset.Contains(nextElement)
? nextElement
: minElement)
let fillHashset = hashset.Add(FirstAppTypeUser)
select new {
FirstAppTypeUser,
PackageName = x.Key,
ThirdPartyAssembliesImpacted,
AppAssembliesUser,
PackageVersion = x.First().asm.Version,
PatchAllAdvisoriesVersion = x.First().advisories.Max(ad => ad.PatchedVersion),
HighestSeverity,
AdvisoryWithHighestSeverity = AdvisoryWithHighestSeverity.Url,
AdvisorySummary = AdvisoryWithHighestSeverity.Summary,
NbAdvisory = x.First().advisories.Count,
AdvisoriesUrl = x.First().advisories.Select(ad=> ad.Url).Except(AdvisoryWithHighestSeverity.Url).Aggregate(" ; "),
Debt = 20.ToMinutes().ToDebt()
}
//<Expl>
// The application uses the NuGet Package **{1} version {4}** which is affected by {9} Github security advisor{9?y:ies}.
//
// **Recommended Action:** Update the package to version *{5}* or higher that has no known security advisory.
//
// Advisory with highest severity *{6}* is {7} : **{8}**. {9?:Other advisory url: }{10}
//
// The *{0}* is the first type (alphabetically) in the application that uses this package.
//</Expl>
//<Desc>
// Raises an issue per consumed NuGet package version with known security
// advisories listed on GitHub:
// https://github.com/advisories?query=+ecosystem%3Anuget
// The issue is reported on the first type (alphabetically) using a
// third-party assembly from the affected package.
//
// **Flagged Code**
// ```xml
// <!-- MyApp.csproj: Newtonsoft.Json 12.0.1 has the advisory GHSA-5crp-9r3c-p9vr -->
// <ItemGroup>
// <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
// </ItemGroup>
// ```
//
// **Fixed Code**
// ```xml
// <!-- 13.0.1 is the patched version that resolves all known advisories of this package -->
// <ItemGroup>
// <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
// </ItemGroup>
// ```
//</Desc>
//<HowToFix>
// Update the affected package to at least the patched version recommended,
// which resolves all known advisories for the package. It is not always the
// latest version (e.g. an issue in 4.0.2 might be fixed in 4.1.0 while 7.0.8
// exists).
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Transitive Software Composition Analysis (SCA) - Security
Raises an issue per NuGet package with known GitHub security advisories (
https://github.com/advisories?query=+ecosystem%3Anuget) used by a package used by the application — hence
transitive, unlike the rule
Software Composition Analysis (SCA). The issue is reported on the first type (alphabetically) using a third-party assembly using the affected package.
Flagged Code<!-- Acme.Sdk 2.1.0 depends on Newtonsoft.Json 12.0.1 (advisory GHSA-5crp-9r3c-p9vr) -->
<!-- Newtonsoft.Json is not referenced directly: the advisory is transitive -->
<ItemGroup>
<PackageReference Include="Acme.Sdk" Version="2.1.0" />
</ItemGroup>
Fixed Code<!-- Acme.Sdk 2.2.0 depends on Newtonsoft.Json 13.0.1, which has no known advisory -->
<ItemGroup>
<PackageReference Include="Acme.Sdk" Version="2.2.0" />
</ItemGroup>
<!-- Alternative when no such Acme.Sdk version exists: pin the transitive package -->
<ItemGroup>
<PackageReference Include="Acme.Sdk" Version="2.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
How to Fix:Update the package containing
DirectAsm to a version not using a package with known security advisories.
// <Name>Transitive Software Composition Analysis (SCA)</Name>
// <Id>ND3121:SoftwareCompositionAnalysisTransitive</Id>
warnif count > 0
// Use this hashset to avoid to have several SAC issues on a type
let hashset = new HashSet<IType>()
let thirdPartyAsmNames = ThirdParty.Assemblies.Select(a => a.Name).ToHashSetEx(StringComparer.OrdinalIgnoreCase)
// Avoid reporting packages reported by the non-transitive SAC rule
let directPackageNamesWithAdvisories =
(from asm in ThirdParty.Assemblies
let advisories = asm.GetSecurityAdvisories()
where advisories.Count > 0
select (string)advisories.First().PackageName).ToHashSetEx(StringComparer.OrdinalIgnoreCase)
// Gather assembly references used by third-party assemblies
// with one or several GitHub Security Advisories
let indirectAsmWithAdvisories =
from directAsm in ThirdParty.Assemblies
// Assemblies from Microsoft.NetCore.App.Runtime pkg reference version
// are always X.0.0.0 (like 8.0.0.0 or 9.0.0.0)
// Thus this rule cannot detect if patched version is referenced.
where !directAsm.Name.StartsWithAny("System.","Microsoft.")
from indirectAsm in directAsm.AssemblyReferences
where !indirectAsm.Name.StartsWithAny("System.","Microsoft.")
// Don't match assemblies directly used by the app
where !thirdPartyAsmNames.Contains(indirectAsm.Name)
let advisories = indirectAsm.GetSecurityAdvisories()
where advisories.Count > 0
let PackageName = (string)advisories.First().PackageName
// Avoid reporting packages reported by the non-transitive SAC rule
where !directPackageNamesWithAdvisories.Contains(PackageName)
select new {
directAsm,
indirectAsm,
advisories,
PackageName
}
// Merge assemblies references within the same NuGet Package
from x in indirectAsmWithAdvisories.ToLookup(a1 => a1.PackageName)
// Gather data useful to describe the advisories and how to fix
let HighestSeverity = x.SelectMany(r => r.advisories).Max(ad => ad.Severity)
let AdvisoryWithHighestSeverity = x.SelectMany(r => r.advisories).First(ad => ad.Severity == HighestSeverity)
let ThirdPartyAssembliesImpacted = x.Select(x1 => x1.directAsm).Distinct().ToArray()
// Application AND test assemblies: a vulnerable package referenced only by tests is still a vulnerability
let AppAssembliesUser = ApplicationAndTestCode.Assemblies.UsingAny(ThirdPartyAssembliesImpacted)
// The issue appears on the first type using one culprit third-party assembly
let typeUsers = ApplicationAndTestCode.Types.UsingAny(ThirdPartyAssembliesImpacted)
.Where(t => !t.IsGeneratedByCompiler || t.Name.StartsWith("Program+")).ToArray()
let FirstAppTypeUser = (typeUsers.Length == 0 ? ApplicationAndTestCode.Types : typeUsers ) // Make sure at least one type else we get an exception!
.Aggregate((minElement, nextElement) =>
string.Compare(nextElement.Name, minElement.Name, StringComparison.Ordinal) < 0 && !hashset.Contains(nextElement)
? nextElement
: minElement)
let fillHashset = hashset.Add(FirstAppTypeUser)
select new {
FirstAppTypeUser,
DirectAsm = x.First().directAsm,
DirectAsmVersion = x.First().directAsm.Version,
PackageName = x.Key,
PackageVersion = x.First().indirectAsm.Version,
ThirdPartyAssembliesImpacted,
AppAssembliesUser,
PatchAllAdvisoriesVersion = x.First().advisories.Max(ad => ad.PatchedVersion),
HighestSeverity,
AdvisoryWithHighestSeverity = AdvisoryWithHighestSeverity.Url,
AdvisorySummary = AdvisoryWithHighestSeverity.Summary,
NbAdvisory = x.First().advisories.Count,
AdvisoriesUrl = x.First().advisories.Select(ad=> ad.Url).Except(AdvisoryWithHighestSeverity.Url).Aggregate(" ; "),
Debt = 20.ToMinutes().ToDebt()
}
//<Expl>
// The application uses the **{1} version {2}** that uses the NuGet Package **{3} version {4}** which is affected by {11} Github security advisor{11?y:ies}.
//
// **Recommended Action:** Update the package of the *{1}* to a version that uses at least *{3}* version *{7}* or higher that has no known security advisory.
//
// Advisory with highest severity *{8}* is {9} : **{10}**. {11?:Other advisory url: }{12}
//
// The *{0}* is the first type (alphabetically) in the application that uses the *{1}*.
//</Expl>
//<Desc>
// Raises an issue per NuGet package with known GitHub security advisories
// (https://github.com/advisories?query=+ecosystem%3Anuget) used by a package
// used by the application — hence **transitive**, unlike the rule *Software
// Composition Analysis (SCA)*. The issue is reported on the first type
// (alphabetically) using a third-party assembly using the affected package.
//
// **Flagged Code**
// ```xml
// <!-- Acme.Sdk 2.1.0 depends on Newtonsoft.Json 12.0.1 (advisory GHSA-5crp-9r3c-p9vr) -->
// <!-- Newtonsoft.Json is not referenced directly: the advisory is transitive -->
// <ItemGroup>
// <PackageReference Include="Acme.Sdk" Version="2.1.0" />
// </ItemGroup>
// ```
//
// **Fixed Code**
// ```xml
// <!-- Acme.Sdk 2.2.0 depends on Newtonsoft.Json 13.0.1, which has no known advisory -->
// <ItemGroup>
// <PackageReference Include="Acme.Sdk" Version="2.2.0" />
// </ItemGroup>
// <!-- Alternative when no such Acme.Sdk version exists: pin the transitive package -->
// <ItemGroup>
// <PackageReference Include="Acme.Sdk" Version="2.1.0" />
// <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
// </ItemGroup>
// ```
//</Desc>
//<HowToFix>
// Update the package containing *DirectAsm* to a version not using a
// package with known security advisories.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't hard-code secrets in constants - Security
Matches
const string fields, and
static readonly string fields whose static constructor only manipulates strings, named like a password, API key, private key, connection string, token, client secret or signing key. A literal secret ships in every build, leaks through decompilation, source control history and crash dumps, and cannot be rotated without a redeploy.
Names that merely designate a setting (
PasswordFieldName,
ApiKeyHeader,
PasswordMinLength,
PasswordHash...) or a user-facing text (
PasswordTooltip,
InvalidSecretMessage...) are excluded. The rule cannot see the literal value: a placeholder like
"CHANGE_ME" or an empty string is reported too, suppress such issues.
This is CWE-798
Use of Hard-coded Credentials.
Flagged Codestatic class DbSettings {
const string Password = "P@ssw0rd!"; // flagged: const secret
static readonly string ApiKey = "sk-live-" + "4f9c2e1a"; // flagged: only string ops
}
Fixed Codeclass DbSettings {
readonly string _password;
readonly string _apiKey;
DbSettings(IConfiguration config) {
_password = config["Db:Password"]; // user-secrets, environment or key vault
_apiKey = Environment.GetEnvironmentVariable("API_KEY");
}
}
How to Fix:Read the secret at runtime from a configuration source that is not committed: environment variables,
dotnet user-secrets in development, a secret store (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault) or a protected configuration provider in production. Rotate the secret that was committed. Once the field is gone, scan the source history to purge the value.
// <Name>Don't hard-code secrets in constants</Name>
// <Id>ND3114:DontHardCodeSecretsInConstants</Id>
warnif count > 0
from f in Application.Fields
where f.FieldType != null &&
f.FieldType.FullName == "System.String" &&
!f.IsGeneratedByCompiler &&
// Names that designate a secret value...
f.NameLike(@"(password|passwd|pwd|secret|apikey|api_key|privatekey|private_key|connectionstring|connection_string|accesstoken|access_token|authtoken|auth_token|bearertoken|bearer_token|refreshtoken|refresh_token|clientsecret|client_secret|encryptionkey|encryption_key|signingkey|signing_key|securitykey|security_key|hmackey|hmac_key|masterkey|master_key|credential)\i") &&
// ...but not a user-facing text mentioning it...
!f.NameLike(@"(tooltip|message|msg|failure|error|label|title|hint|help|desc|caption|prompt|question|warning|notice|info|text)\i") &&
// ...nor the name of a setting, header, claim, validation rule or hashed form of it.
// (key$ matches "ApiKeyKey"-like names that hold the *name* of the setting storing the secret.)
!f.NameLike(@"(name|names|length|len|regex|pattern|format|header|headers|param|parameter|field|property|prop|placeholder|prefix|suffix|url|uri|path|type|kind|id|hash|hashed|salt|policy|option|options|setting|settings|section|claim|claimtype|scheme|scope|template|required|min|max|count|size|resource|expiration|expiry|lifetime|timeout|attempts|provider|source|env|environment|variable|var|key)$\i")
let cctors = f.ParentType.Methods.Where(m => m.IsClassConstructor).ToArray()
// const: the value is a literal for sure.
// static readonly: the value is a literal unless the static constructor calls anything but String members.
where f.IsLiteral ||
(f.IsStatic && f.IsInitOnly && cctors.Length > 0 &&
cctors.All(cctor => !cctor.MethodsCalled.Any(mc => mc.ParentType.FullName != "System.String")))
select new {
f,
IsConst = f.IsLiteral,
Debt = 30.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The {1?constant:static read-only} field *{0}* is named like a secret and holds a
// literal value baked into the assembly. Anyone with the binary (or the
// source repository) can read it.
//</Expl>
//<Desc>
// Matches *const* string fields, and *static readonly* string fields whose
// static constructor only manipulates strings, named like a password,
// API key, private key, connection string, token, client secret or
// signing key. A literal secret ships in every build, leaks through
// decompilation, source control history and crash dumps, and cannot be
// rotated without a redeploy.
//
// Names that merely designate a setting (*PasswordFieldName*,
// *ApiKeyHeader*, *PasswordMinLength*, *PasswordHash*...) or a user-facing
// text (*PasswordTooltip*, *InvalidSecretMessage*...) are excluded.
// The rule cannot see the literal value: a placeholder like *"CHANGE_ME"*
// or an empty string is reported too, suppress such issues.
//
// This is CWE-798 *Use of Hard-coded Credentials*.
//
// **Flagged Code**
// ```csharp
// static class DbSettings {
// const string Password = "P@ssw0rd!"; // flagged: const secret
// static readonly string ApiKey = "sk-live-" + "4f9c2e1a"; // flagged: only string ops
// }
// ```
//
// **Fixed Code**
// ```csharp
// class DbSettings {
// readonly string _password;
// readonly string _apiKey;
// DbSettings(IConfiguration config) {
// _password = config["Db:Password"]; // user-secrets, environment or key vault
// _apiKey = Environment.GetEnvironmentVariable("API_KEY");
// }
// }
// ```
//</Desc>
//<HowToFix>
// Read the secret at runtime from a configuration source that is not
// committed: environment variables, *dotnet user-secrets* in development,
// a secret store (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault)
// or a protected configuration provider in production. Rotate the secret
// that was committed. Once the field is gone, scan the source history to
// purge the value.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review credentials built from hard-coded values - Security
Matches methods that create a
NetworkCredential, set the
Password of a connection string builder, or load a PKCS#12 certificate with a password, while nothing in the method can supply that secret at runtime: no string parameter, no string field read, no call returning a string (configuration, environment, property getter, secret store). The only remaining source is a string literal.
This is CWE-798
Use of Hard-coded Credentials.
Flagged Codestatic HttpClientHandler CreateHandler() {
var handler = new HttpClientHandler();
handler.Credentials = new NetworkCredential("svc-account", "P@ssw0rd!"); // flagged
return handler;
}
Fixed Codestatic HttpClientHandler CreateHandler(IConfiguration config) {
var handler = new HttpClientHandler();
string user = config["Svc:User"]; // the secret comes from configuration
string password = config["Svc:Password"];
handler.Credentials = new NetworkCredential(user, password);
return handler;
}
How to Fix:Take the secret from a configuration source that is not committed (environment variable,
dotnet user-secrets, Azure Key Vault, AWS Secrets Manager, HashiCorp Vault...) and pass it to the method or inject it through options. Rotate the credential that was committed.
// <Name>Review credentials built from hard-coded values</Name>
// <Id>ND3115:ReviewCredentialsBuiltFromHardCodedValues</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
let credentialMembers =
// new NetworkCredential("user", "password")
ThirdParty.Types.WithFullName("System.Net.NetworkCredential")
.ChildMethods().Where(m => m.IsConstructor && m.NbParameters >= 2)
// builder.Password = "..." (SqlConnectionStringBuilder, NpgsqlConnectionStringBuilder, MySqlConnectionStringBuilder...)
.Concat(ThirdParty.Methods.Where(m => m.SimpleName == "set_Password" && m.ParentType.SimpleName.EndsWith("ConnectionStringBuilder")))
// new X509Certificate2(file, "password") / X509CertificateLoader.LoadPkcs12FromFile(file, "password")
.Concat(ThirdParty.Types.WithFullNameIn("System.Security.Cryptography.X509Certificates.X509Certificate2",
"System.Security.Cryptography.X509Certificates.X509Certificate")
.ChildMethods().Where(m => m.IsConstructor && m.NbParameters >= 2 && m.Name.Contains("String")))
.Concat(ThirdParty.Types.WithFullName("System.Security.Cryptography.X509Certificates.X509CertificateLoader")
.ChildMethods().Where(m => m.SimpleName.StartsWith("LoadPkcs12") && m.Name.Contains("String")))
.ToHashSetEx()
where credentialMembers.Any()
from m in Application.Methods.UsingAny(credentialMembers)
let credentialMembersUsed = m.MethodsCalled.Intersect(credentialMembers).ToArray()
// The secret can come from... a string parameter
where !m.Name.Contains("String") &&
// ...a field of the type (injected options, configuration)
!m.FieldsUsed.Any(f => f.FieldType != null && f.FieldType.FullNameEqualsAny("System.String", "System.Security.SecureString", "System.Char[]")) &&
// ...any other call returning a string (configuration, environment variable, property getter, secret store, prompt)
!m.MethodsCalled.Except(credentialMembers).Any(mc =>
mc.ReturnType != null &&
mc.ReturnType.FullNameEqualsAny("System.String", "System.Security.SecureString") &&
mc.ParentType.FullName != "System.String")
select new {
m,
credentialMembersUsed,
Debt = 30.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* builds a credential with *{1}* without any string parameter,
// string field or call returning a string: the user name, password or
// certificate password can only be a literal baked into the assembly.
//</Expl>
//<Desc>
// Matches methods that create a *NetworkCredential*, set the *Password* of
// a connection string builder, or load a PKCS#12 certificate with a
// password, while nothing in the method can supply that secret at runtime:
// no string parameter, no string field read, no call returning a string
// (configuration, environment, property getter, secret store). The only
// remaining source is a string literal.
//
// This is CWE-798 *Use of Hard-coded Credentials*.
//
// **Flagged Code**
// ```csharp
// static HttpClientHandler CreateHandler() {
// var handler = new HttpClientHandler();
// handler.Credentials = new NetworkCredential("svc-account", "P@ssw0rd!"); // flagged
// return handler;
// }
// ```
//
// **Fixed Code**
// ```csharp
// static HttpClientHandler CreateHandler(IConfiguration config) {
// var handler = new HttpClientHandler();
// string user = config["Svc:User"]; // the secret comes from configuration
// string password = config["Svc:Password"];
// handler.Credentials = new NetworkCredential(user, password);
// return handler;
// }
// ```
//</Desc>
//<HowToFix>
// Take the secret from a configuration source that is not committed
// (environment variable, *dotnet user-secrets*, Azure Key Vault, AWS
// Secrets Manager, HashiCorp Vault...) and pass it to the method or inject
// it through options. Rotate the credential that was committed.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't use insecure deserializers - Security
Matches methods using a deserializer that instantiates arbitrary types named in the payload:
BinaryFormatter (obsolete SYSLIB0011, removed from .NET 9),
SoapFormatter,
NetDataContractSerializer,
LosFormatter,
ObjectStateFormatter,
JavaScriptSerializer with a type resolver, or Newtonsoft.Json with
TypeNameHandling set. Gadget chains available in the framework turn such deserialization of untrusted input into remote code execution:
https://learn.microsoft.com/dotnet/standard/serialization/binaryformatter-security-guide
This is CWE-502
Deserialization of Untrusted Data.
Flagged Codestatic Order Load(byte[] payload) {
var formatter = new BinaryFormatter(); // flagged: BinaryFormatter
using var stream = new MemoryStream(payload);
return (Order)formatter.Deserialize(stream);
}
static JsonSerializerSettings Settings() =>
new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }; // flagged
Fixed Codestatic Order Load(byte[] payload) =>
JsonSerializer.Deserialize<Order>(payload); // System.Text.Json, fixed type
static JsonSerializerSettings Settings() =>
new JsonSerializerSettings(); // TypeNameHandling stays None
How to Fix:Use
System.Text.Json,
XmlSerializer or
DataContractSerializer with a fixed, known set of types. With Newtonsoft.Json keep
TypeNameHandling.None or provide a restrictive
SerializationBinder. If a legacy format must be read, deserialize it only from trusted sources and suppress the issue with a justification.
// <Name>Don't use insecure deserializers</Name>
// <Id>ND3110:DontUseInsecureDeserializers</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
let insecureDeserializerTypes = ThirdParty.Types.WithFullNameIn(
"System.Runtime.Serialization.Formatters.Binary.BinaryFormatter",
"System.Runtime.Serialization.Formatters.Soap.SoapFormatter",
"System.Runtime.Serialization.NetDataContractSerializer",
"System.Web.UI.LosFormatter",
"System.Web.UI.ObjectStateFormatter",
"System.Messaging.BinaryMessageFormatter",
// JavaScriptSerializer becomes unsafe as soon as a type resolver is provided
"System.Web.Script.Serialization.SimpleTypeResolver",
"System.Web.Script.Serialization.JavaScriptTypeResolver").ToHashSetEx()
// Newtonsoft.Json: TypeNameHandling other than None lets the payload choose the types to instantiate.
// The enum value is not visible in IL, but explicitly setting the property is the smell to review.
let typeNameHandlingSetters = ThirdParty.Types.WithFullNameIn(
"Newtonsoft.Json.JsonSerializerSettings",
"Newtonsoft.Json.JsonSerializer")
.ChildMethods().Where(m => m.SimpleName == "set_TypeNameHandling").ToHashSetEx()
where insecureDeserializerTypes.Any() || typeNameHandlingSetters.Any()
from m in Application.Methods
let insecureTypesUsed = insecureDeserializerTypes.Where(t => m.IsUsing(t)).ToArray()
let typeNameHandlingSettersCalled = m.MethodsCalled.Intersect(typeNameHandlingSetters).ToArray()
where insecureTypesUsed.Length > 0 || typeNameHandlingSettersCalled.Length > 0
select new {
m,
insecureTypesUsed,
typeNameHandlingSettersCalled,
Debt = (insecureTypesUsed.Length > 0 ? 2.ToHours() : 30.ToMinutes()).ToDebt(),
Severity = insecureTypesUsed.Length > 0 ? Severity.Critical : Severity.High
}
//<Expl>
// The *{0}* uses {1}{2}: deserializing untrusted data with it can lead to
// *remote code execution*, since the payload decides which types get instantiated.
//</Expl>
//<Desc>
// Matches methods using a deserializer that instantiates arbitrary types
// named in the payload: *BinaryFormatter* (obsolete SYSLIB0011, removed
// from .NET 9), *SoapFormatter*, *NetDataContractSerializer*,
// *LosFormatter*, *ObjectStateFormatter*, *JavaScriptSerializer* with a
// type resolver, or Newtonsoft.Json with *TypeNameHandling* set.
// Gadget chains available in the framework turn such deserialization of
// untrusted input into remote code execution:
// https://learn.microsoft.com/dotnet/standard/serialization/binaryformatter-security-guide
//
// This is CWE-502 *Deserialization of Untrusted Data*.
//
// **Flagged Code**
// ```csharp
// static Order Load(byte[] payload) {
// var formatter = new BinaryFormatter(); // flagged: BinaryFormatter
// using var stream = new MemoryStream(payload);
// return (Order)formatter.Deserialize(stream);
// }
// static JsonSerializerSettings Settings() =>
// new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }; // flagged
// ```
//
// **Fixed Code**
// ```csharp
// static Order Load(byte[] payload) =>
// JsonSerializer.Deserialize<Order>(payload); // System.Text.Json, fixed type
// static JsonSerializerSettings Settings() =>
// new JsonSerializerSettings(); // TypeNameHandling stays None
// ```
//</Desc>
//<HowToFix>
// Use *System.Text.Json*, *XmlSerializer* or *DataContractSerializer*
// with a fixed, known set of types. With Newtonsoft.Json keep
// *TypeNameHandling.None* or provide a restrictive *SerializationBinder*.
// If a legacy format must be read, deserialize it only from trusted
// sources and suppress the issue with a justification.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't use weak hash algorithms (MD5, SHA1) - Security
Matches methods calling MD5, SHA1, RIPEMD160 or HMACMD5 constructors, factories (
MD5.Create()), one-shot helpers (
SHA1.HashData()) or selecting them through
HashAlgorithmName.MD5 /
HashAlgorithmName.SHA1. Practical collision attacks exist against MD5 (since 2004) and SHA1 (SHAttered, 2017):
https://shattered.io/ . NIST deprecated SHA1 for digital signatures in 2011 and plans its full retirement by 2030.
This is CWE-328
Use of Weak Hash.
Flagged Codestatic string Fingerprint(byte[] data) {
using var md5 = MD5.Create(); // flagged: MD5
return Convert.ToHexString(md5.ComputeHash(data));
}
static byte[] Digest(byte[] data) => SHA1.HashData(data); // flagged: SHA1
Fixed Codestatic string Fingerprint(byte[] data) {
using var sha256 = SHA256.Create();
return Convert.ToHexString(sha256.ComputeHash(data));
}
static byte[] Digest(byte[] data) => SHA256.HashData(data);
How to Fix:Use
SHA256 or stronger (
SHA384,
SHA512,
SHA3_256) for integrity and signatures, and a dedicated password hashing function (
Rfc2898DeriveBytes with SHA256,
bcrypt,
scrypt or
Argon2) for passwords.
When the hash is not security-related (cache keys, ETags, deduplication of non-hostile data), prefer a fast non-cryptographic hash such as
System.IO.Hashing.XxHash3 and suppress the issue.
// <Name>Don't use weak hash algorithms (MD5, SHA1)</Name>
// <Id>ND3109:DontUseWeakHashAlgorithms</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
let weakHashTypes = ThirdParty.Types.WithFullNameIn(
"System.Security.Cryptography.MD5",
"System.Security.Cryptography.MD5CryptoServiceProvider",
"System.Security.Cryptography.MD5Cng",
"System.Security.Cryptography.SHA1",
"System.Security.Cryptography.SHA1Managed",
"System.Security.Cryptography.SHA1CryptoServiceProvider",
"System.Security.Cryptography.SHA1Cng",
"System.Security.Cryptography.RIPEMD160",
"System.Security.Cryptography.RIPEMD160Managed",
"System.Security.Cryptography.HMACMD5",
"System.Security.Cryptography.HMACRIPEMD160",
"System.Security.Cryptography.MACTripleDES")
// Constructors, factories and one-shot helpers: new MD5CryptoServiceProvider(), SHA1.Create(), MD5.HashData(bytes)...
let weakHashMembers = weakHashTypes.ChildMethods()
.Where(m => m.IsConstructor ||
(m.IsStatic && m.SimpleName.StartsWithAny("Create", "HashData", "TryHashData", "HashDataAsync")))
// HashAlgorithmName.MD5 / HashAlgorithmName.SHA1 select a weak algorithm in
// IncrementalHash, Rfc2898DeriveBytes, RSA.SignData(), ECDsa.SignData(), X509 requests...
.Concat(ThirdParty.Types.WithFullName("System.Security.Cryptography.HashAlgorithmName")
.ChildMethods().Where(m => m.SimpleNameEqualsAny("get_MD5", "get_SHA1")))
.ToHashSetEx()
where weakHashMembers.Any()
from m in Application.Methods.UsingAny(weakHashMembers)
let weakHashMembersUsed = m.MethodsCalled.Intersect(weakHashMembers).ToArray()
select new {
m,
weakHashMembersUsed,
Debt = (20 + 10 * weakHashMembersUsed.Length).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* uses the weak hash algorithm(s) *{1}*.
// MD5 and SHA1 are vulnerable to collision attacks and must not protect
// passwords, signatures, tokens or integrity checks.
//</Expl>
//<Desc>
// Matches methods calling MD5, SHA1, RIPEMD160 or HMACMD5 constructors,
// factories (*MD5.Create()*), one-shot helpers (*SHA1.HashData()*) or
// selecting them through *HashAlgorithmName.MD5* / *HashAlgorithmName.SHA1*.
// Practical collision attacks exist against MD5 (since 2004) and SHA1
// (SHAttered, 2017): https://shattered.io/ . NIST deprecated SHA1 for
// digital signatures in 2011 and plans its full retirement by 2030.
//
// This is CWE-328 *Use of Weak Hash*.
//
// **Flagged Code**
// ```csharp
// static string Fingerprint(byte[] data) {
// using var md5 = MD5.Create(); // flagged: MD5
// return Convert.ToHexString(md5.ComputeHash(data));
// }
// static byte[] Digest(byte[] data) => SHA1.HashData(data); // flagged: SHA1
// ```
//
// **Fixed Code**
// ```csharp
// static string Fingerprint(byte[] data) {
// using var sha256 = SHA256.Create();
// return Convert.ToHexString(sha256.ComputeHash(data));
// }
// static byte[] Digest(byte[] data) => SHA256.HashData(data);
// ```
//</Desc>
//<HowToFix>
// Use *SHA256* or stronger (*SHA384*, *SHA512*, *SHA3_256*) for integrity
// and signatures, and a dedicated password hashing function (*Rfc2898DeriveBytes
// with SHA256*, *bcrypt*, *scrypt* or *Argon2*) for passwords.
//
// When the hash is not security-related (cache keys, ETags, deduplication
// of non-hostile data), prefer a fast non-cryptographic hash such as
// *System.IO.Hashing.XxHash3* and suppress the issue.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't use DES/3DES/RC2 weak cipher algorithms - Security
Since 2005 the NIST no longer considers DES and 3DES cipher algorithms secure (3DES was fully withdrawn at the end of 2023). RC2 shares the 64-bit block size that makes them vulnerable to the
Sweet32 birthday attack.
Rijndael /
RijndaelManaged are obsolete in .NET (SYSLIB0022): they only add non-standard block sizes to AES and are not FIPS validated. Sources:
https://www.nist.gov/news-events/news/2005/06/nist-withdraws-outdated-data-encryption-standard https://sweet32.info/
This is CWE-327
Use of a Broken or Risky Cryptographic Algorithm.
Flagged Codestatic byte[] Encrypt(byte[] plain, byte[] key, byte[] iv) {
using var cipher = TripleDES.Create(); // flagged: 3DES
cipher.Key = key;
cipher.IV = iv;
using var encryptor = cipher.CreateEncryptor();
return encryptor.TransformFinalBlock(plain, 0, plain.Length);
}
Fixed Codestatic byte[] Encrypt(byte[] plain, byte[] key, byte[] iv) {
using var cipher = Aes.Create(); // AES (or AesGcm)
cipher.Key = key;
cipher.IV = iv;
using var encryptor = cipher.CreateEncryptor();
return encryptor.TransformFinalBlock(plain, 0, plain.Length);
}
How to Fix:Use AES via
System.Security.Cryptography.Aes.Create() (or the authenticated
AesGcm which also detects tampering). Suppress issues when DES/3DES/RC2 is kept for legacy data compatibility.
// <Name>Don't use DES/3DES/RC2 weak cipher algorithms</Name>
// <Id>ND3102:DontUseDES3DESWeakCipherAlgorithms</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
let weakCipherTypes = ThirdParty.Types.WithFullNameIn(
"System.Security.Cryptography.DES",
"System.Security.Cryptography.DESCryptoServiceProvider",
"System.Security.Cryptography.TripleDES",
"System.Security.Cryptography.TripleDESCryptoServiceProvider",
"System.Security.Cryptography.TripleDESCng",
"System.Security.Cryptography.RC2",
"System.Security.Cryptography.RC2CryptoServiceProvider",
// Rijndael is obsolete (SYSLIB0022): AES with non-standard block sizes, not FIPS validated
"System.Security.Cryptography.Rijndael",
"System.Security.Cryptography.RijndaelManaged")
// Constructors and factories: new DESCryptoServiceProvider(), TripleDES.Create(), RC2.Create("RC2")
let weakCipherMembers = weakCipherTypes.ChildMethods()
.Where(m => m.IsConstructor || (m.IsStatic && m.SimpleName == "Create"))
.ToHashSetEx()
where weakCipherMembers.Any()
from m in Application.Methods.UsingAny(weakCipherMembers)
let weakCipherMembersUsed = m.MethodsCalled.Intersect(weakCipherMembers).ToArray()
let onlyRijndael = weakCipherMembersUsed.All(mc => mc.ParentType.SimpleName.StartsWith("Rijndael"))
select new {
m,
weakCipherMembersUsed,
Debt = 1.ToHours().ToDebt(),
Severity = onlyRijndael ? Severity.Medium : Severity.High
}
//<Expl>
// The *{0}* uses the weak or obsolete cipher algorithm(s) *{1}*.
// Use *System.Security.Cryptography.Aes* instead.
//</Expl>
//<Desc>
// Since 2005 the NIST no longer considers DES and 3DES cipher algorithms
// secure (3DES was fully withdrawn at the end of 2023). RC2 shares the
// 64-bit block size that makes them vulnerable to the *Sweet32* birthday
// attack. *Rijndael* / *RijndaelManaged* are obsolete in .NET (SYSLIB0022):
// they only add non-standard block sizes to AES and are not FIPS validated.
// Sources:
// https://www.nist.gov/news-events/news/2005/06/nist-withdraws-outdated-data-encryption-standard
// https://sweet32.info/
//
// This is CWE-327 *Use of a Broken or Risky Cryptographic Algorithm*.
//
// **Flagged Code**
// ```csharp
// static byte[] Encrypt(byte[] plain, byte[] key, byte[] iv) {
// using var cipher = TripleDES.Create(); // flagged: 3DES
// cipher.Key = key;
// cipher.IV = iv;
// using var encryptor = cipher.CreateEncryptor();
// return encryptor.TransformFinalBlock(plain, 0, plain.Length);
// }
// ```
//
// **Fixed Code**
// ```csharp
// static byte[] Encrypt(byte[] plain, byte[] key, byte[] iv) {
// using var cipher = Aes.Create(); // AES (or AesGcm)
// cipher.Key = key;
// cipher.IV = iv;
// using var encryptor = cipher.CreateEncryptor();
// return encryptor.TransformFinalBlock(plain, 0, plain.Length);
// }
// ```
//</Desc>
//<HowToFix>
// Use AES via *System.Security.Cryptography.Aes.Create()* (or the
// authenticated *AesGcm* which also detects tampering).
// Suppress issues when DES/3DES/RC2 is kept for legacy data compatibility.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't use weak password-based key derivation - Security
Matches methods creating
PasswordDeriveBytes (PBKDF1, obsolete SYSLIB0041) or
Rfc2898DeriveBytes through a constructor without a
HashAlgorithmName parameter: such constructors silently use SHA1 and default to 1000 iterations, orders of magnitude below the OWASP 2023 recommendation (600.000 iterations for PBKDF2-HMAC-SHA256).
This is CWE-916
Use of Password Hash With Insufficient Computational Effort.
Flagged Codestatic byte[] DeriveKey(string password, byte[] salt) {
using var kdf = new Rfc2898DeriveBytes(password, salt, 1000); // flagged: SHA1 default
return kdf.GetBytes(32);
}
static byte[] LegacyKey(string password, byte[] salt) =>
new PasswordDeriveBytes(password, salt).GetBytes(20); // flagged: PBKDF1
Fixed Codestatic byte[] DeriveKey(string password, byte[] salt) =>
Rfc2898DeriveBytes.Pbkdf2(password, salt, 600_000, HashAlgorithmName.SHA256, 32);
// Or the constructor overload that names the hash algorithm explicitly:
static byte[] DeriveKeyCtor(string password, byte[] salt) {
using var kdf = new Rfc2898DeriveBytes(password, salt, 600_000, HashAlgorithmName.SHA256);
return kdf.GetBytes(32);
}
How to Fix:Use
Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA256, length) (or the constructor overload taking a
HashAlgorithmName) with an explicit high iteration count, or a memory-hard function such as
Argon2id or
scrypt from a maintained library. Store the iteration count with the hash so it can be raised over time.
// <Name>Don't use weak password-based key derivation</Name>
// <Id>ND3123:DontUseWeakPasswordBasedKeyDerivation</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
let weakKdfMembers =
// PBKDF1 (PasswordDeriveBytes) is obsolete (SYSLIB0041): it produces at most 20 bytes and is not a standard KDF
ThirdParty.Types.WithFullName("System.Security.Cryptography.PasswordDeriveBytes")
.ChildMethods().Where(m => m.IsConstructor)
// Rfc2898DeriveBytes constructors without a HashAlgorithmName parameter default to SHA1 (and 1000 iterations)
.Concat(ThirdParty.Types.WithFullName("System.Security.Cryptography.Rfc2898DeriveBytes")
.ChildMethods().Where(m => m.IsConstructor && !m.Name.Contains("HashAlgorithmName")))
.ToHashSetEx()
where weakKdfMembers.Any()
from m in Application.Methods.UsingAny(weakKdfMembers)
let weakKdfMembersUsed = m.MethodsCalled.Intersect(weakKdfMembers).ToArray()
select new {
m,
weakKdfMembersUsed,
Debt = 30.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* derives a key from a password with *{1}*, which relies on
// PBKDF1 or on PBKDF2-SHA1 with a default iteration count.
//</Expl>
//<Desc>
// Matches methods creating *PasswordDeriveBytes* (PBKDF1, obsolete
// SYSLIB0041) or *Rfc2898DeriveBytes* through a constructor without a
// *HashAlgorithmName* parameter: such constructors silently use SHA1 and
// default to 1000 iterations, orders of magnitude below the OWASP 2023
// recommendation (600.000 iterations for PBKDF2-HMAC-SHA256).
//
// This is CWE-916 *Use of Password Hash With Insufficient Computational
// Effort*.
//
// **Flagged Code**
// ```csharp
// static byte[] DeriveKey(string password, byte[] salt) {
// using var kdf = new Rfc2898DeriveBytes(password, salt, 1000); // flagged: SHA1 default
// return kdf.GetBytes(32);
// }
// static byte[] LegacyKey(string password, byte[] salt) =>
// new PasswordDeriveBytes(password, salt).GetBytes(20); // flagged: PBKDF1
// ```
//
// **Fixed Code**
// ```csharp
// static byte[] DeriveKey(string password, byte[] salt) =>
// Rfc2898DeriveBytes.Pbkdf2(password, salt, 600_000, HashAlgorithmName.SHA256, 32);
// // Or the constructor overload that names the hash algorithm explicitly:
// static byte[] DeriveKeyCtor(string password, byte[] salt) {
// using var kdf = new Rfc2898DeriveBytes(password, salt, 600_000, HashAlgorithmName.SHA256);
// return kdf.GetBytes(32);
// }
// ```
//</Desc>
//<HowToFix>
// Use *Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA256, length)*
// (or the constructor overload taking a *HashAlgorithmName*) with an
// explicit high iteration count, or a memory-hard function such as
// *Argon2id* or *scrypt* from a maintained library. Store the iteration
// count with the hash so it can be raised over time.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't use the ECB encryption mode - Security
Matches methods that call the one-shot ECB APIs of
SymmetricAlgorithm (
Aes,
TripleDES,
DES,
RC2):
EncryptEcb(),
DecryptEcb(),
TryEncryptEcb(),
TryDecryptEcb() and
GetCiphertextLengthEcb().
In Electronic Codebook (ECB) mode every block is encrypted independently with the same key, hence two identical plaintext blocks produce two identical ciphertext blocks. An attacker can spot repeated structures, swap or replay blocks, and in many cases recover the content without the key. This is CWE-327
Use of a Broken or Risky Cryptographic Algorithm (OWASP Top 10 A02 Cryptographic Failures).
Assigning
CipherMode.ECB to the
Mode property cannot be detected because enum values are not visible in the compiled code.
Flagged Codestatic byte[] Encrypt(byte[] plaintext, byte[] key) {
using var aes = Aes.Create();
aes.Key = key;
return aes.EncryptEcb(plaintext, PaddingMode.PKCS7); // flagged: ECB
}
Fixed Codestatic byte[] Encrypt(byte[] plaintext, byte[] key, byte[] nonce, out byte[] tag) {
using var aes = new AesGcm(key, AesGcm.TagByteSizes.MaxSize);
var ciphertext = new byte[plaintext.Length];
tag = new byte[AesGcm.TagByteSizes.MaxSize];
aes.Encrypt(nonce, plaintext, ciphertext, tag); // authenticated encryption
return ciphertext;
}
How to Fix:Use an authenticated encryption mode:
AesGcm or
AesCcm with a unique nonce per message. When authenticated encryption is not available, use
EncryptCbc() with a random IV per message and protect the ciphertext with an HMAC (encrypt-then-MAC).
Never reuse a nonce or an IV with the same key.
// <Name>Don't use the ECB encryption mode</Name>
// <Id>ND3132:DontUseEcbEncryptionMode</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
// Since .NET 6 the one-shot APIs carry the mode in their name: EncryptEcb(), DecryptEcb(),
// TryEncryptEcb(), TryDecryptEcb(), GetCiphertextLengthEcb().
// Setting SymmetricAlgorithm.Mode = CipherMode.ECB is not detectable: enum values are not visible in IL.
let ecbMembers = ThirdParty.Methods.Where(m =>
m.ParentType.FullName.StartsWith("System.Security.Cryptography.") &&
m.SimpleNameEqualsAny("EncryptEcb", "DecryptEcb", "TryEncryptEcb", "TryDecryptEcb", "GetCiphertextLengthEcb"))
.ToHashSetEx()
where ecbMembers.Any()
from m in Application.Methods.UsingAny(ecbMembers)
let ecbMembersUsed = m.MethodsCalled.Intersect(ecbMembers).ToArray()
select new {
m,
ecbMembersUsed,
Debt = 20.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The method *{0}* calls *{1}*, which encrypts or decrypts with the ECB mode.
// ECB encrypts identical plaintext blocks into identical ciphertext blocks,
// so patterns of the plaintext remain visible in the ciphertext.
//</Expl>
// <Desc>
// Matches methods that call the one-shot ECB APIs of *SymmetricAlgorithm*
// (*Aes*, *TripleDES*, *DES*, *RC2*): *EncryptEcb()*, *DecryptEcb()*,
// *TryEncryptEcb()*, *TryDecryptEcb()* and *GetCiphertextLengthEcb()*.
//
// In Electronic Codebook (ECB) mode every block is encrypted independently
// with the same key, hence two identical plaintext blocks produce two
// identical ciphertext blocks. An attacker can spot repeated structures,
// swap or replay blocks, and in many cases recover the content without
// the key. This is CWE-327 *Use of a Broken or Risky Cryptographic
// Algorithm* (OWASP Top 10 A02 Cryptographic Failures).
//
// Assigning *CipherMode.ECB* to the *Mode* property cannot be detected
// because enum values are not visible in the compiled code.
//
// **Flagged Code**
// ```csharp
// static byte[] Encrypt(byte[] plaintext, byte[] key) {
// using var aes = Aes.Create();
// aes.Key = key;
// return aes.EncryptEcb(plaintext, PaddingMode.PKCS7); // flagged: ECB
// }
// ```
//
// **Fixed Code**
// ```csharp
// static byte[] Encrypt(byte[] plaintext, byte[] key, byte[] nonce, out byte[] tag) {
// using var aes = new AesGcm(key, AesGcm.TagByteSizes.MaxSize);
// var ciphertext = new byte[plaintext.Length];
// tag = new byte[AesGcm.TagByteSizes.MaxSize];
// aes.Encrypt(nonce, plaintext, ciphertext, tag); // authenticated encryption
// return ciphertext;
// }
// ```
// </Desc>
// <HowToFix>
// Use an authenticated encryption mode: *AesGcm* or *AesCcm* with a unique
// nonce per message. When authenticated encryption is not available, use
// *EncryptCbc()* with a random IV per message and protect the ciphertext
// with an HMAC (encrypt-then-MAC).
//
// Never reuse a nonce or an IV with the same key.
// </HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't disable certificate validation - Security
Matched methods replace the certificate validation of a TLS client or server:
ServicePointManager.ServerCertificateValidationCallback,
HttpClientHandler.ServerCertificateCustomValidationCallback (or its
DangerousAcceptAnyServerCertificateValidator),
SslClientAuthenticationOptions.RemoteCertificateValidationCallback (used by
SocketsHttpHandler), an
SslStream constructor taking a callback, the obsolete
ServicePointManager.CertificatePolicy, or Kestrel's
ClientCertificateValidation. These callbacks are mostly written to accept a self-signed or expired certificate, which means trusting any certificate: a
vulnerability to man-in-the-middle attacks https://en.wikipedia.org/wiki/Man-in-the-middle_attack
This is CWE-295
Improper Certificate Validation.
Flagged Codepublic HttpClient CreateClient() {
var handler = new HttpClientHandler();
// flagged: any certificate is trusted, a man-in-the-middle goes unnoticed
handler.ServerCertificateCustomValidationCallback = (msg, cert, chain, errors) => true;
return new HttpClient(handler);
}
Fixed Codepublic HttpClient CreateClient() {
// The issuing CA is installed in the machine trust store: the default
// validation checks the whole chain, no callback is needed.
var handler = new HttpClientHandler();
return new HttpClient(handler);
}
How to Fix:Install the issuing CA in the machine trust store, or pin the expected certificate thumbprint inside the callback instead of returning true. Never ship
DangerousAcceptAnyServerCertificateValidator outside a development build. Suppress issues for legitimate custom validation procedures that still verify the chain.
// <Name>Don't disable certificate validation</Name>
// <Id>ND3103:DontDisableCertificateValidation</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
let certificateValidationOverrides = ThirdParty.Methods.Where(m =>
m.ParentNamespace.Name.StartsWithAny("System.Net", "Microsoft.AspNetCore") &&
(m.SimpleNameEqualsAny(
"set_ServerCertificateValidationCallback", // ServicePointManager, HttpWebRequest, WebRequestHandler
"set_ServerCertificateCustomValidationCallback", // HttpClientHandler
"get_DangerousAcceptAnyServerCertificateValidator",// HttpClientHandler
"set_RemoteCertificateValidationCallback", // SslClientAuthenticationOptions, SslServerAuthenticationOptions (SocketsHttpHandler.SslOptions)
"set_CertificatePolicy", // ServicePointManager (obsolete ICertificatePolicy)
"set_ClientCertificateValidation") || // Kestrel HttpsConnectionAdapterOptions
// new SslStream(stream, leaveOpen, RemoteCertificateValidationCallback)
(m.IsConstructor && m.ParentType.FullName == "System.Net.Security.SslStream" && m.Name.Contains("RemoteCertificateValidationCallback"))))
.ToHashSetEx()
where certificateValidationOverrides.Any()
from m in Application.Methods.UsingAny(certificateValidationOverrides)
let certificateValidationOverridesUsed = m.MethodsCalled.Intersect(certificateValidationOverrides).ToArray()
select new {
m,
certificateValidationOverridesUsed,
Debt = 30.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* overrides certificate validation through *{1}*.
// A callback that returns true (or *DangerousAcceptAnyServerCertificateValidator*)
// makes the client trust any certificate, opening the door to *man-in-the-middle attacks*.
//</Expl>
//<Desc>
// Matched methods replace the certificate validation of a TLS client or
// server: *ServicePointManager.ServerCertificateValidationCallback*,
// *HttpClientHandler.ServerCertificateCustomValidationCallback* (or its
// *DangerousAcceptAnyServerCertificateValidator*),
// *SslClientAuthenticationOptions.RemoteCertificateValidationCallback*
// (used by *SocketsHttpHandler*), an *SslStream* constructor taking a
// callback, the obsolete *ServicePointManager.CertificatePolicy*, or
// Kestrel's *ClientCertificateValidation*. These callbacks are mostly
// written to accept a self-signed or expired certificate, which means
// trusting any certificate: a **vulnerability to man-in-the-middle
// attacks** https://en.wikipedia.org/wiki/Man-in-the-middle_attack
//
// This is CWE-295 *Improper Certificate Validation*.
//
// **Flagged Code**
// ```csharp
// public HttpClient CreateClient() {
// var handler = new HttpClientHandler();
// // flagged: any certificate is trusted, a man-in-the-middle goes unnoticed
// handler.ServerCertificateCustomValidationCallback = (msg, cert, chain, errors) => true;
// return new HttpClient(handler);
// }
// ```
//
// **Fixed Code**
// ```csharp
// public HttpClient CreateClient() {
// // The issuing CA is installed in the machine trust store: the default
// // validation checks the whole chain, no callback is needed.
// var handler = new HttpClientHandler();
// return new HttpClient(handler);
// }
// ```
//</Desc>
//<HowToFix>
// Install the issuing CA in the machine trust store, or pin the expected
// certificate thumbprint inside the callback instead of returning true.
// Never ship *DangerousAcceptAnyServerCertificateValidator* outside a
// development build. Suppress issues for legitimate custom validation
// procedures that still verify the chain.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't hard-code TLS/SSL protocol versions - Security
Matches methods setting
ServicePointManager.SecurityProtocol,
HttpClientHandler.SslProtocols,
SslClientAuthenticationOptions.EnabledSslProtocols, Kestrel
HttpsConnectionAdapterOptions.SslProtocols, or calling an
SslStream.Authenticate... overload with an explicit
SslProtocols. Microsoft's guidance since .NET Framework 4.7 / .NET Core is to leave the protocol at
SystemDefault /
None so that the operating system picks the strongest version available and drops SSL 3.0, TLS 1.0 and TLS 1.1 as they are retired:
https://learn.microsoft.com/dotnet/framework/network-programming/tls
This is CWE-326
Inadequate Encryption Strength when the pinned list includes an obsolete version.
Flagged Codepublic HttpClient CreateClient() {
// flagged: freezes the application on TLS 1.2 and blocks TLS 1.3
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
var handler = new HttpClientHandler { SslProtocols = SslProtocols.Tls12 }; // flagged
return new HttpClient(handler);
}
Fixed Codepublic HttpClient CreateClient() {
// Target .NET Framework 4.7+ or .NET Core and leave the protocol to the OS
// (SystemDefault): the strongest version is negotiated, retired ones drop out.
return new HttpClient(new HttpClientHandler());
}
How to Fix:Remove the assignment (or set
SecurityProtocolType.SystemDefault /
SslProtocols.None) and target .NET Framework 4.7+ or .NET Core, so the OS controls protocol selection. If a peer requires a specific version, isolate that configuration and suppress the issue with a justification.
// <Name>Don't hard-code TLS/SSL protocol versions</Name>
// <Id>ND3122:DontHardCodeTlsSslProtocolVersions</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
// The enum value assigned is not visible in IL, but pinning any version is discouraged:
// the OS negotiates the best protocol when the setting is left to SystemDefault.
let protocolPinningMembers = ThirdParty.Methods.Where(m =>
(m.SimpleName == "set_SecurityProtocol" && m.ParentType.FullName == "System.Net.ServicePointManager") ||
(m.SimpleName == "set_SslProtocols" && m.ParentType.FullNameEqualsAny("System.Net.Http.HttpClientHandler", "System.Net.Http.WebRequestHandler", "Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions")) ||
(m.SimpleName == "set_EnabledSslProtocols" && m.ParentType.FullNameEqualsAny("System.Net.Security.SslClientAuthenticationOptions", "System.Net.Security.SslServerAuthenticationOptions")) ||
// SslStream.AuthenticateAsClient(host, certs, SslProtocols, checkRevocation) and its Server / Async siblings
(m.ParentType.FullName == "System.Net.Security.SslStream" && m.SimpleName.StartsWith("Authenticate") && m.Name.Contains("SslProtocols")))
.ToHashSetEx()
where protocolPinningMembers.Any()
from m in Application.Methods.UsingAny(protocolPinningMembers)
let protocolPinningMembersUsed = m.MethodsCalled.Intersect(protocolPinningMembers).ToArray()
select new {
m,
protocolPinningMembersUsed,
Debt = 15.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* pins the TLS/SSL protocol version(s) through *{1}*.
// A hard-coded version list freezes the application on today's protocols:
// it keeps deprecated ones enabled after they are broken and blocks newer ones.
//</Expl>
//<Desc>
// Matches methods setting *ServicePointManager.SecurityProtocol*,
// *HttpClientHandler.SslProtocols*, *SslClientAuthenticationOptions.EnabledSslProtocols*,
// Kestrel *HttpsConnectionAdapterOptions.SslProtocols*, or calling an
// *SslStream.Authenticate...* overload with an explicit *SslProtocols*.
// Microsoft's guidance since .NET Framework 4.7 / .NET Core is to leave
// the protocol at *SystemDefault* / *None* so that the operating system
// picks the strongest version available and drops SSL 3.0, TLS 1.0 and
// TLS 1.1 as they are retired:
// https://learn.microsoft.com/dotnet/framework/network-programming/tls
//
// This is CWE-326 *Inadequate Encryption Strength* when the pinned list
// includes an obsolete version.
//
// **Flagged Code**
// ```csharp
// public HttpClient CreateClient() {
// // flagged: freezes the application on TLS 1.2 and blocks TLS 1.3
// ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
// var handler = new HttpClientHandler { SslProtocols = SslProtocols.Tls12 }; // flagged
// return new HttpClient(handler);
// }
// ```
//
// **Fixed Code**
// ```csharp
// public HttpClient CreateClient() {
// // Target .NET Framework 4.7+ or .NET Core and leave the protocol to the OS
// // (SystemDefault): the strongest version is negotiated, retired ones drop out.
// return new HttpClient(new HttpClientHandler());
// }
// ```
//</Desc>
//<HowToFix>
// Remove the assignment (or set *SecurityProtocolType.SystemDefault* /
// *SslProtocols.None*) and target .NET Framework 4.7+ or .NET Core, so
// the OS controls protocol selection. If a peer requires a specific
// version, isolate that configuration and suppress the issue with a
// justification.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review commands vulnerable to SQL injection - Security
Matches methods creating a DB command (like
SqlCommand) that call an
Execute method without using command parameters: prone to
SQL Injection https://en.wikipedia.org/wiki/SQL_injection since malicious SQL might be injected in string parameter values.
False positives are possible; review matches and suppress issues when needed. To limit them, the rule also checks command parameters access from sub method calls (a solid indication of non-vulnerability).
Flagged Codepublic int CountUsers(SqlConnection connection, string name) {
var sql = "SELECT COUNT(*) FROM Users WHERE Name = '" + name + "'";
using var command = new SqlCommand(sql, connection); // flagged: no command parameter
return (int)command.ExecuteScalar();
}
Fixed Codepublic int CountUsers(SqlConnection connection, string name) {
const string sql = "SELECT COUNT(*) FROM Users WHERE Name = @name";
using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@name", name); // parameterized: not matched
return (int)command.ExecuteScalar();
}
How to Fix:If the method indeed injects parameter values in the SQL string, use
command.Parameters.Add(...) instead. More on explicit parameters:
https://stackoverflow.com/questions/4892166/how-does-sqlparameter-prevent-sql-injection
// <Name>Review commands vulnerable to SQL injection</Name>
// <Id>ND3107:ReviewCommandsVulnerableToSQLInjection</Id>
warnif count > 0
let commands = ThirdParty.Types.WithFullNameIn(
"System.Data.Common.DbCommand",
"Microsoft.Data.SqlClient.SqlCommand",
"System.Data.SqlClient.SqlCommand",
"System.Data.OleDb.OleDbCommand",
"System.Data.Odbc.OdbcCommand",
"System.Data.OracleClient.OracleCommand")
where commands.Any()
let commandCtors = commands.ChildMethods().Where(m => m.IsConstructor).ToHashSetEx()
let commandExecutes = commands.ChildMethods().Where(m => m.SimpleName.Contains("Execute")).ToHashSetEx()
let commandParameters = commands.ChildMethods().Where(m => m.IsPropertyGetter && m.SimpleNameEquals("get_Parameters")).ToHashSetEx()
from m in Application.Methods.UsingAny(commandCtors)
.UsingAny(commandExecutes)
where !m.MethodsCalled.Intersect(commandParameters).Any() &&
// Check also that non of the method call rely on command parameters to get less false positives:
!m.MethodsCalled.SelectMany(mc => mc.MethodsCalled).Intersect(commandParameters).Any()
select new { m,
createA = m.MethodsCalled.Intersect(commandCtors).First().ParentType,
calls = m.MethodsCalled.Intersect(commandExecutes).First(),
Debt = 15.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* creates an instance of the *{1}* and calls the *{2}*.
// This is susceptible to *SQL Injection* as malicious SQL code
// can be injected into string parameter values.
// Instead, use *command.Parameters.Add(...)*.
//</Expl>
// <Desc>
// Matches methods creating a DB command (like *SqlCommand*) that call an
// *Execute* method without using command parameters: prone to
// **SQL Injection** https://en.wikipedia.org/wiki/SQL_injection since
// malicious SQL might be injected in string parameter values.
//
// False positives are possible; review matches and suppress issues when
// needed. To limit them, the rule also checks command parameters access
// from sub method calls (a solid indication of non-vulnerability).
//
// **Flagged Code**
// ```csharp
// public int CountUsers(SqlConnection connection, string name) {
// var sql = "SELECT COUNT(*) FROM Users WHERE Name = '" + name + "'";
// using var command = new SqlCommand(sql, connection); // flagged: no command parameter
// return (int)command.ExecuteScalar();
// }
// ```
//
// **Fixed Code**
// ```csharp
// public int CountUsers(SqlConnection connection, string name) {
// const string sql = "SELECT COUNT(*) FROM Users WHERE Name = @name";
// using var command = new SqlCommand(sql, connection);
// command.Parameters.AddWithValue("@name", name); // parameterized: not matched
// return (int)command.ExecuteScalar();
// }
// ```
// </Desc>
// <HowToFix>
// If the method indeed injects parameter values in the SQL string, use
// **command.Parameters.Add(...)** instead. More on explicit parameters:
// https://stackoverflow.com/questions/4892166/how-does-sqlparameter-prevent-sql-injection
// </HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review data adapters vulnerable to SQL injection - Security
Matches methods creating a DB adapter (like
SqlDataAdapter) that call
Fill() without using DB command parameters: prone to
SQL Injection https://en.wikipedia.org/wiki/SQL_injection since malicious SQL might be injected in string parameter values.
False positives are possible; review matches and suppress issues when needed. To limit them, the rule also checks command parameters access from sub method calls (a solid indication of non-vulnerability).
Flagged Codepublic DataTable LoadOrders(SqlConnection connection, string customerId) {
var sql = "SELECT * FROM Orders WHERE CustomerId = '" + customerId + "'";
using var adapter = new SqlDataAdapter(sql, connection); // flagged: no command parameter
var table = new DataTable();
adapter.Fill(table);
return table;
}
Fixed Codepublic DataTable LoadOrders(SqlConnection connection, string customerId) {
const string sql = "SELECT * FROM Orders WHERE CustomerId = @customerId";
using var adapter = new SqlDataAdapter(sql, connection);
adapter.SelectCommand.Parameters.AddWithValue("@customerId", customerId); // parameterized
var table = new DataTable();
adapter.Fill(table);
return table;
}
How to Fix:If the method indeed injects parameter values in the SQL string, use
adapter.SelectCommand.Parameters.Add(...) instead (or
adapter.UpdateCommand /
adapter.InsertCommand, depending on context). More on explicit parameters:
https://stackoverflow.com/questions/4892166/how-does-sqlparameter-prevent-sql-injection
// <Name>Review data adapters vulnerable to SQL injection</Name>
// <Id>ND3108:ReviewDataAdaptersVulnerableToSQLInjection</Id>
warnif count > 0
let adapters = ThirdParty.Types.WithFullNameIn(
"System.Data.Common.DataAdapter",
"System.Data.Common.DbDataAdapter",
"Microsoft.Data.SqlClient.SqlDataAdapter",
"System.Data.SqlClient.SqlDataAdapter",
"System.Data.OleDb.OleDbDataAdapter",
"System.Data.Odbc.OdbcDataAdapter",
"System.Data.OracleClient.OracleDataAdapter")
where adapters.Any()
let adapterCtors = adapters.ChildMethods().Where(m => m.IsConstructor).ToHashSetEx()
let adapterFill = adapters.ChildMethods().Where(m => m.SimpleName.Contains("Fill")).ToHashSetEx()
let commands = ThirdParty.Types.WithFullNameIn(
"System.Data.Common.DbCommand",
"Microsoft.Data.SqlClient.SqlCommand",
"System.Data.SqlClient.SqlCommand",
"System.Data.OleDb.OleDbCommand",
"System.Data.Odbc.OdbcCommand",
"System.Data.OracleClient.OracleCommand")
where commands.Any()
let commandParameters = commands.ChildMethods().Where(m => m.IsPropertyGetter && m.SimpleNameEquals("get_Parameters")).ToHashSetEx()
from m in Application.Methods.UsingAny(adapterCtors)
.UsingAny(adapterFill)
where !m.MethodsCalled.Intersect(commandParameters).Any() &&
// Check also that non of the method call rely on command parameters to get less false positives:
!m.MethodsCalled.SelectMany(mc => mc.MethodsCalled).Intersect(commandParameters).Any()
select new { m,
createA = m.MethodsCalled.Intersect(adapterCtors).First().ParentType,
calls = m.MethodsCalled.Intersect(adapterFill).First(),
Debt = 15.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* creates an instance of *{1}* and calls the *{2}*.
// This is susceptible to *SQL Injection* as malicious SQL code
// can be injected into string parameter values.
// Instead, use *adapter.SelectCommand.Parameters.Add(...)*.
//</Expl>
// <Desc>
// Matches methods creating a DB adapter (like *SqlDataAdapter*) that call
// *Fill()* without using DB command parameters: prone to **SQL Injection**
// https://en.wikipedia.org/wiki/SQL_injection since malicious SQL might be
// injected in string parameter values.
//
// False positives are possible; review matches and suppress issues when
// needed. To limit them, the rule also checks command parameters access
// from sub method calls (a solid indication of non-vulnerability).
//
// **Flagged Code**
// ```csharp
// public DataTable LoadOrders(SqlConnection connection, string customerId) {
// var sql = "SELECT * FROM Orders WHERE CustomerId = '" + customerId + "'";
// using var adapter = new SqlDataAdapter(sql, connection); // flagged: no command parameter
// var table = new DataTable();
// adapter.Fill(table);
// return table;
// }
// ```
//
// **Fixed Code**
// ```csharp
// public DataTable LoadOrders(SqlConnection connection, string customerId) {
// const string sql = "SELECT * FROM Orders WHERE CustomerId = @customerId";
// using var adapter = new SqlDataAdapter(sql, connection);
// adapter.SelectCommand.Parameters.AddWithValue("@customerId", customerId); // parameterized
// var table = new DataTable();
// adapter.Fill(table);
// return table;
// }
// ```
// </Desc>
// <HowToFix>
// If the method indeed injects parameter values in the SQL string, use
// **adapter.SelectCommand.Parameters.Add(...)** instead (or
// *adapter.UpdateCommand* / *adapter.InsertCommand*, depending on context).
// More on explicit parameters:
// https://stackoverflow.com/questions/4892166/how-does-sqlparameter-prevent-sql-injection
// </HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review raw SQL built from composed strings - Security
Matches methods that call an Entity Framework Core
Raw entry point (
FromSqlRaw(),
ExecuteSqlRaw(),
SqlQueryRaw()) or a Dapper
Query /
Execute member, while also building strings in the same body (
String.Concat(),
Format(),
Join(), interpolation,
StringBuilder). These members hand the SQL text to the database verbatim, so a composed fragment coming from a request, a file name or a configuration value is executed as SQL:
SQL Injection https://en.wikipedia.org/wiki/SQL_injection
The rules
Review commands vulnerable to SQL injection and
Review data adapters vulnerable to SQL injection only cover ADO.NET commands, which modern code rarely uses directly. This rule covers the ORM path.
False positives are possible when every composed fragment is a constant or an internally generated identifier; review matches and suppress issues when needed.
This is CWE-89
SQL Injection (OWASP Top 10 A03 Injection).
Flagged Codepublic List<Blog> FindBlogs(BloggingContext db, string name) {
var sql = $"SELECT * FROM Blogs WHERE Name = '{name}'"; // SQL composed in the method
return db.Blogs.FromSqlRaw(sql).ToList(); // flagged: the composed text runs verbatim
}
Fixed Codepublic List<Blog> FindBlogs(BloggingContext db, string name) {
// FromSql takes a FormattableString: each hole becomes a DbParameter
return db.Blogs.FromSql($"SELECT * FROM Blogs WHERE Name = {name}").ToList();
}
How to Fix:With Entity Framework Core use the interpolated siblings, which turn every hole into a
DbParameter:
FromSql($"... {value}"),
ExecuteSql($"... {value}"),
FromSqlInterpolated(...). With Dapper pass an anonymous parameter object:
connection.Query<T>("... WHERE Id = @id", new { id }).
A table or column name cannot be parameterized: validate it against a whitelist of known names before interpolating it.
// <Name>Review raw SQL built from composed strings</Name>
// <Id>ND3128:ReviewRawSQLBuiltFromComposedStrings</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
// Only the *Raw* / Dapper entry points take a SQL string the caller assembled.
// The interpolated EF Core siblings (FromSql, FromSqlInterpolated, ExecuteSqlInterpolated)
// turn every hole into a DbParameter, so they are deliberately absent from this list.
let rawSqlMembers = ThirdParty.Methods.Where(m =>
m.SimpleNameEqualsAny(
"FromSqlRaw", "ExecuteSqlRaw", "ExecuteSqlRawAsync", "SqlQueryRaw",
"ExecuteSqlCommand", "ExecuteSqlCommandAsync", "SqlQuery") ||
// Dapper: Query/Execute overloads all take the SQL as a plain string
(m.ParentType.FullName.StartsWith("Dapper.") && m.SimpleName.StartsWithAny("Query", "Execute")))
.ToHashSetEx()
where rawSqlMembers.Any()
// Concatenation, formatting, interpolation or a builder: the SQL is assembled in the method.
let stringBuildingMembers = ThirdParty.Methods.Where(m =>
(m.ParentType.FullName == "System.String" && m.SimpleNameEqualsAny("Concat", "Format", "Join", "Replace", "Insert")) ||
m.ParentType.FullNameEqualsAny("System.Text.StringBuilder", "System.Runtime.CompilerServices.DefaultInterpolatedStringHandler"))
.ToHashSetEx()
from m in Application.Methods.UsingAny(rawSqlMembers)
where m.MethodsCalled.Intersect(stringBuildingMembers).Any()
select new {
m,
rawSqlMembersUsed = m.MethodsCalled.Intersect(rawSqlMembers).ToArray(),
Debt = 15.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* assembles a SQL string and passes it to *{1}*.
// If any fragment derives from user input, malicious SQL is executed:
// use parameters or the interpolated overloads instead.
//</Expl>
// <Desc>
// Matches methods that call an Entity Framework Core *Raw* entry point
// (*FromSqlRaw()*, *ExecuteSqlRaw()*, *SqlQueryRaw()*) or a Dapper
// *Query* / *Execute* member, while also building strings in the same body
// (*String.Concat()*, *Format()*, *Join()*, interpolation, *StringBuilder*).
// These members hand the SQL text to the database verbatim, so a composed
// fragment coming from a request, a file name or a configuration value is
// executed as SQL: **SQL Injection**
// https://en.wikipedia.org/wiki/SQL_injection
//
// The rules *Review commands vulnerable to SQL injection* and *Review data
// adapters vulnerable to SQL injection* only cover ADO.NET commands, which
// modern code rarely uses directly. This rule covers the ORM path.
//
// False positives are possible when every composed fragment is a constant
// or an internally generated identifier; review matches and suppress
// issues when needed.
//
// This is CWE-89 *SQL Injection* (OWASP Top 10 A03 Injection).
//
// **Flagged Code**
// ```csharp
// public List<Blog> FindBlogs(BloggingContext db, string name) {
// var sql = $"SELECT * FROM Blogs WHERE Name = '{name}'"; // SQL composed in the method
// return db.Blogs.FromSqlRaw(sql).ToList(); // flagged: the composed text runs verbatim
// }
// ```
//
// **Fixed Code**
// ```csharp
// public List<Blog> FindBlogs(BloggingContext db, string name) {
// // FromSql takes a FormattableString: each hole becomes a DbParameter
// return db.Blogs.FromSql($"SELECT * FROM Blogs WHERE Name = {name}").ToList();
// }
// ```
// </Desc>
// <HowToFix>
// With Entity Framework Core use the interpolated siblings, which turn
// every hole into a *DbParameter*: *FromSql($"... {value}")*,
// *ExecuteSql($"... {value}")*, *FromSqlInterpolated(...)*. With Dapper
// pass an anonymous parameter object:
// **connection.Query<T>("... WHERE Id = @id", new { id })**.
//
// A table or column name cannot be parameterized: validate it against a
// whitelist of known names before interpolating it.
// </HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review LDAP filters composed from strings - Security
Matches methods that assign
DirectorySearcher.Filter, construct a
DirectorySearcher, a
SearchRequest or a
DirectoryEntry from a string, or call
LdapConnection.Search(), while also assembling a string in the same method (
String.Concat(),
String.Format(), interpolation,
StringBuilder).
LDAP filters have their own syntax: a value containing
)(uid=)
or \
) closes the intended clause and opens another, which turns a login lookup into a query that matches every user, or bypasses the authentication altogether. This is
LDAP injection, CWE-90 (OWASP Top 10 A03 Injection).
Flagged Codestatic SearchResult FindUser(DirectoryEntry root, string login) {
var searcher = new DirectorySearcher(root);
searcher.Filter = "(&(objectClass=user)(sAMAccountName=" + login + "))"; // flagged
return searcher.FindOne();
}
Fixed Codestatic SearchResult FindUser(DirectoryEntry root, string login) {
var searcher = new DirectorySearcher(root);
searcher.Filter = LdapFilter.ForUser(login); // escaped and composed in one reviewed helper
return searcher.FindOne();
}
static class LdapFilter {
public static string ForUser(string login) =>
"(&(objectClass=user)(sAMAccountName=" + Escape(login) + "))";
static string Escape(string v) => // RFC 4515: escape \ * ( ) and NUL
v.Replace(@"\", @"\5c").Replace("*", @"\2a").Replace("(", @"\28")
.Replace(")", @"\29").Replace("\0", @"\00");
}
How to Fix:Escape every dynamic value inserted in a filter as RFC 4515 requires (the characters backslash, parenthesis, star and NUL become
\5c,
\28,
\29,
\2a,
\00), and escape values inserted in a distinguished name as RFC 4514 requires. Validate the value against the expected shape first (a login is alphanumeric, an email has one
@).
Keep the escaping and the composition in one dedicated helper, as in the fixed code: the method that assigns the filter no longer assembles strings itself, and the rule cannot tell an escaped fragment from a raw one.
// <Name>Review LDAP filters composed from strings</Name>
// <Id>ND3137:ReviewLdapFiltersComposedFromStrings</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
// Members that take an LDAP filter or search base as a string.
let ldapFilterMembers = ThirdParty.Methods.Where(m =>
(m.ParentType.FullName == "System.DirectoryServices.DirectorySearcher" &&
(m.SimpleName == "set_Filter" || (m.IsConstructor && m.Name.Contains("String")))) ||
(m.ParentType.FullName == "System.DirectoryServices.Protocols.SearchRequest" &&
(m.SimpleName == "set_Filter" || (m.IsConstructor && m.Name.Contains("String")))) ||
(m.ParentType.FullName == "System.DirectoryServices.DirectoryEntry" && m.IsConstructor && m.Name.Contains("String")) ||
(m.ParentType.FullName == "Novell.Directory.Ldap.LdapConnection" && m.SimpleName.StartsWith("Search")))
.ToHashSetEx()
where ldapFilterMembers.Any()
// Concatenation, formatting, interpolation or a builder: the filter is assembled in the method.
let stringBuildingMembers = ThirdParty.Methods.Where(m =>
(m.ParentType.FullName == "System.String" && m.SimpleNameEqualsAny("Concat", "Format", "Join", "Replace", "Insert")) ||
m.ParentType.FullNameEqualsAny("System.Text.StringBuilder", "System.Runtime.CompilerServices.DefaultInterpolatedStringHandler"))
.ToHashSetEx()
from m in Application.Methods.UsingAny(ldapFilterMembers)
where m.MethodsCalled.Intersect(stringBuildingMembers).Any()
select new {
m,
ldapFilterMembersUsed = m.MethodsCalled.Intersect(ldapFilterMembers).ToArray(),
Debt = 15.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The method *{0}* assembles a string and passes it to *{1}* as an LDAP
// filter or path. If any fragment derives from user input, characters
// such as *)(|* rewrite the filter: use an escaping helper on every
// dynamic value.
//</Expl>
// <Desc>
// Matches methods that assign *DirectorySearcher.Filter*, construct a
// *DirectorySearcher*, a *SearchRequest* or a *DirectoryEntry* from a
// string, or call *LdapConnection.Search()*, while also assembling a
// string in the same method (*String.Concat()*, *String.Format()*,
// interpolation, *StringBuilder*).
//
// LDAP filters have their own syntax: a value containing *)(uid=*)* or
// *\*)* closes the intended clause and opens another, which turns a
// login lookup into a query that matches every user, or bypasses the
// authentication altogether. This is **LDAP injection**, CWE-90 (OWASP
// Top 10 A03 Injection).
//
// **Flagged Code**
// ```csharp
// static SearchResult FindUser(DirectoryEntry root, string login) {
// var searcher = new DirectorySearcher(root);
// searcher.Filter = "(&(objectClass=user)(sAMAccountName=" + login + "))"; // flagged
// return searcher.FindOne();
// }
// ```
//
// **Fixed Code**
// ```csharp
// static SearchResult FindUser(DirectoryEntry root, string login) {
// var searcher = new DirectorySearcher(root);
// searcher.Filter = LdapFilter.ForUser(login); // escaped and composed in one reviewed helper
// return searcher.FindOne();
// }
// static class LdapFilter {
// public static string ForUser(string login) =>
// "(&(objectClass=user)(sAMAccountName=" + Escape(login) + "))";
// static string Escape(string v) => // RFC 4515: escape \ * ( ) and NUL
// v.Replace(@"\", @"\5c").Replace("*", @"\2a").Replace("(", @"\28")
// .Replace(")", @"\29").Replace("\0", @"\00");
// }
// ```
// </Desc>
// <HowToFix>
// Escape every dynamic value inserted in a filter as RFC 4515 requires
// (the characters backslash, parenthesis, star and NUL become *\5c*,
// *\28*, *\29*, *\2a*, *\00*), and escape values inserted in a
// distinguished name as RFC 4514 requires. Validate the value against
// the expected shape first (a login is alphanumeric, an email has one
// *@*).
//
// Keep the escaping and the composition in one dedicated helper, as in
// the fixed code: the method that assigns the filter no longer
// assembles strings itself, and the rule cannot tell an escaped fragment
// from a raw one.
// </HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't enable external XML entity resolution (XXE) - Security
Matches methods that create an
XmlUrlResolver (or the obsolete
XmlSecureResolver), enable
XsltSettings.TrustedXslt /
EnableScript /
EnableDocumentFunction, or call the obsolete
ProhibitDtd setter. With external entity resolution enabled, a crafted document can read local files (*
file:///etc/passwd*), reach internal HTTP endpoints (SSRF) or explode memory (
billion laughs).
https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#net
This is CWE-611
Improper Restriction of XML External Entity Reference.
Flagged Codepublic XmlReader OpenUntrusted(Stream xml) {
var settings = new XmlReaderSettings {
DtdProcessing = DtdProcessing.Parse,
XmlResolver = new XmlUrlResolver() // flagged: external entities get fetched
};
return XmlReader.Create(xml, settings);
}
Fixed Codepublic XmlReader OpenUntrusted(Stream xml) {
var settings = new XmlReaderSettings {
DtdProcessing = DtdProcessing.Prohibit, // the default
XmlResolver = null
};
return XmlReader.Create(xml, settings);
}
How to Fix:Leave
XmlResolver null (the default in .NET Core and .NET Framework 4.5.2+), keep
DtdProcessing.Prohibit (or
Ignore) in
XmlReaderSettings, and use
XsltSettings.Default. If external resolution is required for trusted documents only, restrict the resolver to a whitelist and suppress the issue with a justification.
// <Name>Don't enable external XML entity resolution (XXE)</Name>
// <Id>ND3111:DontEnableExternalXmlEntityResolution</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
let xxeMembers =
// An XmlUrlResolver fetches external entities and DTDs from arbitrary URLs.
ThirdParty.Types.WithFullNameIn("System.Xml.XmlUrlResolver", "System.Xml.XmlSecureResolver")
.ChildMethods().Where(m => m.IsConstructor)
// Trusted XSLT: script blocks and document() function execute during the transform.
.Concat(ThirdParty.Types.WithFullName("System.Xml.Xsl.XsltSettings")
.ChildMethods().Where(m => m.SimpleNameEqualsAny("get_TrustedXslt", "set_EnableScript", "set_EnableDocumentFunction")))
// ProhibitDtd is obsolete; the only reason to call the setter is to set it to false.
.Concat(ThirdParty.Types.WithFullNameIn("System.Xml.XmlReaderSettings", "System.Xml.XmlTextReader")
.ChildMethods().Where(m => m.SimpleName == "set_ProhibitDtd"))
.ToHashSetEx()
where xxeMembers.Any()
from m in Application.Methods.UsingAny(xxeMembers)
let xxeMembersUsed = m.MethodsCalled.Intersect(xxeMembers).ToArray()
select new {
m,
xxeMembersUsed,
Debt = 30.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* calls *{1}*, which lets an XML document pull external
// entities or run trusted XSLT features. If the XML comes from an
// untrusted source this exposes files, internal endpoints and denial of service.
//</Expl>
//<Desc>
// Matches methods that create an *XmlUrlResolver* (or the obsolete
// *XmlSecureResolver*), enable *XsltSettings.TrustedXslt* / *EnableScript*
// / *EnableDocumentFunction*, or call the obsolete *ProhibitDtd* setter.
// With external entity resolution enabled, a crafted document can read
// local files (*file:///etc/passwd*), reach internal HTTP endpoints
// (SSRF) or explode memory (*billion laughs*).
// https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#net
//
// This is CWE-611 *Improper Restriction of XML External Entity Reference*.
//
// **Flagged Code**
// ```csharp
// public XmlReader OpenUntrusted(Stream xml) {
// var settings = new XmlReaderSettings {
// DtdProcessing = DtdProcessing.Parse,
// XmlResolver = new XmlUrlResolver() // flagged: external entities get fetched
// };
// return XmlReader.Create(xml, settings);
// }
// ```
//
// **Fixed Code**
// ```csharp
// public XmlReader OpenUntrusted(Stream xml) {
// var settings = new XmlReaderSettings {
// DtdProcessing = DtdProcessing.Prohibit, // the default
// XmlResolver = null
// };
// return XmlReader.Create(xml, settings);
// }
// ```
//</Desc>
//<HowToFix>
// Leave *XmlResolver* null (the default in .NET Core and .NET Framework
// 4.5.2+), keep *DtdProcessing.Prohibit* (or *Ignore*) in
// *XmlReaderSettings*, and use *XsltSettings.Default*. If external
// resolution is required for trusted documents only, restrict the resolver
// to a whitelist and suppress the issue with a justification.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review processes started with a composed arguments string - Security
Matches methods that start a process with
Process.Start(fileName, arguments),
new ProcessStartInfo(fileName, arguments) or by assigning
ProcessStartInfo.Arguments, while also building strings (
String.Concat(),
Format(),
Join(), interpolation,
StringBuilder) and never using the
ArgumentList collection. A single arguments string is re-parsed by the target program (or by the shell when
UseShellExecute is true): quotes, spaces,
&,
| or
; in an interpolated value change the command executed.
This is CWE-78
OS Command Injection (review: the rule cannot tell whether the composed fragments are user-controlled).
Flagged Codepublic void Convert(string inputFile) {
// flagged: re-parsed by ffmpeg, a quote in inputFile injects extra arguments
var arguments = $"-i \"{inputFile}\" -o output.mp4";
Process.Start("ffmpeg", arguments);
}
Fixed Codepublic void Convert(string inputFile) {
var startInfo = new ProcessStartInfo("ffmpeg") { UseShellExecute = false };
startInfo.ArgumentList.Add("-i");
startInfo.ArgumentList.Add(inputFile); // quoted per argument, nothing is re-parsed
startInfo.ArgumentList.Add("-o");
startInfo.ArgumentList.Add("output.mp4");
Process.Start(startInfo);
}
How to Fix:Pass each argument through
ProcessStartInfo.ArgumentList.Add(...) (escaping is then done per argument), keep
UseShellExecute = false, and validate or whitelist any value that comes from a request, a file name or configuration. Suppress the issue when every composed fragment is a constant or an internally generated path.
// <Name>Review processes started with a composed arguments string</Name>
// <Id>ND3124:ReviewProcessesStartedWithComposedArgumentsString</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
// Members taking the arguments as a single string that the target program (or the shell) re-parses:
let processStringMembers = ThirdParty.Types.WithFullNameIn("System.Diagnostics.Process", "System.Diagnostics.ProcessStartInfo")
.ChildMethods().Where(m =>
(m.SimpleName == "Start" && m.IsStatic && m.Name.StartsWith("Start(String,String")) || // Process.Start(fileName, arguments)
(m.IsConstructor && m.Name.StartsWith(".ctor(String,String")) || // new ProcessStartInfo(fileName, arguments)
m.SimpleName == "set_Arguments") // startInfo.Arguments = ...
.ToHashSetEx()
where processStringMembers.Any()
// ArgumentList quotes each argument: the safe API.
let argumentListGetter = ThirdParty.Types.WithFullName("System.Diagnostics.ProcessStartInfo")
.ChildMethods().Where(m => m.SimpleName == "get_ArgumentList").ToHashSetEx()
// String composition in the same method: concatenation, formatting, interpolation, builders.
let stringBuildingMembers = ThirdParty.Methods.Where(m =>
(m.ParentType.FullName == "System.String" && m.SimpleNameEqualsAny("Concat", "Format", "Join", "Replace", "Insert")) ||
m.ParentType.FullNameEqualsAny("System.Text.StringBuilder", "System.Runtime.CompilerServices.DefaultInterpolatedStringHandler"))
.ToHashSetEx()
from m in Application.Methods.UsingAny(processStringMembers)
where !m.MethodsCalled.Intersect(argumentListGetter).Any()
let processMembersUsed = m.MethodsCalled.Intersect(processStringMembers).ToArray()
let composesStrings = m.MethodsCalled.Intersect(stringBuildingMembers).Any()
where composesStrings
select new {
m,
processMembersUsed,
Debt = 15.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* starts a process through *{1}* with an arguments string
// composed in the method. If any fragment derives from user input, a
// crafted value can inject extra arguments or commands.
//</Expl>
//<Desc>
// Matches methods that start a process with *Process.Start(fileName, arguments)*,
// *new ProcessStartInfo(fileName, arguments)* or by assigning
// *ProcessStartInfo.Arguments*, while also building strings
// (*String.Concat()*, *Format()*, *Join()*, interpolation, *StringBuilder*)
// and never using the *ArgumentList* collection. A single arguments
// string is re-parsed by the target program (or by the shell when
// *UseShellExecute* is true): quotes, spaces, *&*, *|* or *;* in an
// interpolated value change the command executed.
//
// This is CWE-78 *OS Command Injection* (review: the rule cannot tell
// whether the composed fragments are user-controlled).
//
// **Flagged Code**
// ```csharp
// public void Convert(string inputFile) {
// // flagged: re-parsed by ffmpeg, a quote in inputFile injects extra arguments
// var arguments = $"-i \"{inputFile}\" -o output.mp4";
// Process.Start("ffmpeg", arguments);
// }
// ```
//
// **Fixed Code**
// ```csharp
// public void Convert(string inputFile) {
// var startInfo = new ProcessStartInfo("ffmpeg") { UseShellExecute = false };
// startInfo.ArgumentList.Add("-i");
// startInfo.ArgumentList.Add(inputFile); // quoted per argument, nothing is re-parsed
// startInfo.ArgumentList.Add("-o");
// startInfo.ArgumentList.Add("output.mp4");
// Process.Start(startInfo);
// }
// ```
//</Desc>
//<HowToFix>
// Pass each argument through *ProcessStartInfo.ArgumentList.Add(...)*
// (escaping is then done per argument), keep *UseShellExecute = false*,
// and validate or whitelist any value that comes from a request, a file
// name or configuration. Suppress the issue when every composed fragment
// is a constant or an internally generated path.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review web endpoints building file paths from their parameters - Security
Matches public actions of MVC controllers, API controllers and Razor page models that take a string parameter, build a file path with it (
Path.Combine(), concatenation, interpolation) and then read or write the file system, without calling
Path.GetFileName(),
Path.GetFullPath() or
Path.IsPathRooted() first.
A segment such as
..\..\appsettings.json or an absolute path then escapes the intended directory:
path traversal. Note that
Path.Combine() makes this worse, since it discards everything before an absolute second argument.
This is CWE-22
Improper Limitation of a Pathname to a Restricted Directory (OWASP Top 10 A01 Broken Access Control).
Flagged Codepublic class DownloadController : ControllerBase {
[HttpGet("download")]
public IActionResult Download(string fileName) {
var path = Path.Combine(_root, fileName); // flagged: "..\..\web.config" escapes _root
return File(System.IO.File.ReadAllBytes(path), "application/octet-stream");
}
}
Fixed Codepublic class DownloadController : ControllerBase {
[HttpGet("download")]
public IActionResult Download(string fileName) {
var safeName = Path.GetFileName(fileName); // drops any directory segment
var path = Path.Combine(_root, safeName);
return File(System.IO.File.ReadAllBytes(path), "application/octet-stream");
}
}
How to Fix:Reduce the caller's segment to a file name with
Path.GetFileName(), or resolve the combined path with
Path.GetFullPath() and verify it still starts with the intended root directory before touching it. Better still, map the caller's value to an identifier (a Guid, a database row) and build the path from trusted data only.
Suppress the issue when the string parameter cannot reach the path, for instance when it only names a format or a culture.
// <Name>Review web endpoints building file paths from their parameters</Name>
// <Id>ND3129:ReviewWebEndpointsBuildingFilePathsFromTheirParameters</Id>
warnif count > 0
// Third-party types are part of the code model only when the application uses them.
let controllerBaseTypes = ThirdParty.Types.WithFullNameIn(
"Microsoft.AspNetCore.Mvc.ControllerBase",
"Microsoft.AspNetCore.Mvc.RazorPages.PageModel",
"System.Web.Mvc.Controller",
"System.Web.Http.ApiController").ToArray()
where controllerBaseTypes.Any()
let fileSystemMembers = ThirdParty.Methods.Where(m =>
m.ParentType.FullName.StartsWithAny(
"System.IO.File", "System.IO.Directory", "System.IO.FileStream",
"System.IO.FileInfo", "System.IO.DirectoryInfo", "System.IO.StreamReader")).ToHashSetEx()
// The path is assembled rather than picked from a closed set of known paths.
let pathBuildingMembers = ThirdParty.Methods.Where(m =>
(m.ParentType.FullName == "System.IO.Path" && m.SimpleNameEqualsAny("Combine", "Join")) ||
(m.ParentType.FullName == "System.String" && m.SimpleNameEqualsAny("Concat", "Format")) ||
m.ParentType.FullName == "System.Runtime.CompilerServices.DefaultInterpolatedStringHandler").ToHashSetEx()
// Calling any of these means the segment is being reduced or resolved before use.
let pathSanitizingMembers = ThirdParty.Methods.Where(m =>
m.ParentType.FullName == "System.IO.Path" &&
m.SimpleNameEqualsAny("GetFileName", "GetFullPath", "IsPathRooted", "IsPathFullyQualified",
"GetInvalidFileNameChars", "GetInvalidPathChars")).ToHashSetEx()
from t in Application.Types.ThatDeriveFromAny(controllerBaseTypes)
where !t.IsGeneratedByCompiler
from m in t.Methods
where m.IsPublic && !m.IsConstructor && !m.IsGeneratedByCompiler &&
// The endpoint receives a string from the caller...
m.Name.Contains("String") &&
// ...assembles a path with it and reaches the file system...
m.MethodsCalled.Intersect(pathBuildingMembers).Any() &&
m.MethodsCalled.Intersect(fileSystemMembers).Any() &&
// ...without reducing the segment first.
!m.MethodsCalled.Intersect(pathSanitizingMembers).Any()
select new {
m,
fileSystemMembersUsed = m.MethodsCalled.Intersect(fileSystemMembers).ToArray(),
Debt = 20.ToMinutes().ToDebt(),
Severity = Severity.High
}
// <Desc>
// Matches public actions of MVC controllers, API controllers and Razor
// page models that take a string parameter, build a file path with it
// (*Path.Combine()*, concatenation, interpolation) and then read or write
// the file system, without calling *Path.GetFileName()*,
// *Path.GetFullPath()* or *Path.IsPathRooted()* first.
//
// A segment such as *..\..\appsettings.json* or an absolute path then
// escapes the intended directory: **path traversal**. Note that
// *Path.Combine()* makes this worse, since it discards everything before
// an absolute second argument.
//
// This is CWE-22 *Improper Limitation of a Pathname to a Restricted
// Directory* (OWASP Top 10 A01 Broken Access Control).
//
// **Flagged Code**
// ```csharp
// public class DownloadController : ControllerBase {
// [HttpGet("download")]
// public IActionResult Download(string fileName) {
// var path = Path.Combine(_root, fileName); // flagged: "..\..\web.config" escapes _root
// return File(System.IO.File.ReadAllBytes(path), "application/octet-stream");
// }
// }
// ```
//
// **Fixed Code**
// ```csharp
// public class DownloadController : ControllerBase {
// [HttpGet("download")]
// public IActionResult Download(string fileName) {
// var safeName = Path.GetFileName(fileName); // drops any directory segment
// var path = Path.Combine(_root, safeName);
// return File(System.IO.File.ReadAllBytes(path), "application/octet-stream");
// }
// }
// ```
// </Desc>
//<Expl>
// The web endpoint *{0}* builds a file path from a string it receives and
// calls *{1}*, without reducing the segment first. A caller sending
// *..\..\* or an absolute path can reach files outside the intended directory.
//</Expl>
// <HowToFix>
// Reduce the caller's segment to a file name with *Path.GetFileName()*, or
// resolve the combined path with *Path.GetFullPath()* and verify it still
// starts with the intended root directory before touching it. Better
// still, map the caller's value to an identifier (a Guid, a database row)
// and build the path from trusted data only.
//
// Suppress the issue when the string parameter cannot reach the path, for
// instance when it only names a format or a culture.
// </HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review custom archive extraction (Zip Slip) - Security
Matches methods that read the name of an archive entry (
ZipArchiveEntry.FullName,
TarEntry.Name, SharpZipLib
ZipEntry.Name, SharpCompress
IEntry.Key), build a path with it (
Path.Combine(), concatenation, interpolation) and write to the file system, without both resolving the path with
Path.GetFullPath() and checking it with
String.StartsWith() against the destination root, and without reducing the name with
Path.GetFileName().
Archive entry names are data supplied by whoever built the archive. A name such as
..\..\..\Windows\Startup\evil.cmd combined with the destination directory escapes it and overwrites an arbitrary file: the
Zip Slip vulnerability, CWE-22
Improper Limitation of a Pathname to a Restricted Directory (OWASP Top 10 A01 Broken Access Control).
Path.Combine() makes it worse by discarding the destination when the entry name is an absolute path.
Path.GetFullPath() alone does not suppress the issue: it resolves the
.. segments but verifies nothing.
Flagged Codestatic void Extract(ZipArchive archive, string destination) {
foreach (var entry in archive.Entries) {
var path = Path.Combine(destination, entry.FullName); // flagged: no containment
entry.ExtractToFile(path, overwrite: true);
}
}
Fixed Codestatic void Extract(ZipArchive archive, string destination) {
var root = Path.GetFullPath(destination + Path.DirectorySeparatorChar);
foreach (var entry in archive.Entries) {
var path = Path.GetFullPath(Path.Combine(root, entry.FullName));
if (!path.StartsWith(root, StringComparison.Ordinal))
throw new IOException("Entry outside of the destination: " + entry.FullName);
entry.ExtractToFile(path, overwrite: true);
}
}
How to Fix:Prefer the framework extraction that already verifies containment:
ZipFile.ExtractToDirectory() and
ZipArchive.ExtractToDirectory() reject entries that escape the destination. When entries must be processed one by one, resolve the combined path with
Path.GetFullPath() and verify that it starts with the destination root (terminated by a directory separator) before writing, or reduce the entry name with
Path.GetFileName() when the archive layout is flat.
Suppress the issue when the containment check is done in a helper called by the method.
// <Name>Review custom archive extraction (Zip Slip)</Name>
// <Id>ND3135:ReviewCustomArchiveExtraction</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
// The entry name comes from the archive, hence from whoever produced the archive.
let entryNameGetters = ThirdParty.Methods.Where(m =>
m.IsPropertyGetter && (
(m.ParentType.FullName == "System.IO.Compression.ZipArchiveEntry" && m.SimpleName == "get_FullName") ||
(m.ParentType.FullName == "System.Formats.Tar.TarEntry" && m.SimpleName == "get_Name") ||
(m.ParentType.FullNameEqualsAny("ICSharpCode.SharpZipLib.Zip.ZipEntry", "ICSharpCode.SharpZipLib.Tar.TarEntry") && m.SimpleName == "get_Name") ||
(m.ParentType.FullName.StartsWith("SharpCompress.") && m.SimpleName == "get_Key")))
.ToHashSetEx()
where entryNameGetters.Any()
// The destination path is assembled with the entry name.
let pathBuildingMembers = ThirdParty.Methods.Where(m =>
(m.ParentType.FullName == "System.IO.Path" && m.SimpleNameEqualsAny("Combine", "Join")) ||
(m.ParentType.FullName == "System.String" && m.SimpleNameEqualsAny("Concat", "Format")) ||
m.ParentType.FullName == "System.Runtime.CompilerServices.DefaultInterpolatedStringHandler").ToHashSetEx()
// Something is written at that path.
let fileWriteMembers = ThirdParty.Methods.Where(m =>
(m.ParentType.FullName == "System.IO.File" && m.SimpleName.StartsWithAny("Create", "Write", "Open", "Copy", "Move", "AppendAll")) ||
(m.ParentType.FullNameEqualsAny("System.IO.FileStream", "System.IO.StreamWriter") && m.IsConstructor) ||
(m.ParentType.FullName == "System.IO.Directory" && m.SimpleName == "CreateDirectory") ||
m.SimpleName.StartsWithAny("ExtractToFile", "WriteToFile", "WriteToDirectory"))
.ToHashSetEx()
// Containment verification: the combined path is resolved AND compared with the destination root.
// Path.GetFullPath() alone resolves the '..' segments but verifies nothing.
let getFullPath = ThirdParty.Methods.Where(m => m.ParentType.FullName == "System.IO.Path" && m.SimpleName == "GetFullPath").ToHashSetEx()
let startsWith = ThirdParty.Methods.Where(m => m.ParentType.FullName == "System.String" && m.SimpleName == "StartsWith").ToHashSetEx()
// Reducing the entry name to a file name discards any directory segment.
let getFileName = ThirdParty.Methods.Where(m => m.ParentType.FullName == "System.IO.Path" && m.SimpleName == "GetFileName").ToHashSetEx()
from m in Application.Methods.UsingAny(entryNameGetters)
where m.MethodsCalled.Intersect(pathBuildingMembers).Any() &&
m.MethodsCalled.Intersect(fileWriteMembers).Any() &&
!(m.MethodsCalled.Intersect(getFullPath).Any() && m.MethodsCalled.Intersect(startsWith).Any()) &&
!m.MethodsCalled.Intersect(getFileName).Any()
select new {
m,
fileWriteMembersUsed = m.MethodsCalled.Intersect(fileWriteMembers).ToArray(),
Debt = 20.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The method *{0}* builds a destination path from an archive entry name
// and writes through *{1}*, without verifying that the resolved path
// stays under the destination directory. An entry named
// *..\..\startup.cmd* is written outside that directory.
//</Expl>
// <Desc>
// Matches methods that read the name of an archive entry
// (*ZipArchiveEntry.FullName*, *TarEntry.Name*, SharpZipLib *ZipEntry.Name*,
// SharpCompress *IEntry.Key*), build a path with it (*Path.Combine()*,
// concatenation, interpolation) and write to the file system, without
// both resolving the path with *Path.GetFullPath()* and checking it with
// *String.StartsWith()* against the destination root, and without
// reducing the name with *Path.GetFileName()*.
//
// Archive entry names are data supplied by whoever built the archive. A
// name such as *..\..\..\Windows\Startup\evil.cmd* combined with the
// destination directory escapes it and overwrites an arbitrary file:
// the **Zip Slip** vulnerability, CWE-22 *Improper Limitation of a
// Pathname to a Restricted Directory* (OWASP Top 10 A01 Broken Access
// Control). *Path.Combine()* makes it worse by discarding the destination
// when the entry name is an absolute path.
//
// *Path.GetFullPath()* alone does not suppress the issue: it resolves
// the *..* segments but verifies nothing.
//
// **Flagged Code**
// ```csharp
// static void Extract(ZipArchive archive, string destination) {
// foreach (var entry in archive.Entries) {
// var path = Path.Combine(destination, entry.FullName); // flagged: no containment
// entry.ExtractToFile(path, overwrite: true);
// }
// }
// ```
//
// **Fixed Code**
// ```csharp
// static void Extract(ZipArchive archive, string destination) {
// var root = Path.GetFullPath(destination + Path.DirectorySeparatorChar);
// foreach (var entry in archive.Entries) {
// var path = Path.GetFullPath(Path.Combine(root, entry.FullName));
// if (!path.StartsWith(root, StringComparison.Ordinal))
// throw new IOException("Entry outside of the destination: " + entry.FullName);
// entry.ExtractToFile(path, overwrite: true);
// }
// }
// ```
// </Desc>
// <HowToFix>
// Prefer the framework extraction that already verifies containment:
// *ZipFile.ExtractToDirectory()* and *ZipArchive.ExtractToDirectory()*
// reject entries that escape the destination. When entries must be
// processed one by one, resolve the combined path with
// *Path.GetFullPath()* and verify that it starts with the destination
// root (terminated by a directory separator) before writing, or reduce
// the entry name with *Path.GetFileName()* when the archive layout is
// flat.
//
// Suppress the issue when the containment check is done in a helper
// called by the method.
// </HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review assemblies and types loaded from runtime-provided names - Security
Matches methods that load assemblies from a path or from bytes (
Assembly.LoadFrom(),
LoadFile(),
Load(byte[]),
AssemblyLoadContext.LoadFromAssemblyPath(),
LoadFromStream()), instantiate a type from assembly and type names (
Activator.CreateInstance(assemblyName, typeName)), or, in a method receiving a string, combine
Type.GetType(string) with
Activator.CreateInstance(Type). Plugin systems legitimately do this, but a path or name that comes from a request, a configuration file, a database or a user-writable directory turns it into remote code execution.
This is CWE-470
Use of Externally-Controlled Input to Select Classes or Code and CWE-829
Inclusion of Functionality from Untrusted Control Sphere (review).
Flagged Codepublic object LoadPlugin(string pluginPath, string typeName)
{
var asm = Assembly.LoadFrom(pluginPath); // flagged: path from caller
return Activator.CreateInstance(asm.GetType(typeName));
}
Fixed Codestatic readonly Dictionary<string, Func<IPlugin>> s_Plugins = new()
{
["csv"] = () => new CsvPlugin(),
["json"] = () => new JsonPlugin()
};
public IPlugin LoadPlugin(string name)
{
// The name maps to a closed set of known types: nothing is resolved by reflection.
if (!s_Plugins.TryGetValue(name, out var create)) throw new ArgumentException(name);
return create();
}
How to Fix:Load plugins only from a directory the application owns (not user-writable), verify a signature or hash before loading, and map user-provided names to a whitelist of known types instead of resolving them by reflection. Suppress the issue once the source of the path or name is confirmed trusted.
// <Name>Review assemblies and types loaded from runtime-provided names</Name>
// <Id>ND3125:ReviewAssembliesAndTypesLoadedFromRuntimeProvidedNames</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
let dynamicLoadMembers =
ThirdParty.Types.WithFullName("System.Reflection.Assembly").ChildMethods()
.Where(m => m.SimpleNameEqualsAny("LoadFrom", "LoadFile", "UnsafeLoadFrom", "LoadWithPartialName") ||
(m.SimpleName == "Load" && m.Name.Contains("Byte[]")))
.Concat(ThirdParty.Types.WithFullName("System.Runtime.Loader.AssemblyLoadContext").ChildMethods()
.Where(m => m.SimpleNameEqualsAny("LoadFromAssemblyPath", "LoadFromStream", "LoadFromNativeImagePath")))
.Concat(ThirdParty.Types.WithFullNameIn("System.Activator", "System.AppDomain").ChildMethods()
.Where(m => m.SimpleNameEqualsAny("CreateInstance", "CreateInstanceFrom", "CreateInstanceAndUnwrap", "CreateInstanceFromAndUnwrap") &&
m.Name.Contains("String,String"))) // (assemblyName, typeName) overloads
.ToHashSetEx()
// Type.GetType(string) alone is common; it becomes a dynamic instantiation when combined with
// Activator.CreateInstance(Type...) in a method that receives a string (the type name may flow in).
let typeFromName = ThirdParty.Types.WithFullNameIn("System.Type", "System.Reflection.Assembly").ChildMethods()
.Where(m => m.SimpleName == "GetType" && m.Name.StartsWith("GetType(String")).ToHashSetEx()
let createInstanceFromType = ThirdParty.Types.WithFullName("System.Activator").ChildMethods()
.Where(m => m.SimpleName == "CreateInstance" && m.Name.StartsWith("CreateInstance(Type")).ToHashSetEx()
where dynamicLoadMembers.Any() || (typeFromName.Any() && createInstanceFromType.Any())
from m in Application.Methods.UsingAny(dynamicLoadMembers.Concat(typeFromName))
let dynamicLoadMembersUsed = m.MethodsCalled.Where(x => dynamicLoadMembers.Contains(x)).ToArray()
let instantiatesTypeByName = m.Name.Contains("String") &&
m.MethodsCalled.Any(x => typeFromName.Contains(x)) &&
m.MethodsCalled.Any(x => createInstanceFromType.Contains(x))
where dynamicLoadMembersUsed.Length > 0 || instantiatesTypeByName
select new {
m,
dynamicLoadMembersUsed,
instantiatesTypeByName,
Debt = 20.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* {2?resolves a type by name and instantiates it:loads code dynamically through {1}}.
// If the path, bytes or type name can be influenced by a user or by a
// writable location, arbitrary code runs inside the process.
//</Expl>
//<Desc>
// Matches methods that load assemblies from a path or from bytes
// (*Assembly.LoadFrom()*, *LoadFile()*, *Load(byte[])*,
// *AssemblyLoadContext.LoadFromAssemblyPath()*, *LoadFromStream()*),
// instantiate a type from assembly and type names (*Activator.CreateInstance(assemblyName, typeName)*),
// or, in a method receiving a string, combine *Type.GetType(string)* with
// *Activator.CreateInstance(Type)*. Plugin systems legitimately do this,
// but a path or name that comes from a request, a configuration file, a
// database or a user-writable directory turns it into remote code
// execution.
//
// This is CWE-470 *Use of Externally-Controlled Input to Select Classes
// or Code* and CWE-829 *Inclusion of Functionality from Untrusted Control
// Sphere* (review).
//
// **Flagged Code**
// ```csharp
// public object LoadPlugin(string pluginPath, string typeName)
// {
// var asm = Assembly.LoadFrom(pluginPath); // flagged: path from caller
// return Activator.CreateInstance(asm.GetType(typeName));
// }
// ```
//
// **Fixed Code**
// ```csharp
// static readonly Dictionary<string, Func<IPlugin>> s_Plugins = new()
// {
// ["csv"] = () => new CsvPlugin(),
// ["json"] = () => new JsonPlugin()
// };
// public IPlugin LoadPlugin(string name)
// {
// // The name maps to a closed set of known types: nothing is resolved by reflection.
// if (!s_Plugins.TryGetValue(name, out var create)) throw new ArgumentException(name);
// return create();
// }
// ```
//</Desc>
//<HowToFix>
// Load plugins only from a directory the application owns (not
// user-writable), verify a signature or hash before loading, and map
// user-provided names to a whitelist of known types instead of resolving
// them by reflection. Suppress the issue once the source of the path or
// name is confirmed trusted.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review web endpoints without [Authorize] nor [AllowAnonymous] - Security
Matches ASP.NET Core controllers, SignalR hubs and legacy MVC / Web API controllers that are not authorized at class level (including inherited classes) and expose public actions carrying neither an authorization attribute (
[Authorize], a derived attribute, or any attribute implementing
IAuthorizeData) nor an explicit
[AllowAnonymous]. The rule stays silent when the application sets a global fallback policy (
AuthorizationOptions.FallbackPolicy) or registers a global
AuthorizeFilter, since every endpoint is then protected by default.
An endpoint that is anonymous by omission rather than by decision is a classic broken access control (OWASP A01:2021, CWE-306
Missing Authentication for Critical Function).
Flagged Codepublic class OrdersController : ControllerBase // flagged: no [Authorize] on the class
{
[HttpGet("{id}")] // no [Authorize], no [AllowAnonymous]
public IActionResult Get(int id) => Ok(_orders.Find(id));
[HttpGet("status")]
public IActionResult Status() => Ok("up");
}
Fixed Code[Authorize] // every action requires authentication
public class OrdersController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult Get(int id) => Ok(_orders.Find(id));
[AllowAnonymous] // explicit decision for the public probe
[HttpGet("status")]
public IActionResult Status() => Ok("up");
}
How to Fix:Tag the controller (or its base class) with
[Authorize] and opt out with
[AllowAnonymous] on the few public actions, or configure a fallback policy so that everything requires authentication by default:
builder.Services.AddAuthorization(o => o.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()). Public helper methods that are not actions should be non-public or tagged
[NonAction].
// <Name>Review web endpoints without [Authorize] nor [AllowAnonymous]</Name>
// <Id>ND3118:ReviewWebEndpointsWithoutAuthorizeNorAllowAnonymous</Id>
warnif count > 0
// Third-party types are part of the code model only when the application uses them.
let controllerBaseTypes = ThirdParty.Types.WithFullNameIn(
"Microsoft.AspNetCore.Mvc.ControllerBase",
"Microsoft.AspNetCore.SignalR.Hub",
"System.Web.Mvc.Controller",
"System.Web.Http.ApiController").ToArray()
where controllerBaseTypes.Any()
// A fallback policy or a global AuthorizeFilter protects every endpoint: nothing to report.
let appHasGlobalAuthorization = Application.Methods.Any(m =>
m.IsUsing("Microsoft.AspNetCore.Authorization.AuthorizationOptions.set_FallbackPolicy(AuthorizationPolicy)".AllowNoMatch()) ||
m.CreateA("Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter".AllowNoMatch()) ||
m.IsUsing("System.Web.Mvc.AuthorizeAttribute..ctor()".AllowNoMatch()))
where !appHasGlobalAuthorization
// [Authorize], attributes deriving from it, and any attribute implementing IAuthorizeData (custom policies)
let authorizeAttrs = ThirdParty.Types.WithFullNameIn(
"Microsoft.AspNetCore.Authorization.AuthorizeAttribute",
"System.Web.Mvc.AuthorizeAttribute",
"System.Web.Http.AuthorizeAttribute").ToArray()
let authorizeDataInterfaces = ThirdParty.Types.WithFullName("Microsoft.AspNetCore.Authorization.IAuthorizeData").ToArray()
let authorizeAttrClasses = authorizeAttrs
.Concat(Types.ThatDeriveFromAny(authorizeAttrs))
.Concat(Types.ThatImplementAny(authorizeDataInterfaces))
.ToHashSetEx()
let anonymousAttrs = ThirdParty.Types.WithFullNameIn(
"Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute",
"System.Web.Mvc.AllowAnonymousAttribute",
"System.Web.Http.AllowAnonymousAttribute").ToArray()
let allowAnonymousInterfaces = ThirdParty.Types.WithFullName("Microsoft.AspNetCore.Authorization.IAllowAnonymous").ToArray()
let anonymousAttrClasses = anonymousAttrs
.Concat(Types.ThatDeriveFromAny(anonymousAttrs))
.Concat(Types.ThatImplementAny(allowAnonymousInterfaces))
.ToHashSetEx()
let nonActionAttrs = ThirdParty.Types.WithFullNameIn("Microsoft.AspNetCore.Mvc.NonActionAttribute", "System.Web.Mvc.NonActionAttribute").ToHashSetEx()
let isAuthorized = new Func<IAttributeTarget, bool>(x => x.AttributeClassesThatTagMe.Any(a => authorizeAttrClasses.Contains(a)))
let isAnonymous = new Func<IAttributeTarget, bool>(x => x.AttributeClassesThatTagMe.Any(a => anonymousAttrClasses.Contains(a)))
from t in Application.Types.ThatDeriveFromAny(controllerBaseTypes)
where !t.IsAbstract && !t.IsGeneratedByCompiler
// An attribute on the class or on any base class covers every action.
let classChain = t.BaseClasses.Concat(new[] { t }).ToArray()
where !classChain.Any(c => isAuthorized(c)) && !classChain.Any(c => isAnonymous(c))
let unprotectedActions = t.Methods.Where(m =>
m.IsPublic && !m.IsStatic && !m.IsConstructor && !m.IsGeneratedByCompiler &&
!m.IsPropertyGetter && !m.IsPropertySetter && !m.IsEventAdder && !m.IsEventRemover &&
!m.OverriddensBase.Any(mb => mb.IsThirdParty) && // OnActionExecuting(), Dispose(), ToString()...
!m.AttributeClassesThatTagMe.Any(a => nonActionAttrs.Contains(a)) &&
!isAuthorized(m) && !isAnonymous(m)).ToArray()
where unprotectedActions.Length > 0
select new {
t,
unprotectedActions,
Debt = (5 * unprotectedActions.Length).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The controller *{0}* has no *[Authorize]* on its class chain and
// exposes action(s) *{1}* tagged with neither *[Authorize]* nor *[AllowAnonymous]*:
// they answer any unauthenticated request.
//</Expl>
//<Desc>
// Matches ASP.NET Core controllers, SignalR hubs and legacy MVC / Web API
// controllers that are not authorized at class level (including inherited
// classes) and expose public actions carrying neither an authorization
// attribute (*[Authorize]*, a derived attribute, or any attribute
// implementing *IAuthorizeData*) nor an explicit *[AllowAnonymous]*.
// The rule stays silent when the application sets a global fallback
// policy (*AuthorizationOptions.FallbackPolicy*) or registers a global
// *AuthorizeFilter*, since every endpoint is then protected by default.
//
// An endpoint that is anonymous by omission rather than by decision is a
// classic broken access control (OWASP A01:2021, CWE-306 *Missing
// Authentication for Critical Function*).
//
// **Flagged Code**
// ```csharp
// public class OrdersController : ControllerBase // flagged: no [Authorize] on the class
// {
// [HttpGet("{id}")] // no [Authorize], no [AllowAnonymous]
// public IActionResult Get(int id) => Ok(_orders.Find(id));
//
// [HttpGet("status")]
// public IActionResult Status() => Ok("up");
// }
// ```
//
// **Fixed Code**
// ```csharp
// [Authorize] // every action requires authentication
// public class OrdersController : ControllerBase
// {
// [HttpGet("{id}")]
// public IActionResult Get(int id) => Ok(_orders.Find(id));
//
// [AllowAnonymous] // explicit decision for the public probe
// [HttpGet("status")]
// public IActionResult Status() => Ok("up");
// }
// ```
//</Desc>
//<HowToFix>
// Tag the controller (or its base class) with *[Authorize]* and opt out
// with *[AllowAnonymous]* on the few public actions, or configure a
// fallback policy so that everything requires authentication by default:
// *builder.Services.AddAuthorization(o => o.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build())*.
// Public helper methods that are not actions should be non-public or
// tagged *[NonAction]*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review state-changing endpoints opened to anonymous callers - Security
Matches actions carrying both
[AllowAnonymous] and a state-changing HTTP verb attribute.
[AllowAnonymous] wins over every
[Authorize] on the class, the base class and the global filters, so such an action answers any unauthenticated request, whatever the rest of the configuration says. That is intended for sign-in, sign-up, password-recovery, webhook and callback endpoints, which the rule skips by name, and rarely intended anywhere else.
This is CWE-284
Improper Access Control (OWASP Top 10 A01 Broken Access Control).
Flagged Code[Authorize]
public class CommentsController : ControllerBase
{
[AllowAnonymous] // flagged: wins over [Authorize]
[HttpDelete("{id}")]
public IActionResult DeleteComment(int id) { _comments.Remove(id); return NoContent(); }
}
Fixed Code[Authorize]
public class CommentsController : ControllerBase
{
[HttpDelete("{id}")] // the class policy applies again
public IActionResult DeleteComment(int id) { _comments.Remove(id); return NoContent(); }
[AllowAnonymous]
[HttpPost("login")] // sign-in endpoints are skipped by name
public IActionResult Login(LoginModel model) => SignInAsync(model);
}
How to Fix:Remove
[AllowAnonymous] so the controller or the global policy applies again. When the endpoint genuinely must be reachable without a session, protect it another way (a signed payload, a shared secret header, a rate limit) and suppress the issue with that justification.
// <Name>Review state-changing endpoints opened to anonymous callers</Name>
// <Id>ND3130:ReviewStateChangingEndpointsOpenedToAnonymousCallers</Id>
warnif count > 0
// Third-party types are part of the code model only when the application uses them.
let anonymousAttrs = ThirdParty.Types.WithFullNameIn(
"Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute",
"System.Web.Mvc.AllowAnonymousAttribute",
"System.Web.Http.AllowAnonymousAttribute").ToHashSetEx()
let stateChangingVerbAttrs = ThirdParty.Types.WithFullNameIn(
"Microsoft.AspNetCore.Mvc.HttpPostAttribute",
"Microsoft.AspNetCore.Mvc.HttpPutAttribute",
"Microsoft.AspNetCore.Mvc.HttpDeleteAttribute",
"Microsoft.AspNetCore.Mvc.HttpPatchAttribute",
"System.Web.Mvc.HttpPostAttribute",
"System.Web.Mvc.HttpPutAttribute",
"System.Web.Mvc.HttpDeleteAttribute").ToHashSetEx()
where anonymousAttrs.Any() && stateChangingVerbAttrs.Any()
from m in Application.Methods.TaggedWithAnyAttributes(anonymousAttrs)
where m.AttributeClassesThatTagMe.Any(a => stateChangingVerbAttrs.Contains(a)) &&
// Sign-in, sign-up and password-recovery endpoints have to be anonymous.
!m.SimpleNameContainsAny(
"Login", "SignIn", "SignUp", "Authenticate", "Register", "Token",
"ForgotPassword", "ResetPassword", "QuickConnect", "Webhook", "Callback")
select new {
m,
m.ParentType,
Debt = 15.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The action *{0}* of the *{1}* changes state (POST, PUT, DELETE or PATCH)
// and is explicitly opened to anonymous callers by *[AllowAnonymous]*,
// although its name does not suggest a sign-in or password-recovery endpoint.
//</Expl>
// <Desc>
// Matches actions carrying both *[AllowAnonymous]* and a state-changing
// HTTP verb attribute. *[AllowAnonymous]* wins over every *[Authorize]* on
// the class, the base class and the global filters, so such an action
// answers any unauthenticated request, whatever the rest of the
// configuration says. That is intended for sign-in, sign-up,
// password-recovery, webhook and callback endpoints, which the rule skips
// by name, and rarely intended anywhere else.
//
// This is CWE-284 *Improper Access Control* (OWASP Top 10 A01 Broken
// Access Control).
//
// **Flagged Code**
// ```csharp
// [Authorize]
// public class CommentsController : ControllerBase
// {
// [AllowAnonymous] // flagged: wins over [Authorize]
// [HttpDelete("{id}")]
// public IActionResult DeleteComment(int id) { _comments.Remove(id); return NoContent(); }
// }
// ```
//
// **Fixed Code**
// ```csharp
// [Authorize]
// public class CommentsController : ControllerBase
// {
// [HttpDelete("{id}")] // the class policy applies again
// public IActionResult DeleteComment(int id) { _comments.Remove(id); return NoContent(); }
//
// [AllowAnonymous]
// [HttpPost("login")] // sign-in endpoints are skipped by name
// public IActionResult Login(LoginModel model) => SignInAsync(model);
// }
// ```
// </Desc>
// <HowToFix>
// Remove *[AllowAnonymous]* so the controller or the global policy applies
// again. When the endpoint genuinely must be reachable without a session,
// protect it another way (a signed payload, a shared secret header, a rate
// limit) and suppress the issue with that justification.
// </HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review state-changing MVC actions without anti-forgery validation - Security
Matches view-based MVC controllers (deriving from
Microsoft.AspNetCore.Mvc.Controller or
System.Web.Mvc.Controller) whose POST/PUT/DELETE/PATCH actions are not covered by
[ValidateAntiForgeryToken] or
[AutoValidateAntiforgeryToken] on the action, the class chain, or a global filter, and are not explicitly exempted with
[IgnoreAntiforgeryToken]. Controllers or actions tagged
[ApiController] or
[Consumes(...)] are skipped: they are not reachable from an HTML form. Cookie-authenticated form posts without a token are the textbook CSRF (CWE-352).
Flagged Codepublic class AccountController : Controller
{
[HttpPost] // flagged: no anti-forgery validation
public IActionResult UpdateEmail(string email)
{
_accounts.SetEmail(User.Identity.Name, email);
return RedirectToAction("Index");
}
}
Fixed Codepublic class AccountController : Controller
{
[HttpPost]
[ValidateAntiForgeryToken] // the form must carry the token
public IActionResult UpdateEmail(string email)
{
_accounts.SetEmail(User.Identity.Name, email);
return RedirectToAction("Index");
}
}
How to Fix:Register
AutoValidateAntiforgeryTokenAttribute as a global filter in
AddControllersWithViews(o => o.Filters.Add(new AutoValidateAntiforgeryTokenAttribute())) and emit the token in forms with the
asp-antiforgery tag helper (on by default for
<form>), or tag each unsafe action with
[ValidateAntiForgeryToken]. Exempt token-authenticated endpoints explicitly with
[IgnoreAntiforgeryToken].
// <Name>Review state-changing MVC actions without anti-forgery validation</Name>
// <Id>ND3119:ReviewStateChangingMvcActionsWithoutAntiForgeryValidation</Id>
warnif count > 0
// Only view-based MVC controllers are concerned: API controllers authenticate with tokens, not cookies.
// Third-party types are part of the code model only when the application uses them.
let mvcControllerTypes = ThirdParty.Types.WithFullNameIn("Microsoft.AspNetCore.Mvc.Controller", "System.Web.Mvc.Controller").ToArray()
where mvcControllerTypes.Any()
let validateAttrs = ThirdParty.Types.WithFullNameIn(
"Microsoft.AspNetCore.Mvc.ValidateAntiForgeryTokenAttribute",
"Microsoft.AspNetCore.Mvc.AutoValidateAntiforgeryTokenAttribute",
"System.Web.Mvc.ValidateAntiForgeryTokenAttribute").ToHashSetEx()
let ignoreAttrs = ThirdParty.Types.WithFullName("Microsoft.AspNetCore.Mvc.IgnoreAntiforgeryTokenAttribute").ToHashSetEx()
let apiControllerAttrs = ThirdParty.Types.WithFullName("Microsoft.AspNetCore.Mvc.ApiControllerAttribute").ToHashSetEx()
let consumesAttrs = ThirdParty.Types.WithFullName("Microsoft.AspNetCore.Mvc.ConsumesAttribute").ToHashSetEx()
let stateChangingVerbAttrs = ThirdParty.Types.WithFullNameIn(
"Microsoft.AspNetCore.Mvc.HttpPostAttribute", "Microsoft.AspNetCore.Mvc.HttpPutAttribute",
"Microsoft.AspNetCore.Mvc.HttpDeleteAttribute", "Microsoft.AspNetCore.Mvc.HttpPatchAttribute",
"System.Web.Mvc.HttpPostAttribute", "System.Web.Mvc.HttpPutAttribute", "System.Web.Mvc.HttpDeleteAttribute").ToHashSetEx()
// A global filter validates every unsafe request: nothing to report.
let appHasGlobalValidation = Application.Methods.Any(m => validateAttrs.Any(a => m.IsUsing(a)))
where !appHasGlobalValidation
let tagged = new Func<IAttributeTarget, HashSet<IType>, bool>((x, attrs) => x.AttributeClassesThatTagMe.Any(a => attrs.Contains(a)))
from t in Application.Types.ThatDeriveFromAny(mvcControllerTypes)
where !t.IsAbstract && !t.IsGeneratedByCompiler
let classChain = t.BaseClasses.Concat(new[] { t }).ToArray()
where !classChain.Any(c => tagged(c, validateAttrs) || tagged(c, ignoreAttrs) || tagged(c, apiControllerAttrs) || tagged(c, consumesAttrs))
let unvalidatedActions = t.Methods.Where(m =>
m.IsPublic && !m.IsStatic && !m.IsGeneratedByCompiler &&
tagged(m, stateChangingVerbAttrs) &&
!tagged(m, validateAttrs) && !tagged(m, ignoreAttrs) &&
!tagged(m, consumesAttrs)).ToArray() // [Consumes("application/json")]: not reachable from an HTML form
where unvalidatedActions.Length > 0
select new {
t,
unvalidatedActions,
Debt = (10 * unvalidatedActions.Length).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The MVC controller *{0}* exposes state-changing action(s) *{1}* (POST, PUT,
// DELETE or PATCH) without *[ValidateAntiForgeryToken]*: a malicious page can
// submit a form to them with the victim's cookies (Cross-Site Request Forgery).
//</Expl>
//<Desc>
// Matches view-based MVC controllers (deriving from
// *Microsoft.AspNetCore.Mvc.Controller* or *System.Web.Mvc.Controller*)
// whose POST/PUT/DELETE/PATCH actions are not covered by
// *[ValidateAntiForgeryToken]* or *[AutoValidateAntiforgeryToken]* on the
// action, the class chain, or a global filter, and are not explicitly
// exempted with *[IgnoreAntiforgeryToken]*. Controllers or actions tagged
// *[ApiController]* or *[Consumes(...)]* are skipped: they are not
// reachable from an HTML form. Cookie-authenticated form posts without a
// token are the textbook CSRF (CWE-352).
//
// **Flagged Code**
// ```csharp
// public class AccountController : Controller
// {
// [HttpPost] // flagged: no anti-forgery validation
// public IActionResult UpdateEmail(string email)
// {
// _accounts.SetEmail(User.Identity.Name, email);
// return RedirectToAction("Index");
// }
// }
// ```
//
// **Fixed Code**
// ```csharp
// public class AccountController : Controller
// {
// [HttpPost]
// [ValidateAntiForgeryToken] // the form must carry the token
// public IActionResult UpdateEmail(string email)
// {
// _accounts.SetEmail(User.Identity.Name, email);
// return RedirectToAction("Index");
// }
// }
// ```
//</Desc>
//<HowToFix>
// Register *AutoValidateAntiforgeryTokenAttribute* as a global filter in
// *AddControllersWithViews(o => o.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()))*
// and emit the token in forms with the *asp-antiforgery* tag helper (on by
// default for *<form>*), or tag each unsafe action with
// *[ValidateAntiForgeryToken]*. Exempt token-authenticated endpoints
// explicitly with *[IgnoreAntiforgeryToken]*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review HTML-encoding bypasses - Security
Matches methods that bypass the HTML encoder that Razor and Blazor apply by default:
Html.Raw(),
new HtmlString(),
new MarkupString(),
MvcHtmlString.Create(), and the
AppendHtml(string) /
SetHtmlContent(string) members of tag helpers and
IHtmlContentBuilder.
Each of these members tells the view engine
trust this string, it is already HTML. When the string contains data that a user controls (a comment, a profile field, a query string value), a payload such as
<script> is rendered as markup and executed in the browser of every visitor:
cross-site scripting. This is CWE-79 (OWASP Top 10 A03 Injection).
Compiled Razor views are generated methods and are deliberately included: the issue then points to the view. Methods that also assemble the string (
String.Concat(),
String.Format(), interpolation,
StringBuilder) are reported with the
High severity, since the raw fragment is built rather than picked from a trusted source.
Flagged Codepublic class CommentViewComponent : ViewComponent {
public IViewComponentResult Invoke(Comment comment) {
var html = new HtmlString("<p>" + comment.Body + "</p>"); // flagged: raw HTML
return View(html);
}
}
Fixed Codepublic class CommentViewComponent : ViewComponent {
public IViewComponentResult Invoke(Comment comment) {
return View(comment); // the view writes @Model.Body, encoded by Razor
}
}
How to Fix:Let the view engine encode the value: write
@Model.Body instead of
@Html.Raw(Model.Body). When markup must be produced from data, build it with an
IHtmlContentBuilder and
Append() (encoded) the dynamic parts, or sanitize the fragment with an allow-list HTML sanitizer before wrapping it in an
HtmlString.
Suppress the issue when the raw string is a constant or comes from a trusted source, such as a resource file or markup authored by administrators only.
// <Name>Review HTML-encoding bypasses</Name>
// <Id>ND3133:ReviewHtmlEncodingBypasses</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
// Razor and Blazor HTML-encode every value by default; these members are the explicit opt-outs.
let rawHtmlMembers = ThirdParty.Methods.Where(m =>
// @Html.Raw(value) in MVC and Razor Pages, both ASP.NET Core and ASP.NET MVC 5
(m.SimpleName == "Raw" &&
m.ParentType.SimpleNameEqualsAny("IHtmlHelper", "IHtmlHelper<TModel>", "HtmlHelper", "HtmlHelper<TModel>") &&
m.ParentType.FullName.StartsWithAny("Microsoft.AspNetCore.Mvc.", "System.Web.Mvc.")) ||
// new HtmlString(value), new MarkupString(value), MvcHtmlString.Create(value)
(m.NbParameters == 1 && (m.IsConstructor || m.SimpleName == "Create") &&
m.ParentType.FullNameEqualsAny(
"Microsoft.AspNetCore.Html.HtmlString",
"Microsoft.AspNetCore.Components.MarkupString",
"System.Web.HtmlString",
"System.Web.Mvc.MvcHtmlString")) ||
// Tag helpers and IHtmlContentBuilder: AppendHtml(string) / SetHtmlContent(string) skip the encoder
(m.SimpleNameEqualsAny("AppendHtml", "SetHtmlContent") &&
m.Name.Contains("String") &&
m.ParentType.FullName.StartsWithAny("Microsoft.AspNetCore.Html.", "Microsoft.AspNetCore.Razor.TagHelpers.")))
.ToHashSetEx()
where rawHtmlMembers.Any()
// Compiled Razor views are generated methods: they are the primary place where Html.Raw() is used,
// hence generated methods are NOT excluded here.
from m in Application.Methods.UsingAny(rawHtmlMembers)
// The static constructor of a compiled Razor view only builds the tag helper attribute values
// written literally in the markup: new HtmlString("stylesheet") wraps a constant, never user input.
where !(m.IsClassConstructor &&
m.ParentType.HasAttribute("Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemMetadataAttribute".AllowNoMatch()) &&
m.MethodsCalled.Any(mc => mc.IsConstructor &&
mc.ParentType.FullName == "Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute"))
let rawHtmlMembersUsed = m.MethodsCalled.Intersect(rawHtmlMembers).ToArray()
// A raw fragment assembled with a string operation is the riskiest shape.
let composesString = m.MethodsCalled.Any(mc =>
(mc.ParentType.FullName == "System.String" && mc.SimpleNameEqualsAny("Concat", "Format", "Join", "Replace", "Insert")) ||
mc.ParentType.FullNameEqualsAny("System.Text.StringBuilder", "System.Runtime.CompilerServices.DefaultInterpolatedStringHandler"))
select new {
m,
rawHtmlMembersUsed,
composesString,
Debt = 15.ToMinutes().ToDebt(),
Severity = composesString ? Severity.High : Severity.Medium
}
//<Expl>
// The method *{0}* calls *{1}*, which emits a string as HTML without
// encoding it. If any part of that string derives from user input, a
// script injected in it runs in the browser of every visitor (XSS).
//</Expl>
// <Desc>
// Matches methods that bypass the HTML encoder that Razor and Blazor
// apply by default: *Html.Raw()*, *new HtmlString()*,
// *new MarkupString()*, *MvcHtmlString.Create()*, and the
// *AppendHtml(string)* / *SetHtmlContent(string)* members of tag helpers
// and *IHtmlContentBuilder*.
//
// Each of these members tells the view engine *trust this string, it is
// already HTML*. When the string contains data that a user controls (a
// comment, a profile field, a query string value), a payload such as
// *<script>* is rendered as markup and executed in the browser
// of every visitor: **cross-site scripting**. This is CWE-79 (OWASP Top
// 10 A03 Injection).
//
// Compiled Razor views are generated methods and are deliberately
// included: the issue then points to the view. Methods that also
// assemble the string (*String.Concat()*, *String.Format()*,
// interpolation, *StringBuilder*) are reported with the *High* severity,
// since the raw fragment is built rather than picked from a trusted
// source.
//
// **Flagged Code**
// ```csharp
// public class CommentViewComponent : ViewComponent {
// public IViewComponentResult Invoke(Comment comment) {
// var html = new HtmlString("<p>" + comment.Body + "</p>"); // flagged: raw HTML
// return View(html);
// }
// }
// ```
//
// **Fixed Code**
// ```csharp
// public class CommentViewComponent : ViewComponent {
// public IViewComponentResult Invoke(Comment comment) {
// return View(comment); // the view writes @Model.Body, encoded by Razor
// }
// }
// ```
// </Desc>
// <HowToFix>
// Let the view engine encode the value: write *@Model.Body* instead of
// *@Html.Raw(Model.Body)*. When markup must be produced from data, build
// it with an *IHtmlContentBuilder* and *Append()* (encoded) the dynamic
// parts, or sanitize the fragment with an allow-list HTML sanitizer
// before wrapping it in an *HtmlString*.
//
// Suppress the issue when the raw string is a constant or comes from a
// trusted source, such as a resource file or markup authored by
// administrators only.
// </HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review explicitly disabled request validation - Security
Matches ASP.NET MVC controllers, actions and model properties that explicitly opt out of request validation with
[ValidateInput(false)] or
[AllowHtml].
Request validation is the ASP.NET safety net that rejects requests containing HTML-looking input. Disabling it is legitimate for rich-text editors, but then every value received through that action or property can carry a script: it must be sanitized with an allow-list sanitizer before being stored, and HTML-encoded whenever it is rendered. A controller-level
[ValidateInput(false)] is reported with the
High severity since it opens every action of the controller.
This is CWE-79
Cross-site Scripting (OWASP Top 10 A03 Injection). ASP.NET Core has no request validation: the rule is silent there.
Flagged Code[ValidateInput(false)] // flagged: whole controller
public class ArticleController : Controller {
[HttpPost]
public ActionResult Save(Article article) {
_repository.Save(article); // raw HTML stored as-is
return RedirectToAction("Index");
}
}
Fixed Codepublic class ArticleController : Controller { // request validation stays on
[HttpPost]
public ActionResult Save(Article article) { // article.Body is Markdown, not HTML
_repository.Save(article); // markup submissions are rejected
return RedirectToAction("Index");
}
}
How to Fix:Keep request validation on and accept a format that carries no markup (Markdown, BBCode, plain text) rendered server-side. When rich HTML must be accepted, narrow the opt-out to the single property that receives it (
[AllowHtml] on that property, never
[ValidateInput(false)] on a controller), sanitize the value with an allow-list HTML sanitizer before storing it, rely on Razor encoding when rendering it, then suppress the issue on that property.
// <Name>Review explicitly disabled request validation</Name>
// <Id>ND3134:ReviewExplicitlyDisabledRequestValidation</Id>
warnif count > 0
// Third-party types are part of the code model only when the application uses them.
// ASP.NET MVC 5 / Web Forms request validation rejects HTML-looking input; these two attributes disable it.
// ASP.NET Core has no request validation, so this rule is silent on ASP.NET Core applications.
let validateInputAttr = ThirdParty.Types.WithFullName("System.Web.Mvc.ValidateInputAttribute").FirstOrDefault()
let allowHtmlAttr = ThirdParty.Types.WithFullName("System.Web.Mvc.AllowHtmlAttribute").FirstOrDefault()
where validateInputAttr != null || allowHtmlAttr != null
// [ValidateInput(false)]: the boolean is the first constructor argument, readable from the attribute metadata.
let disablesValidation = new Func<IAttributeTarget, bool>(x =>
(validateInputAttr != null &&
x.AttributeTagsOf(validateInputAttr).Any(tag =>
tag.Parameters.Any(p => p.Name == "Arg0" && p.Value.ToString().ToLower() == "false"))) ||
(allowHtmlAttr != null && x.AttributeClassesThatTagMe.Contains(allowHtmlAttr)))
from t in Application.Types
where !t.IsGeneratedByCompiler
// [ValidateInput(false)] on an action or a controller, [AllowHtml] on a model property (its getter and setter)
let membersDisablingValidation = t.Methods.Cast<IMember>().Concat(t.Fields.Cast<IMember>())
.Where(x => disablesValidation(x)).ToArray()
let typeDisablesValidation = disablesValidation(t)
where typeDisablesValidation || membersDisablingValidation.Length > 0
select new {
t,
membersDisablingValidation,
typeDisablesValidation,
Debt = (10 + 10 * membersDisablingValidation.Length).ToMinutes().ToDebt(),
Severity = typeDisablesValidation ? Severity.High : Severity.Medium
}
//<Expl>
// The type *{0}* disables ASP.NET request validation, on itself or on
// *{1}*. Markup and script submitted by a caller then reach the
// application unfiltered: make sure it is encoded or sanitized before
// being stored or rendered.
//</Expl>
// <Desc>
// Matches ASP.NET MVC controllers, actions and model properties that
// explicitly opt out of request validation with *[ValidateInput(false)]*
// or *[AllowHtml]*.
//
// Request validation is the ASP.NET safety net that rejects requests
// containing HTML-looking input. Disabling it is legitimate for rich-text
// editors, but then every value received through that action or property
// can carry a script: it must be sanitized with an allow-list sanitizer
// before being stored, and HTML-encoded whenever it is rendered. A
// controller-level *[ValidateInput(false)]* is reported with the *High*
// severity since it opens every action of the controller.
//
// This is CWE-79 *Cross-site Scripting* (OWASP Top 10 A03 Injection).
// ASP.NET Core has no request validation: the rule is silent there.
//
// **Flagged Code**
// ```csharp
// [ValidateInput(false)] // flagged: whole controller
// public class ArticleController : Controller {
// [HttpPost]
// public ActionResult Save(Article article) {
// _repository.Save(article); // raw HTML stored as-is
// return RedirectToAction("Index");
// }
// }
// ```
//
// **Fixed Code**
// ```csharp
// public class ArticleController : Controller { // request validation stays on
// [HttpPost]
// public ActionResult Save(Article article) { // article.Body is Markdown, not HTML
// _repository.Save(article); // markup submissions are rejected
// return RedirectToAction("Index");
// }
// }
// ```
// </Desc>
// <HowToFix>
// Keep request validation on and accept a format that carries no markup
// (Markdown, BBCode, plain text) rendered server-side. When rich HTML
// must be accepted, narrow the opt-out to the single property that
// receives it (*[AllowHtml]* on that property, never
// *[ValidateInput(false)]* on a controller), sanitize the value with an
// allow-list HTML sanitizer before storing it, rely on Razor encoding
// when rendering it, then suppress the issue on that property.
// </HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review redirects to a URL that is not validated as local - Security
Matches methods calling
Redirect(url) (or its permanent / preserve-method variants) on a controller, page model, HTTP response or results helper, that take a string parameter or read request data (query, form, headers, route values) and never call
Url.IsLocalUrl() nor
LocalRedirect(). A
returnUrl forwarded as-is is the classic
open redirect used in phishing campaigns.
This is CWE-601
URL Redirection to Untrusted Site (review).
Flagged Codepublic IActionResult AfterLogin(string returnUrl)
{
return Redirect(returnUrl); // flagged: any absolute URL is accepted
}
Fixed Codepublic IActionResult AfterLogin(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl))
return LocalRedirect(returnUrl); // only paths on this site
return RedirectToAction("Index");
}
How to Fix:Use
LocalRedirect(returnUrl) or guard with
if (Url.IsLocalUrl(returnUrl)) ... else RedirectToAction("Index"). For legitimate external redirects, validate the host against a whitelist. Suppress the issue when the URL is built from constants only, from the server's own configuration (an administrator-controlled base URL), or as a relative URI computed against the request's own scheme and host (
Uri.MakeRelativeUri between two URIs built on the same host): such a target cannot leave the site.
// <Name>Review redirects to a URL that is not validated as local</Name>
// <Id>ND3126:ReviewRedirectsToUrlNotValidatedAsLocal</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
let redirectMembers = ThirdParty.Methods.Where(m =>
m.SimpleNameEqualsAny("Redirect", "RedirectPermanent", "RedirectPreserveMethod", "RedirectPermanentPreserveMethod") &&
m.Name.Contains("(String") &&
m.ParentType.FullNameEqualsAny(
"Microsoft.AspNetCore.Mvc.ControllerBase", "Microsoft.AspNetCore.Mvc.RazorPages.PageModel",
"Microsoft.AspNetCore.Http.HttpResponse", "Microsoft.AspNetCore.Http.Results", "Microsoft.AspNetCore.Http.TypedResults",
"System.Web.HttpResponse", "System.Web.HttpResponseBase", "System.Web.Mvc.Controller"))
.ToHashSetEx()
where redirectMembers.Any()
let localUrlChecks = ThirdParty.Methods.Where(m =>
m.SimpleNameEqualsAny("IsLocalUrl", "LocalRedirect", "LocalRedirectPermanent", "LocalRedirectPreserveMethod", "LocalRedirectPermanentPreserveMethod"))
.ToHashSetEx()
// Request data read in the method: query string, form, headers, route values...
let requestReaders = ThirdParty.Methods.Where(m =>
m.IsPropertyGetter &&
m.SimpleNameEqualsAny("get_Query", "get_QueryString", "get_Form", "get_Headers", "get_RouteValues", "get_Cookies", "get_Path", "get_Referer", "get_UrlReferrer") &&
m.ParentType.FullNameEqualsAny("Microsoft.AspNetCore.Http.HttpRequest", "System.Web.HttpRequest", "System.Web.HttpRequestBase", "Microsoft.AspNetCore.Http.Headers.RequestHeaders"))
.ToHashSetEx()
from m in Application.Methods.UsingAny(redirectMembers)
where !m.MethodsCalled.Intersect(localUrlChecks).Any()
// The redirected URL can be influenced by the caller: a string parameter or request data.
where m.Name.Contains("String") || m.MethodsCalled.Intersect(requestReaders).Any()
select new {
m,
redirectMembersUsed = m.MethodsCalled.Intersect(redirectMembers).ToArray(),
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* redirects with *{1}* to a URL that may come from a parameter
// or from the request, without checking that it is local. A crafted link
// can bounce the user to a phishing site under the application's own domain.
//</Expl>
//<Desc>
// Matches methods calling *Redirect(url)* (or its permanent /
// preserve-method variants) on a controller, page model, HTTP response or
// results helper, that take a string parameter or read request data
// (query, form, headers, route values) and never call *Url.IsLocalUrl()*
// nor *LocalRedirect()*. A *returnUrl* forwarded as-is is the classic
// *open redirect* used in phishing campaigns.
//
// This is CWE-601 *URL Redirection to Untrusted Site* (review).
//
// **Flagged Code**
// ```csharp
// public IActionResult AfterLogin(string returnUrl)
// {
// return Redirect(returnUrl); // flagged: any absolute URL is accepted
// }
// ```
//
// **Fixed Code**
// ```csharp
// public IActionResult AfterLogin(string returnUrl)
// {
// if (Url.IsLocalUrl(returnUrl))
// return LocalRedirect(returnUrl); // only paths on this site
// return RedirectToAction("Index");
// }
// ```
//</Desc>
//<HowToFix>
// Use *LocalRedirect(returnUrl)* or guard with
// *if (Url.IsLocalUrl(returnUrl)) ... else RedirectToAction("Index")*.
// For legitimate external redirects, validate the host against a
// whitelist. Suppress the issue when the URL is built from constants only,
// from the server's own configuration (an administrator-controlled base URL),
// or as a relative URI computed against the request's own scheme and host
// (*Uri.MakeRelativeUri* between two URIs built on the same host): such a
// target cannot leave the site.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review request-influenced outbound HTTP calls (SSRF) - Security
Matches public actions of MVC controllers, API controllers, Razor page models and SignalR hubs that take a string or a
Uri parameter and either call an HTTP client directly (
HttpClient,
HttpRequestMessage,
WebRequest,
WebClient, RestSharp) or carry a name that says they fetch a remote resource (
Url,
Uri,
Remote,
Fetch,
Download,
Proxy,
Webhook,
Callback). The
callDepthToHttpClient column gives the length of the shortest call path to an HTTP client (0 when the client sits behind an interface), and
validatesUri tells whether the action itself inspects the host, scheme or IP address of a URI.
The rule does not prove that the received value flows into the outbound request: it lists the endpoints where that flow must be reviewed. When the value does reach the request address, an attacker steers the server toward hosts it can reach and the attacker cannot: internal admin services, cloud instance metadata (
169.254.169.254), databases. This is
server-side request forgery, CWE-918 (OWASP Top 10 A10).
Flagged Codepublic class PreviewController : ControllerBase {
[HttpGet("preview")]
public async Task<IActionResult> Preview(string url) { // flagged: calls HttpClient
var html = await _httpClient.GetStringAsync(url); // url chosen by the caller
return Content(Summarize(html));
}
}
Fixed Codepublic class PreviewController : ControllerBase {
[HttpGet("preview")]
public async Task<IActionResult> Preview(Guid feedId) { // an identifier, not an address
var feed = await _feeds.FindAsync(feedId); // address from trusted storage
if (feed == null) return NotFound();
var html = await _httpClient.GetStringAsync(feed.Url);
return Content(Summarize(html));
}
}
How to Fix:Never build the outbound address from a value received in the request. Map the value to an identifier (a row, a configured entry) and read the address from trusted storage, or validate the received URI against an allow-list of hosts and schemes: parse it with
Uri.TryCreate(), accept
https only, compare
Uri.Host with the allowed hosts, reject loopback, link-local and private IP ranges, and disable redirects on the
HttpClientHandler so a permitted host cannot bounce the request.
Suppress the issue when the string parameter cannot influence the address, for instance when it only names a format or a culture.
// <Name>Review request-influenced outbound HTTP calls (SSRF)</Name>
// <Id>ND3136:ReviewRequestInfluencedOutboundHttpCalls</Id>
warnif count > 0
// Third-party types are part of the code model only when the application uses them.
let controllerBaseTypes = ThirdParty.Types.WithFullNameIn(
"Microsoft.AspNetCore.Mvc.ControllerBase",
"Microsoft.AspNetCore.Mvc.RazorPages.PageModel",
"Microsoft.AspNetCore.SignalR.Hub",
"System.Web.Mvc.Controller",
"System.Web.Http.ApiController").ToArray()
where controllerBaseTypes.Any()
// Members that open an outbound HTTP connection to an address given as a string or a Uri.
let httpClientMembers = ThirdParty.Methods.Where(m =>
(m.ParentType.FullName == "System.Net.Http.HttpClient" && m.IsPublic && !m.IsConstructor &&
m.SimpleName.EndsWith("Async") && m.Name.ContainsAny("String", "Uri")) ||
(m.ParentType.FullName == "System.Net.Http.HttpRequestMessage" && m.IsConstructor && m.Name.ContainsAny("String", "Uri")) ||
(m.ParentType.FullNameEqualsAny("System.Net.WebRequest", "System.Net.HttpWebRequest") && m.SimpleName.StartsWith("Create")) ||
(m.ParentType.FullName == "System.Net.WebClient" && m.SimpleName.StartsWithAny("Download", "Upload", "OpenRead", "OpenWrite")) ||
(m.ParentType.FullName.StartsWith("RestSharp.") && (m.SimpleNameEqualsAny("ExecuteAsync", "Execute") || (m.IsConstructor && m.ParentType.SimpleName == "RestRequest"))))
.ToHashSetEx()
where httpClientMembers.Any()
// Length of the shortest call path from each application method down to an HTTP client member:
// 1 = direct call, 2 = through one helper... The fill only touches methods that actually reach the client.
// A path broken by an interface (the client is behind an injected service) is not seen.
let depthToHttpClient = httpClientMembers.FillIterative(methods => methods.SelectMany(m => m.MethodsCallingMe))
// The action itself inspects the host, scheme or IP address: a hint that an allow-list is applied.
let uriValidationMembers = ThirdParty.Methods.Where(m =>
(m.ParentType.FullName == "System.Uri" && m.SimpleNameEqualsAny("get_Host", "get_IsLoopback", "get_Scheme", "get_DnsSafeHost", "get_IdnHost")) ||
(m.ParentType.FullName == "System.Net.IPAddress" && m.SimpleNameEqualsAny("TryParse", "Parse", "IsLoopback")))
.ToHashSetEx()
from t in Application.Types.ThatDeriveFromAny(controllerBaseTypes)
where !t.IsGeneratedByCompiler
from m in t.Methods
where m.IsPublic && !m.IsConstructor && !m.IsGeneratedByCompiler &&
// The endpoint receives a string or a Uri from the caller...
m.Name.ContainsAny("String", "Uri") &&
// ...and either calls an HTTP client itself, or its name says it fetches a remote resource.
// The name test is case-sensitive on purpose: PascalCase words only ("Download" but not "loaded").
((depthToHttpClient[m] != null && depthToHttpClient[m] <= 1) ||
m.SimpleNameContainsAny("Url", "Uri", "Remote", "Fetch", "Download", "Proxy", "Webhook", "Callback"))
// 0 when the HTTP client is reached only through an interface
let callDepthToHttpClient = depthToHttpClient[m] != null ? (int)depthToHttpClient[m].Value : 0
let validatesUri = m.MethodsCalled.Intersect(uriValidationMembers).Any()
orderby m.FullName
select new {
m,
callDepthToHttpClient,
validatesUri,
Debt = 20.ToMinutes().ToDebt(),
Severity = validatesUri ? Severity.Medium : Severity.High
}
//<Expl>
// The web endpoint *{0}* receives a string or a URI and fetches a remote
// resource. If the destination derives from the value received, the
// server can be made to request internal services, cloud metadata
// endpoints or arbitrary hosts.
//</Expl>
// <Desc>
// Matches public actions of MVC controllers, API controllers, Razor
// page models and SignalR hubs that take a string or a *Uri* parameter
// and either call an HTTP client directly (*HttpClient*,
// *HttpRequestMessage*, *WebRequest*, *WebClient*, RestSharp) or carry a
// name that says they fetch a remote resource (*Url*, *Uri*, *Remote*,
// *Fetch*, *Download*, *Proxy*, *Webhook*, *Callback*). The
// *callDepthToHttpClient* column gives the length of the shortest call
// path to an HTTP client (0 when the client sits behind an interface),
// and *validatesUri* tells whether the action itself inspects the host,
// scheme or IP address of a URI.
//
// The rule does not prove that the received value flows into the
// outbound request: it lists the endpoints where that flow must be
// reviewed. When the value does reach the request address, an attacker
// steers the server toward hosts it can reach and the attacker cannot:
// internal admin services, cloud instance metadata (*169.254.169.254*),
// databases. This is **server-side request forgery**, CWE-918 (OWASP
// Top 10 A10).
//
// **Flagged Code**
// ```csharp
// public class PreviewController : ControllerBase {
// [HttpGet("preview")]
// public async Task<IActionResult> Preview(string url) { // flagged: calls HttpClient
// var html = await _httpClient.GetStringAsync(url); // url chosen by the caller
// return Content(Summarize(html));
// }
// }
// ```
//
// **Fixed Code**
// ```csharp
// public class PreviewController : ControllerBase {
// [HttpGet("preview")]
// public async Task<IActionResult> Preview(Guid feedId) { // an identifier, not an address
// var feed = await _feeds.FindAsync(feedId); // address from trusted storage
// if (feed == null) return NotFound();
// var html = await _httpClient.GetStringAsync(feed.Url);
// return Content(Summarize(html));
// }
// }
// ```
// </Desc>
// <HowToFix>
// Never build the outbound address from a value received in the request.
// Map the value to an identifier (a row, a configured entry) and read the
// address from trusted storage, or validate the received URI against an
// allow-list of hosts and schemes: parse it with *Uri.TryCreate()*,
// accept *https* only, compare *Uri.Host* with the allowed hosts, reject
// loopback, link-local and private IP ranges, and disable redirects on
// the *HttpClientHandler* so a permitted host cannot bounce the request.
//
// Suppress the issue when the string parameter cannot influence the
// address, for instance when it only names a format or a culture.
// </HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review insecure ASP.NET Core CORS policies and unguarded developer exception page - Security
Matches startup methods where: - a CORS policy combines
AllowAnyOrigin() (or a permissive
SetIsOriginAllowed()) with
AllowCredentials(): any website can then make authenticated cross-origin requests with the user's cookies. ASP.NET Core rejects the literal
AllowAnyOrigin + AllowCredentials pair at runtime, which is why
SetIsOriginAllowed(_ => true) is the common workaround, hence its presence in this rule; -
UseDeveloperExceptionPage() is called without an
IsDevelopment() (or
IsEnvironment()) check in the same method or its caller: stack traces, source snippets, headers and cookies would be shown to anyone triggering an exception in production.
These are CWE-942
Permissive Cross-domain Policy with Untrusted Domains and CWE-209
Generation of Error Message Containing Sensitive Information.
Flagged Codebuilder.Services.AddCors(o => o.AddPolicy("open", p =>
p.SetIsOriginAllowed(_ => true) // flagged: any origin...
.AllowCredentials())); // ...with the user's cookies
var app = builder.Build();
app.UseDeveloperExceptionPage(); // flagged: no environment check
Fixed Codebuilder.Services.AddCors(o => o.AddPolicy("partners", p =>
p.WithOrigins("https://partner.example.com")
.AllowCredentials()));
var app = builder.Build();
if (app.Environment.IsDevelopment())
app.UseDeveloperExceptionPage();
else
app.UseExceptionHandler("/error");
How to Fix:List the allowed origins explicitly with
WithOrigins(...) when credentials are needed. Wrap the developer exception page in
if (app.Environment.IsDevelopment()) and use
UseExceptionHandler() elsewhere. Suppress the issue when the origin predicate is a real whitelist check.
// <Name>Review insecure ASP.NET Core CORS policies and unguarded developer exception page</Name>
// <Id>ND3116:ReviewInsecureCorsPolicyAndUnguardedDeveloperExceptionPage</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
let corsMembers = ThirdParty.Types.WithFullNameIn(
"Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder",
"Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy").ChildMethods().ToArray()
let allowAnyOrigin = corsMembers.Where(m => m.SimpleNameEqualsAny("AllowAnyOrigin", "set_AllowAnyOrigin", "SetIsOriginAllowed", "set_IsOriginAllowed")).ToHashSetEx()
let allowCredentials = corsMembers.Where(m => m.SimpleNameEqualsAny("AllowCredentials", "set_SupportsCredentials")).ToHashSetEx()
let devExceptionPage = ThirdParty.Types.WithFullName("Microsoft.AspNetCore.Builder.DeveloperExceptionPageExtensions")
.ChildMethods().Where(m => m.SimpleName == "UseDeveloperExceptionPage").ToHashSetEx()
let isDevelopment = ThirdParty.Types.WithFullNameIn(
"Microsoft.Extensions.Hosting.HostEnvironmentEnvExtensions",
"Microsoft.AspNetCore.Hosting.HostingEnvironmentExtensions")
.ChildMethods().Where(m => m.SimpleNameEqualsAny("IsDevelopment", "IsEnvironment", "IsProduction", "IsStaging")).ToHashSetEx()
where (allowAnyOrigin.Any() && allowCredentials.Any()) || devExceptionPage.Any()
from m in Application.Methods.UsingAny(allowAnyOrigin.Concat(devExceptionPage))
let anyOriginWithCredentials = m.MethodsCalled.Any(x => allowAnyOrigin.Contains(x)) && m.MethodsCalled.Any(x => allowCredentials.Contains(x))
let unguardedDevPage = m.MethodsCalled.Any(x => devExceptionPage.Contains(x)) &&
!m.MethodsCalled.Any(x => isDevelopment.Contains(x)) &&
!m.MethodsCallingMe.Any(caller => caller.MethodsCalled.Any(x => isDevelopment.Contains(x)))
where anyOriginWithCredentials || unguardedDevPage
select new {
m,
problem = anyOriginWithCredentials ? "CORS policy allows credentials for any origin" : "UseDeveloperExceptionPage() not guarded by an environment check",
Debt = 20.ToMinutes().ToDebt(),
Severity = anyOriginWithCredentials ? Severity.High : Severity.Medium
}
//<Expl>
// The *{0}* configures the web host insecurely: {1}.
//</Expl>
//<Desc>
// Matches startup methods where:
// - a CORS policy combines *AllowAnyOrigin()* (or a permissive
// *SetIsOriginAllowed()*) with *AllowCredentials()*: any website can then
// make authenticated cross-origin requests with the user's cookies.
// ASP.NET Core rejects the literal *AllowAnyOrigin + AllowCredentials*
// pair at runtime, which is why *SetIsOriginAllowed(_ => true)* is the
// common workaround, hence its presence in this rule;
// - *UseDeveloperExceptionPage()* is called without an *IsDevelopment()*
// (or *IsEnvironment()*) check in the same method or its caller: stack
// traces, source snippets, headers and cookies would be shown to anyone
// triggering an exception in production.
//
// These are CWE-942 *Permissive Cross-domain Policy with Untrusted Domains*
// and CWE-209 *Generation of Error Message Containing Sensitive Information*.
//
// **Flagged Code**
// ```csharp
// builder.Services.AddCors(o => o.AddPolicy("open", p =>
// p.SetIsOriginAllowed(_ => true) // flagged: any origin...
// .AllowCredentials())); // ...with the user's cookies
// var app = builder.Build();
// app.UseDeveloperExceptionPage(); // flagged: no environment check
// ```
//
// **Fixed Code**
// ```csharp
// builder.Services.AddCors(o => o.AddPolicy("partners", p =>
// p.WithOrigins("https://partner.example.com")
// .AllowCredentials()));
// var app = builder.Build();
// if (app.Environment.IsDevelopment())
// app.UseDeveloperExceptionPage();
// else
// app.UseExceptionHandler("/error");
// ```
//</Desc>
//<HowToFix>
// List the allowed origins explicitly with *WithOrigins(...)* when
// credentials are needed. Wrap the developer exception page in
// *if (app.Environment.IsDevelopment())* and use *UseExceptionHandler()*
// elsewhere. Suppress the issue when the origin predicate is a real
// whitelist check.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review cookies created without HttpOnly and Secure flags - Security
Matches methods that instantiate
CookieOptions,
CookieBuilder or
HttpCookie without assigning both
HttpOnly and
Secure (or
SecurePolicy), and methods calling
Response.Cookies.Append(name, value) without options while the application does not use
UseCookiePolicy(). The rule cannot see the assigned value: it reports the missing assignment, which in practice means the insecure default.
This is CWE-1004
Sensitive Cookie Without 'HttpOnly' Flag and CWE-614
Sensitive Cookie in HTTPS Session Without 'Secure' Attribute.
Flagged Codepublic void RememberSession(string token)
{
var options = new CookieOptions { Expires = DateTimeOffset.UtcNow.AddDays(7) };
Response.Cookies.Append("session", token, options); // flagged: HttpOnly and Secure not set
}
Fixed Codepublic void RememberSession(string token)
{
var options = new CookieOptions
{
Expires = DateTimeOffset.UtcNow.AddDays(7),
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict
};
Response.Cookies.Append("session", token, options);
}
How to Fix:Set
HttpOnly = true,
Secure = true (or
SecurePolicy = CookieSecurePolicy.Always) and a
SameSite mode on every cookie carrying session or identity data, or apply them globally with
app.UseCookiePolicy(new CookiePolicyOptions { HttpOnly = HttpOnlyPolicy.Always, Secure = CookieSecurePolicy.Always }). Suppress the issue for cookies that scripts legitimately need to read (e.g. a UI preference).
// <Name>Review cookies created without HttpOnly and Secure flags</Name>
// <Id>ND3117:ReviewCookiesCreatedWithoutHttpOnlyAndSecureFlags</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
let cookieTypes = ThirdParty.Types.WithFullNameIn(
"Microsoft.AspNetCore.Http.CookieOptions",
"Microsoft.AspNetCore.Http.CookieBuilder",
"System.Web.HttpCookie").ToArray()
where cookieTypes.Any()
let cookieCtors = cookieTypes.ChildMethods().Where(m => m.IsConstructor).ToHashSetEx()
let httpOnlySetters = cookieTypes.ChildMethods().Where(m => m.SimpleName == "set_HttpOnly").ToHashSetEx()
let secureSetters = cookieTypes.ChildMethods().Where(m => m.SimpleNameEqualsAny("set_Secure", "set_SecurePolicy")).ToHashSetEx()
// response.Cookies.Append(name, value) without options gets neither flag,
// unless a cookie policy middleware applies them globally.
let appUsesCookiePolicy = Application.Methods.Any(m =>
m.IsUsing("Microsoft.AspNetCore.Builder.CookiePolicyAppBuilderExtensions.UseCookiePolicy(IApplicationBuilder)".AllowNoMatch()) ||
m.IsUsing("Microsoft.AspNetCore.Builder.CookiePolicyAppBuilderExtensions.UseCookiePolicy(IApplicationBuilder,CookiePolicyOptions)".AllowNoMatch()))
let appendWithoutOptions = appUsesCookiePolicy ? new IMethod[0] :
ThirdParty.Types.WithFullName("Microsoft.AspNetCore.Http.IResponseCookies")
.ChildMethods().Where(m => m.SimpleName == "Append" && m.NbParameters == 2).ToArray()
from m in Application.Methods
let createsCookie = m.MethodsCalled.Intersect(cookieCtors).Any()
let missingHttpOnly = createsCookie && !m.MethodsCalled.Intersect(httpOnlySetters).Any()
let missingSecure = createsCookie && !m.MethodsCalled.Intersect(secureSetters).Any()
let appendsWithoutOptions = m.MethodsCalled.Intersect(appendWithoutOptions).Any()
where missingHttpOnly || missingSecure || appendsWithoutOptions
select new {
m,
missing = appendsWithoutOptions ? "options (HttpOnly and Secure)" :
missingHttpOnly && missingSecure ? "HttpOnly and Secure" :
missingHttpOnly ? "HttpOnly" : "Secure",
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* creates a cookie without setting {1}. Without *HttpOnly* a
// script injected in the page can read the cookie; without *Secure* it is
// sent in clear over HTTP.
//</Expl>
//<Desc>
// Matches methods that instantiate *CookieOptions*, *CookieBuilder* or
// *HttpCookie* without assigning both *HttpOnly* and *Secure* (or
// *SecurePolicy*), and methods calling *Response.Cookies.Append(name, value)*
// without options while the application does not use
// *UseCookiePolicy()*. The rule cannot see the assigned value: it reports
// the missing assignment, which in practice means the insecure default.
//
// This is CWE-1004 *Sensitive Cookie Without 'HttpOnly' Flag* and CWE-614
// *Sensitive Cookie in HTTPS Session Without 'Secure' Attribute*.
//
// **Flagged Code**
// ```csharp
// public void RememberSession(string token)
// {
// var options = new CookieOptions { Expires = DateTimeOffset.UtcNow.AddDays(7) };
// Response.Cookies.Append("session", token, options); // flagged: HttpOnly and Secure not set
// }
// ```
//
// **Fixed Code**
// ```csharp
// public void RememberSession(string token)
// {
// var options = new CookieOptions
// {
// Expires = DateTimeOffset.UtcNow.AddDays(7),
// HttpOnly = true,
// Secure = true,
// SameSite = SameSiteMode.Strict
// };
// Response.Cookies.Append("session", token, options);
// }
// ```
//</Desc>
//<HowToFix>
// Set *HttpOnly = true*, *Secure = true* (or *SecurePolicy = CookieSecurePolicy.Always*)
// and a *SameSite* mode on every cookie carrying session or identity data,
// or apply them globally with *app.UseCookiePolicy(new CookiePolicyOptions { HttpOnly = HttpOnlyPolicy.Always, Secure = CookieSecurePolicy.Always })*.
// Suppress the issue for cookies that scripts legitimately need to read
// (e.g. a UI preference).
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review regular expressions without a match timeout (ReDoS) - Security
Matches methods that create or run a
Regex through an overload without a
TimeSpan matchTimeout argument (constructors,
Regex.IsMatch(),
Match(),
Matches(),
Replace(),
Split(),
Count(),
EnumerateMatches()) or that declare a
[GeneratedRegex] without the
matchTimeoutMilliseconds argument. The .NET backtracking engine can be driven into catastrophic backtracking by crafted input, a Regular Expression Denial of Service (ReDoS):
https://learn.microsoft.com/dotnet/standard/base-types/best-practices-regex#use-time-out-values The rule is muted when the application sets the process-wide
REGEX_DEFAULT_MATCH_TIMEOUT through
AppDomain.SetData().
This is CWE-1333
Inefficient Regular Expression Complexity.
Flagged Codepublic bool IsValidTitle(string title)
{
return Regex.IsMatch(title, @"^(\w+\s?)*$"); // flagged: no matchTimeout, nested quantifier
}
Fixed Codestatic readonly Regex s_Title =
new Regex(@"^(\w+\s?)*$", RegexOptions.None, TimeSpan.FromMilliseconds(200));
public bool IsValidTitle(string title)
{
return s_Title.IsMatch(title); // instance call, timeout set at construction
}
How to Fix:Pass a
matchTimeout (a few hundred milliseconds is usually plenty), or opt into the linear-time engine with
RegexOptions.NonBacktracking (.NET 7+), or set a process-wide default with
AppDomain.CurrentDomain.SetData("REGEX_DEFAULT_MATCH_TIMEOUT", TimeSpan.FromMilliseconds(500)). Suppress the issue when the pattern only ever runs on trusted, bounded input.
// <Name>Review regular expressions without a match timeout (ReDoS)</Name>
// <Id>ND3112:ReviewRegexWithoutMatchTimeout</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
let regexType = ThirdParty.Types.WithFullName("System.Text.RegularExpressions.Regex")
where regexType.Any()
// A process-wide default timeout makes every regex safe.
let appSetsDefaultTimeout = Application.Methods.UsingAny(
ThirdParty.Methods.WithFullName("System.AppDomain.SetData(String,Object)")).Any()
where !appSetsDefaultTimeout
// Constructors and static helpers that take a pattern but no TimeSpan matchTimeout.
let regexMembersWithoutTimeout = regexType.ChildMethods()
.Where(m => (m.IsConstructor || m.IsStatic) &&
m.IsPublic &&
m.NbParameters > 0 &&
!m.Name.Contains("TimeSpan") &&
m.SimpleNameEqualsAny(".ctor", "IsMatch", "Match", "Matches", "Replace", "Split", "Count", "EnumerateMatches", "EnumerateSplits"))
.ToHashSetEx()
// [GeneratedRegex(pattern, options, matchTimeoutMilliseconds)]: the third argument is the timeout when it is an integer.
let generatedRegexAttr = ThirdParty.Types.WithFullName("System.Text.RegularExpressions.GeneratedRegexAttribute").FirstOrDefault()
let hasNoTimeoutArg = new Func<IAttributeTag, bool>(tag =>
!tag.Parameters.Any(p => p.Name == "Arg2" && p.Value.ToString().All(c => Char.IsDigit(c))))
let generatedRegexMethods = generatedRegexAttr == null ? new IMethod[0] :
Application.Methods.TaggedWithAttribute(generatedRegexAttr).ToArray()
from m in Application.Methods.UsingAny(regexMembersWithoutTimeout).Union(generatedRegexMethods)
let regexMembersUsed = m.MethodsCalled.Where(x => regexMembersWithoutTimeout.Contains(x)).ToArray()
let isGeneratedRegexWithoutTimeout = generatedRegexAttr != null && m.AttributeTagsOf(generatedRegexAttr).Any(tag => hasNoTimeoutArg(tag))
where regexMembersUsed.Length > 0 || isGeneratedRegexWithoutTimeout
select new {
m,
regexMembersUsed,
isGeneratedRegexWithoutTimeout,
Debt = (5 + 3 * regexMembersUsed.Length).ToMinutes().ToDebt(),
Severity = regexMembersUsed.Length > 0 ? Severity.Medium : Severity.Low
}
//<Expl>
// The *{0}* {2?is a [GeneratedRegex] declared without a match timeout:uses regular expressions without a match timeout: {1}}.
// A pattern with nested quantifiers applied to hostile input can take
// exponential time and pin a CPU core.
//</Expl>
//<Desc>
// Matches methods that create or run a *Regex* through an overload without
// a *TimeSpan matchTimeout* argument (constructors, *Regex.IsMatch()*,
// *Match()*, *Matches()*, *Replace()*, *Split()*, *Count()*,
// *EnumerateMatches()*) or that declare a *[GeneratedRegex]* without the
// *matchTimeoutMilliseconds* argument. The .NET backtracking engine can be
// driven into catastrophic backtracking by crafted input, a Regular
// Expression Denial of Service (ReDoS):
// https://learn.microsoft.com/dotnet/standard/base-types/best-practices-regex#use-time-out-values
// The rule is muted when the application sets the process-wide
// *REGEX_DEFAULT_MATCH_TIMEOUT* through *AppDomain.SetData()*.
//
// This is CWE-1333 *Inefficient Regular Expression Complexity*.
//
// **Flagged Code**
// ```csharp
// public bool IsValidTitle(string title)
// {
// return Regex.IsMatch(title, @"^(\w+\s?)*$"); // flagged: no matchTimeout, nested quantifier
// }
// ```
//
// **Fixed Code**
// ```csharp
// static readonly Regex s_Title =
// new Regex(@"^(\w+\s?)*$", RegexOptions.None, TimeSpan.FromMilliseconds(200));
//
// public bool IsValidTitle(string title)
// {
// return s_Title.IsMatch(title); // instance call, timeout set at construction
// }
// ```
//</Desc>
//<HowToFix>
// Pass a *matchTimeout* (a few hundred milliseconds is usually plenty), or
// opt into the linear-time engine with *RegexOptions.NonBacktracking*
// (.NET 7+), or set a process-wide default with
// *AppDomain.CurrentDomain.SetData("REGEX_DEFAULT_MATCH_TIMEOUT", TimeSpan.FromMilliseconds(500))*.
// Suppress the issue when the pattern only ever runs on trusted,
// bounded input.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review secrets compared with non-constant-time equality - Security
Matches methods that compare strings or byte sequences with
String.Equals(),
==,
String.Compare() or
SequenceEqual() and either compute a hash / MAC / derived key in the same body, or carry a name mentioning a token, signature, HMAC, secret, OTP, password, API key, digest or nonce. Ordinary equality short-circuits on the first mismatch, so the elapsed time leaks how many leading bytes were right: a timing side channel that has been exploited over networks.
This is CWE-208
Observable Timing Discrepancy.
Flagged Codebool VerifyToken(string token, byte[] key, byte[] payload) {
using var hmac = new HMACSHA256(key);
string expected = Convert.ToHexString(hmac.ComputeHash(payload));
return expected == token; // flagged: == returns as soon as a byte differs
}
Fixed Codebool VerifyToken(string token, byte[] key, byte[] payload) {
using var hmac = new HMACSHA256(key);
byte[] expected = hmac.ComputeHash(payload);
byte[] provided = Convert.FromHexString(token);
return CryptographicOperations.FixedTimeEquals(expected, provided);
}
How to Fix:Compare secrets with
System.Security.Cryptography.CryptographicOperations.FixedTimeEquals(a, b) on the binary form of both values. For two strings, compare their UTF-16 bytes —
MemoryMarshal.AsBytes(a.AsSpan()) vs
MemoryMarshal.AsBytes(b.AsSpan()) — which is the exact ordinal equality. Do NOT re-encode them to UTF-8 first: an unpaired surrogate becomes U+FFFD, so two different strings can encode to the same bytes and compare equal. For passwords, compare the stored hash with the freshly computed hash the same way. Suppress the issue when the compared values are not secrets.
// <Name>Review secrets compared with non-constant-time equality</Name>
// <Id>ND3113:ReviewSecretsComparedWithNonConstantTimeEquality</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
let equalityMembers = ThirdParty.Types.WithFullNameIn(
"System.String", "System.StringComparer", "System.Linq.Enumerable", "System.MemoryExtensions")
.ChildMethods().Where(m => m.SimpleNameEqualsAny("Equals", "op_Equality", "op_Inequality", "Compare", "CompareOrdinal", "SequenceEqual"))
.ToHashSetEx()
// Computing a MAC, hash or derived key and then comparing it is the strongest signal.
let hashComputingMembers = ThirdParty.Methods
.Where(m => m.ParentNamespace.Name == "System.Security.Cryptography" &&
m.SimpleNameEqualsAny("ComputeHash", "TryComputeHash", "HashData", "TryHashData", "GetBytes", "Pbkdf2", "GetHashAndReset"))
.ToHashSetEx()
let constantTimeEquals = ThirdParty.Types.WithFullName("System.Security.Cryptography.CryptographicOperations")
.ChildMethods().Where(m => m.SimpleName == "FixedTimeEquals").ToHashSetEx()
where equalityMembers.Any()
from m in Application.Methods.UsingAny(equalityMembers)
where !m.MethodsCalled.Intersect(constantTimeEquals).Any()
let comparesComputedHash = m.MethodsCalled.Intersect(hashComputingMembers).Any()
// A name like VerifyToken(), ValidateSignature(), IsPasswordValid(), RedeemPasswordResetPin() or CheckApiKey()
let nameSuggestsSecret =
m.SimpleName.ContainsAny(new[] { "Token", "Signature", "Hmac", "Secret", "Otp", "Password", "Passwd", "ApiKey", "Digest", "Nonce", "Csrf", "Antiforgery" }, StringComparison.OrdinalIgnoreCase) &&
m.SimpleName.ContainsAny(new[] { "Verify", "Validate", "Valid", "Check", "Compare", "Match", "Authenticate", "Redeem", "Confirm", "Equal" }, StringComparison.OrdinalIgnoreCase) &&
!m.SimpleName.ContainsAny(new[] { "Cancellation", "Tokenize", "Tokenizer", "PublicKeyToken", "HelpToken" }, StringComparison.OrdinalIgnoreCase)
where comparesComputedHash || nameSuggestsSecret
select new {
m,
equalityMembersUsed = m.MethodsCalled.Intersect(equalityMembers).ToArray(),
comparesComputedHash,
Debt = 10.ToMinutes().ToDebt(),
Severity = comparesComputedHash ? Severity.High : Severity.Medium
}
//<Expl>
// The *{0}* {2?computes a hash or MAC and compares it:has a name suggesting it checks a secret}
// with *{1}*, which returns as soon as the first byte differs. An attacker
// measuring response times can recover the expected value byte by byte.
//</Expl>
//<Desc>
// Matches methods that compare strings or byte sequences with
// *String.Equals()*, *==*, *String.Compare()* or *SequenceEqual()* and
// either compute a hash / MAC / derived key in the same body, or carry a
// name mentioning a token, signature, HMAC, secret, OTP, password, API key,
// digest or nonce. Ordinary equality short-circuits on the first mismatch,
// so the elapsed time leaks how many leading bytes were right: a timing
// side channel that has been exploited over networks.
//
// This is CWE-208 *Observable Timing Discrepancy*.
//
// **Flagged Code**
// ```csharp
// bool VerifyToken(string token, byte[] key, byte[] payload) {
// using var hmac = new HMACSHA256(key);
// string expected = Convert.ToHexString(hmac.ComputeHash(payload));
// return expected == token; // flagged: == returns as soon as a byte differs
// }
// ```
//
// **Fixed Code**
// ```csharp
// bool VerifyToken(string token, byte[] key, byte[] payload) {
// using var hmac = new HMACSHA256(key);
// byte[] expected = hmac.ComputeHash(payload);
// byte[] provided = Convert.FromHexString(token);
// return CryptographicOperations.FixedTimeEquals(expected, provided);
// }
// ```
//</Desc>
//<HowToFix>
// Compare secrets with
// *System.Security.Cryptography.CryptographicOperations.FixedTimeEquals(a, b)*
// on the binary form of both values. For two strings, compare their UTF-16
// bytes — *MemoryMarshal.AsBytes(a.AsSpan())* vs *MemoryMarshal.AsBytes(b.AsSpan())*
// — which is the exact ordinal equality. Do NOT re-encode them to UTF-8 first:
// an unpaired surrogate becomes U+FFFD, so two different strings can encode
// to the same bytes and compare equal. For passwords, compare the stored hash
// with the freshly computed hash the same way. Suppress the issue when the
// compared values are not secrets.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review secrets passed to logging methods - Security
Matches methods that call a logging API (
ILogger /
LoggerExtensions, Serilog, NLog, log4net,
Trace,
Debug,
Console) and, in the same body, read a string field or property getter named like a password, API key, private key, token, client secret, connection string or credential. Logs are stored longer, replicated more widely and protected less than the secret store; a connection string or bearer token in a log line is one of the most frequent real-world credential leaks.
This is CWE-532
Insertion of Sensitive Information into Log File (review: the rule cannot tell whether the value is part of the message).
Flagged Codeprivate readonly string _connectionString;
void OpenDatabase(ILogger logger) {
logger.LogInformation("Opening {Db}", _connectionString); // flagged: secret logged
using var connection = new SqlConnection(_connectionString);
connection.Open();
}
Fixed Codeprivate readonly string _connectionString;
void OpenDatabase(ILogger logger) {
logger.LogInformation("Opening database {Db}", DatabaseName); // non-secret value
using var connection = CreateConnection();
connection.Open();
}
SqlConnection CreateConnection() => new SqlConnection(_connectionString); // logs nothing
How to Fix:Never pass the secret to the logger: log its presence or a redacted form (last 4 characters, a hash), and use a destructuring policy that masks sensitive properties when logging whole objects. Suppress the issue when the secret is read for another purpose in the method and demonstrably not part of the log call.
// <Name>Review secrets passed to logging methods</Name>
// <Id>ND3127:ReviewSecretsPassedToLoggingMethods</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
let loggingMembers = ThirdParty.Methods.Where(m =>
(m.ParentType.FullNameEqualsAny(
"Microsoft.Extensions.Logging.LoggerExtensions", "Microsoft.Extensions.Logging.ILogger",
"Serilog.ILogger", "Serilog.Log", "NLog.Logger", "NLog.ILogger", "log4net.ILog", "Common.Logging.ILog",
"System.Diagnostics.Trace", "System.Diagnostics.Debug", "System.Diagnostics.TraceSource", "System.Diagnostics.EventLog") &&
m.SimpleName.StartsWithAny("Log", "Write", "Trace", "Debug", "Info", "Warn", "Error", "Fatal", "Verbose", "Critical")) ||
(m.ParentType.FullName == "System.Console" && m.SimpleName.StartsWith("Write")))
.ToHashSetEx()
where loggingMembers.Any()
// Fields and property getters whose name designates a secret (hashed forms and setting names excluded).
let isSecretName = new Func<string, bool>(n =>
n.ToLower().ContainsAny("password", "passwd", "pwd", "secret", "apikey", "api_key", "privatekey", "private_key",
"accesstoken", "access_token", "authtoken", "auth_token", "refreshtoken", "refresh_token",
"clientsecret", "client_secret", "connectionstring", "connection_string", "credential") &&
!n.ToLower().ContainsAny("hash", "salt", "length", "policy", "regex", "pattern", "name", "header", "expir", "required", "attempt", "changed", "reset", "enabled", "type", "kind", "count", "provider"))
from m in Application.Methods.UsingAny(loggingMembers)
let secretFieldsRead = m.FieldsUsed.Where(f => !f.IsGeneratedByCompiler && f.FieldType != null && f.FieldType.FullName == "System.String" && isSecretName(f.SimpleName)).ToArray()
let secretGettersCalled = m.MethodsCalled.Where(mc => mc.IsPropertyGetter && mc.ReturnType != null && mc.ReturnType.FullName == "System.String" && isSecretName(mc.SimpleName)).ToArray()
where secretFieldsRead.Length > 0 || secretGettersCalled.Length > 0
select new {
m,
secretFieldsRead,
secretGettersCalled,
loggingMembersUsed = m.MethodsCalled.Intersect(loggingMembers).ToArray(),
Debt = 15.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* both logs (through *{3}*) and reads the secret-named value(s)
// {1}{2}. If the secret ends up in the log message, it is exposed to
// everyone who can read the logs, log aggregators and support bundles.
//</Expl>
//<Desc>
// Matches methods that call a logging API (*ILogger* / *LoggerExtensions*,
// Serilog, NLog, log4net, *Trace*, *Debug*, *Console*) and, in the same
// body, read a string field or property getter named like a password, API
// key, private key, token, client secret, connection string or credential.
// Logs are stored longer, replicated more widely and protected less than
// the secret store; a connection string or bearer token in a log line is
// one of the most frequent real-world credential leaks.
//
// This is CWE-532 *Insertion of Sensitive Information into Log File*
// (review: the rule cannot tell whether the value is part of the message).
//
// **Flagged Code**
// ```csharp
// private readonly string _connectionString;
//
// void OpenDatabase(ILogger logger) {
// logger.LogInformation("Opening {Db}", _connectionString); // flagged: secret logged
// using var connection = new SqlConnection(_connectionString);
// connection.Open();
// }
// ```
//
// **Fixed Code**
// ```csharp
// private readonly string _connectionString;
//
// void OpenDatabase(ILogger logger) {
// logger.LogInformation("Opening database {Db}", DatabaseName); // non-secret value
// using var connection = CreateConnection();
// connection.Open();
// }
//
// SqlConnection CreateConnection() => new SqlConnection(_connectionString); // logs nothing
// ```
//</Desc>
//<HowToFix>
// Never pass the secret to the logger: log its presence or a redacted
// form (last 4 characters, a hash), and use a destructuring policy that
// masks sensitive properties when logging whole objects. Suppress the
// issue when the secret is read for another purpose in the method and
// demonstrably not part of the log call.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review security operations that log nothing - Security
Matches publicly visible methods whose name denotes an authentication or authorization operation (sign-in, sign-out, authorize, password change or reset, token revocation) when no logging call is reachable within two call levels: not in the method, not in what it calls, not in what that calls.
A call through an interface or abstract method of the application counts as logging when one of its implementations logs: an
IAppLogger<T> adapter over
ILogger<T> is followed.
Login failures, access-control denials and credential changes are the events an incident responder looks for first. When they are not recorded, an attack cannot be detected while it happens nor reconstructed afterwards.
The rule is silent for an application that uses no logging framework at all, since it has nothing to compare against.
This is CWE-778
Insufficient Logging (OWASP Top 10 A09 Security Logging and Monitoring Failures).
Flagged Codepublic async Task<IActionResult> Login(LoginModel model) {
var user = await _users.FindByNameAsync(model.UserName);
if (user == null || !_hasher.Verify(user, model.Password))
return Unauthorized(); // flagged: the failed attempt leaves no trace
await _signInManager.SignInAsync(user, model.RememberMe);
return RedirectToAction("Index");
}
Fixed Codepublic async Task<IActionResult> Login(LoginModel model) {
var user = await _users.FindByNameAsync(model.UserName);
if (user == null || !_hasher.Verify(user, model.Password)) {
_logger.LogWarning("Failed login for {User} from {Ip}", model.UserName, RemoteIp);
return Unauthorized();
}
_logger.LogInformation("User {User} signed in", model.UserName);
await _signInManager.SignInAsync(user, model.RememberMe);
return RedirectToAction("Index");
}
How to Fix:Log the outcome of the operation, successful or not, with the subject, the source address and the time. Log the reason for a denial, never the credential itself: see the rule
Review secrets passed to logging methods. Route these events to a channel someone actually watches.
Suppress the issue when the operation is logged by a middleware, an interceptor or an audit filter that this rule cannot see from the call graph.
// <Name>Review security operations that log nothing</Name>
// <Id>ND3131:ReviewSecurityOperationsThatLogNothing</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
let loggingMembers = ThirdParty.Methods.Where(m =>
m.ParentType.FullName.StartsWithAny("Microsoft.Extensions.Logging", "Serilog", "NLog", "log4net", "Common.Logging") ||
m.ParentType.FullNameEqualsAny("System.Diagnostics.Trace", "System.Diagnostics.TraceSource", "System.Diagnostics.EventLog"))
.ToHashSetEx()
where loggingMembers.Any()
let callsLogging = new Func<IMethod, bool>(x => x.MethodsCalled.Intersect(loggingMembers).Any())
// A call through an interface or abstract method of the application, like its own IAppLogger<T>
// adapter over ILogger<T>, logs when one of the implementations logs.
let logs = new Func<IMethod, bool>(x => callsLogging(x) ||
(x.IsAbstract && x.OverridesDerived.Any(callsLogging)))
from m in Application.Methods
// A one-liner forwarding to the real implementation is not the place to log.
// These cheap checks come first, the name matching below is the costly filter.
where m.NbLinesOfCode > 3 &&
m.IsPubliclyVisible &&
!m.IsGeneratedByCompiler &&
// Names are matched case-sensitively so that a PascalCase word is required:
// *ShowDialogInParameter* must not read as *...LogIn...*
m.SimpleNameContainsAny(
"Authenticate", "Login", "SignIn", "Logout", "SignOut", "Authorize",
"ResetPassword", "ChangePassword", "RevokeToken")
// Nothing logs: neither the method, nor what it calls, nor what that calls.
where !logs(m) &&
!m.MethodsCalled.Any(mc => logs(mc) || mc.MethodsCalled.Any(mc2 => logs(mc2)))
select new {
m,
m.NbLinesOfCode,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* performs an authentication or authorization operation over
// {1} lines, and neither it nor the methods it calls write anything to a
// log. A failed or abusive attempt here leaves no trace.
//</Expl>
// <Desc>
// Matches publicly visible methods whose name denotes an authentication or
// authorization operation (sign-in, sign-out, authorize, password change
// or reset, token revocation) when no logging call is reachable within two
// call levels: not in the method, not in what it calls, not in what that
// calls.
//
// A call through an interface or abstract method of the application
// counts as logging when one of its implementations logs: an
// *IAppLogger<T>* adapter over *ILogger<T>* is followed.
//
// Login failures, access-control denials and credential changes are the
// events an incident responder looks for first. When they are not
// recorded, an attack cannot be detected while it happens nor
// reconstructed afterwards.
//
// The rule is silent for an application that uses no logging framework at
// all, since it has nothing to compare against.
//
// This is CWE-778 *Insufficient Logging* (OWASP Top 10 A09 Security
// Logging and Monitoring Failures).
//
// **Flagged Code**
// ```csharp
// public async Task<IActionResult> Login(LoginModel model) {
// var user = await _users.FindByNameAsync(model.UserName);
// if (user == null || !_hasher.Verify(user, model.Password))
// return Unauthorized(); // flagged: the failed attempt leaves no trace
// await _signInManager.SignInAsync(user, model.RememberMe);
// return RedirectToAction("Index");
// }
// ```
//
// **Fixed Code**
// ```csharp
// public async Task<IActionResult> Login(LoginModel model) {
// var user = await _users.FindByNameAsync(model.UserName);
// if (user == null || !_hasher.Verify(user, model.Password)) {
// _logger.LogWarning("Failed login for {User} from {Ip}", model.UserName, RemoteIp);
// return Unauthorized();
// }
// _logger.LogInformation("User {User} signed in", model.UserName);
// await _signInManager.SignInAsync(user, model.RememberMe);
// return RedirectToAction("Index");
// }
// ```
// </Desc>
// <HowToFix>
// Log the outcome of the operation, successful or not, with the subject,
// the source address and the time. Log the reason for a denial, never the
// credential itself: see the rule *Review secrets passed to logging
// methods*. Route these events to a channel someone actually watches.
//
// Suppress the issue when the operation is logged by a middleware, an
// interceptor or an audit filter that this rule cannot see from the call
// graph.
// </HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't use System.Random for security purposes - Security
System.Random is weak: its numbers can be predicted, which can lead to vulnerabilities in a security critical context (tokens, session ids, salts, password reset codes, CAPTCHA...).
This is CWE-338
Use of Cryptographically Weak Pseudo-Random Number Generator.
Flagged Codestring CreatePasswordResetCode() {
var random = new Random(); // flagged: predictable generator
return random.Next(100000, 999999).ToString();
}
string CreateSessionId() => Random.Shared.NextInt64().ToString("x"); // flagged: Random.Shared
Fixed Codestring CreatePasswordResetCode() =>
RandomNumberGenerator.GetInt32(100000, 1000000).ToString();
string CreateSessionId() => RandomNumberGenerator.GetHexString(32);
How to Fix:In a security critical context use
System.Security.Cryptography.RandomNumberGenerator (
GetInt32(),
GetBytes(),
GetHexString(),
GetString()): slower, but unpredictable. See:
https://stackoverflow.com/questions/32932679/using-rngcryptoserviceprovider-to-generate-random-string Otherwise keep the faster
System.Random and suppress the issues. More on
System.Random weakness:
https://stackoverflow.com/a/6842191/27194
// <Name>Don't use System.Random for security purposes</Name>
// <Id>ND3101:DontUseSystemRandomForSecurityPurposes</Id>
warnif count > 0
from m in Application.Methods
where m.CreateA("System.Random".AllowNoMatch()) ||
m.IsUsing("System.Random.get_Shared()".AllowNoMatch())
select new {
m,
Debt = 15.ToMinutes().ToDebt(),
AnnualInterest = 1.ToHours().ToAnnualInterest()
}
//<Expl>
// The *{0}* should not use *System.Random* (nor *Random.Shared*) because it is a
// *weak* pseudo-random number generator.
// This can potentially allow an attacker to predict security-sensitive
// values that are generated.
//</Expl>
//<Desc>
// **System.Random** is weak: its numbers can be predicted, which can lead
// to vulnerabilities in a security critical context (tokens, session ids,
// salts, password reset codes, CAPTCHA...).
//
// This is CWE-338 *Use of Cryptographically Weak Pseudo-Random Number Generator*.
//
// **Flagged Code**
// ```csharp
// string CreatePasswordResetCode() {
// var random = new Random(); // flagged: predictable generator
// return random.Next(100000, 999999).ToString();
// }
//
// string CreateSessionId() => Random.Shared.NextInt64().ToString("x"); // flagged: Random.Shared
// ```
//
// **Fixed Code**
// ```csharp
// string CreatePasswordResetCode() =>
// RandomNumberGenerator.GetInt32(100000, 1000000).ToString();
//
// string CreateSessionId() => RandomNumberGenerator.GetHexString(32);
// ```
//</Desc>
//<HowToFix>
// In a security critical context use
// **System.Security.Cryptography.RandomNumberGenerator** (*GetInt32()*,
// *GetBytes()*, *GetHexString()*, *GetString()*): slower, but unpredictable.
// See: https://stackoverflow.com/questions/32932679/using-rngcryptoserviceprovider-to-generate-random-string
// Otherwise keep the faster **System.Random** and suppress the issues.
// More on *System.Random* weakness: https://stackoverflow.com/a/6842191/27194
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't use CoSetProxyBlanket and CoInitializeSecurity - Security
Once managed code is JIT'ed and executed, it is too late to call
CoInitializeSecurity() or
CoSetProxyBlanket(): the CLR has already initialized the COM security environment for the whole process.
Flagged Code[DllImport("ole32.dll")] // flagged: P/Invoke declared and called from managed code
static extern int CoInitializeSecurity(IntPtr pSecDesc, int cAuthSvc, IntPtr asAuthSvc,
IntPtr pReserved1, uint dwAuthnLevel, uint dwImpLevel, IntPtr pAuthList,
uint dwCapabilities, IntPtr pReserved3);
static void Main() {
CoInitializeSecurity(IntPtr.Zero, -1, IntPtr.Zero, IntPtr.Zero, 6, 3,
IntPtr.Zero, 0, IntPtr.Zero);
}
Fixed Code// Unmanaged C++ host shim, run before the CLR is loaded: managed code declares no P/Invoke
int wmain() {
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
CoInitializeSecurity(nullptr, -1, nullptr, nullptr, RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_NONE, nullptr);
return RunManagedEntryPoint();
}
How to Fix:Don't call them from managed code. Write an unmanaged C++ "shim" calling them before loading the CLR within the process, as explained here:
https://stackoverflow.com/a/48545055/27194
// <Name>Don't use CoSetProxyBlanket and CoInitializeSecurity</Name>
// <Id>ND3100:DontUseCoSetProxyBlanketAndCoInitializeSecurity</Id>
warnif count > 0
from m in Application.Methods
where (m.HasAttribute ("System.Runtime.InteropServices.DllImportAttribute".AllowNoMatch()))
&& m.SimpleNameEqualsAny("CoSetProxyBlanket","CoInitializeSecurity")
select new {
m,
Debt = 1.ToHours().ToDebt(),
AnnualInterest = 2.ToHours().ToAnnualInterest()
}
//<Expl>
// Don't call the *P/Invoke {0}* from managed code.
//</Expl>
//<Desc>
// Once managed code is JIT'ed and executed, it is too late to call
// *CoInitializeSecurity()* or *CoSetProxyBlanket()*: the CLR has already
// initialized the COM security environment for the whole process.
//
// **Flagged Code**
// ```csharp
// [DllImport("ole32.dll")] // flagged: P/Invoke declared and called from managed code
// static extern int CoInitializeSecurity(IntPtr pSecDesc, int cAuthSvc, IntPtr asAuthSvc,
// IntPtr pReserved1, uint dwAuthnLevel, uint dwImpLevel, IntPtr pAuthList,
// uint dwCapabilities, IntPtr pReserved3);
//
// static void Main() {
// CoInitializeSecurity(IntPtr.Zero, -1, IntPtr.Zero, IntPtr.Zero, 6, 3,
// IntPtr.Zero, 0, IntPtr.Zero);
// }
// ```
//
// **Fixed Code**
// ```cpp
// // Unmanaged C++ host shim, run before the CLR is loaded: managed code declares no P/Invoke
// int wmain() {
// CoInitializeEx(nullptr, COINIT_MULTITHREADED);
// CoInitializeSecurity(nullptr, -1, nullptr, nullptr, RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
// RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_NONE, nullptr);
// return RunManagedEntryPoint();
// }
// ```
//</Desc>
//<HowToFix>
// Don't call them from managed code. Write an unmanaged C++ "shim" calling
// them before loading the CLR within the process, as explained here:
// https://stackoverflow.com/a/48545055/27194
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Review publicly visible event handlers - Security
Review publicly visible event handlers (methods with the standard
(Object,EventArgs) signature) running security critical actions: malicious code can subscribe them to an event to trigger the action on firing. Even with a security check, the handler can run from a chain of trusted callers and cannot detect malicious registration.
Flagged Codepublic class AdminPanel {
public void OnDeleteAllUsers(object sender, EventArgs e) { // flagged: public handler
_userStore.DeleteAll();
}
}
Fixed Codepublic class AdminPanel {
private void OnDeleteAllUsers(object sender, EventArgs e) { // non-public handler
if (!_currentUser.IsAdministrator) return;
_userStore.DeleteAll();
}
}
How to Fix:Make matched handlers non-public; preferably don't run security critical actions from event handlers. Suppress the issue if no such action is involved.
// <Name>Review publicly visible event handlers</Name>
// <Id>ND3104:ReviewPubliclyVisibleEventHandlers</Id>
warnif count > 0
from m in Application.Methods
where
m.IsPubliclyVisible
&& m.Name.EndsWith("(Object,EventArgs)")
select new {
m,
Debt = m.NbLinesOfCode.Linear(3, 2, 300, 12).ToMinutes().ToDebt(),
Severity = Severity.Low
}
//<Expl>
// The *public {0}* is an event handler that might perform a security critical action. Review it!
//</Expl>
//<Desc>
// Review publicly visible event handlers (methods with the standard
// *(Object,EventArgs)* signature) running security critical actions:
// malicious code can subscribe them to an event to trigger the action on
// firing. Even with a security check, the handler can run from a chain of
// trusted callers and cannot detect malicious registration.
//
// **Flagged Code**
// ```csharp
// public class AdminPanel {
// public void OnDeleteAllUsers(object sender, EventArgs e) { // flagged: public handler
// _userStore.DeleteAll();
// }
// }
// ```
//
// **Fixed Code**
// ```csharp
// public class AdminPanel {
// private void OnDeleteAllUsers(object sender, EventArgs e) { // non-public handler
// if (!_currentUser.IsAdministrator) return;
// _userStore.DeleteAll();
// }
// }
// ```
//</Desc>
//<HowToFix>
// Make matched handlers non-public; preferably don't run security critical
// actions from event handlers. Suppress the issue if no such action is
// involved.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Pointers should not be publicly visible - Security
Detects public or protected fields of type
System.IntPtr or
System.UIntPtr. Pointers access unmanaged memory; exposing them publicly lets malicious code read and write the application's unmanaged data — worse if the field is not read-only, since it can be changed to force the application onto arbitrary data.
Flagged Codepublic class NativeBuffer {
public IntPtr Handle; // flagged: public, writable pointer
protected UIntPtr Length; // flagged: protected pointer
}
Fixed Codepublic class NativeBuffer {
private readonly SafeHandle _handle; // pointer wrapped in a SafeHandle
private IntPtr _buffer; // private pointer
public int Length { get; }
}
How to Fix:Make pointers private or internal.
Estimated Debt: 15 minutes plus 10 minutes per method using the field outside its assembly. Severity:
Medium,
High if non read-only.
// <Name>Pointers should not be publicly visible</Name>
// <Id>ND3105:PointersShouldNotBePubliclyVisible</Id>
warnif count > 0
from f in Application.Fields
where
f.FieldType != null &&
f.FieldType.FullNameEqualsAny("System.IntPtr","System.UIntPtr") &&
(f.IsPubliclyVisible || f.IsProtected)
let methodsUserOutsideMyAssembly = f.MethodsUsingMe.Where(m => m.ParentAssembly != m.ParentAssembly)
select new {
f,
f.FieldType,
methodsUserOutsideMyAssembly,
Debt = (15 + 10*methodsUserOutsideMyAssembly.Count()).ToMinutes().ToDebt(),
Severity = f.IsInitOnly ? Severity.Medium : Severity.High
}
//<Expl>
// The *{0}* shouldn't be public because it exposes a pointer of type *{1}*.
// Publicly exposing a pointer can facilitate malicious access
// to unmanaged data.
//</Expl>
//<Desc>
// Detects public or protected fields of type *System.IntPtr* or
// *System.UIntPtr*. Pointers access unmanaged memory; exposing them publicly
// lets malicious code read and write the application's unmanaged data —
// worse if the field is not read-only, since it can be changed to force the
// application onto arbitrary data.
//
// **Flagged Code**
// ```csharp
// public class NativeBuffer {
// public IntPtr Handle; // flagged: public, writable pointer
// protected UIntPtr Length; // flagged: protected pointer
// }
// ```
//
// **Fixed Code**
// ```csharp
// public class NativeBuffer {
// private readonly SafeHandle _handle; // pointer wrapped in a SafeHandle
// private IntPtr _buffer; // private pointer
// public int Length { get; }
// }
// ```
//</Desc>
//<HowToFix>
// Make pointers private or internal.
//
// Estimated Debt: 15 minutes plus 10 minutes per method using the field
// outside its assembly. Severity: *Medium*, *High* if non read-only.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Seal methods that satisfy non-public interfaces - Security
Matches public virtual methods of non-sealed public classes that override a method of an internal interface. The non-public interface indicates a process meant to remain private: a malicious subclass can override the method behavior, which private implementation will then invoke — a security vulnerability.
Flagged Codeinternal interface ILicenseChecker { bool IsLicensed(); }
public class Product : ILicenseChecker {
public virtual bool IsLicensed() => _license.IsValid; // flagged: a subclass can override it
}
Fixed Codeinternal interface ILicenseChecker { bool IsLicensed(); }
public sealed class Product : ILicenseChecker {
public bool IsLicensed() => _license.IsValid; // non-virtual method, sealed class
}
How to Fix:Either seal the parent class, make it non-public, implement the method without
virtual, or make the method non-public. Suppress the issue if no security threat exists.
// <Name>Seal methods that satisfy non-public interfaces</Name>
// <Id>ND3106:SealMethodsThatSatisfyNonPublicInterfaces</Id>
warnif count > 0
from m in Application.Methods
where
!m.IsFinal && // If the method is not marked as virtual the flag final is set
m.IsPubliclyVisible &&
!m.ParentType.IsSealed &&
m.ParentType.IsClass
let overridenInterface =
m.OverriddensBase.FirstOrDefault(mb => mb.ParentType.IsInterface && !mb.ParentType.IsPubliclyVisible)
where overridenInterface != null
select new {
m,
overridenInterface = overridenInterface.ParentType,
Debt = 30.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *public {0}* overrides a method defined
// in the non-public *{1}*.
// This situation might be a security vulnerability because
// it allows the creation of a malicious subclass that can
// override the method behavior.
//</Expl>
//<Desc>
// Matches public virtual methods of non-sealed public classes that override
// a method of an internal interface. The non-public interface indicates a
// process meant to remain private: a malicious subclass can override the
// method behavior, which private implementation will then invoke — a
// security vulnerability.
//
// **Flagged Code**
// ```csharp
// internal interface ILicenseChecker { bool IsLicensed(); }
//
// public class Product : ILicenseChecker {
// public virtual bool IsLicensed() => _license.IsValid; // flagged: a subclass can override it
// }
// ```
//
// **Fixed Code**
// ```csharp
// internal interface ILicenseChecker { bool IsLicensed(); }
//
// public sealed class Product : ILicenseChecker {
// public bool IsLicensed() => _license.IsValid; // non-virtual method, sealed class
// }
// ```
//</Desc>
//<HowToFix>
// Either seal the parent class, make it non-public, implement the method
// without *virtual*, or make the method non-public. Suppress the issue if
// no security threat exists.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Restrict sensitive APIs to reviewed components - Security
Matches application methods that call a sensitive API directly, while not being declared in one of the reviewed components named in the rule source: process execution (
Process.Start()), dynamic loading (
Assembly.Load(),
Activator.CreateInstance(string),
Type.GetType(string)), raw SQL (
DbCommand.CommandText, EF Core
Raw entry points, Dapper) and cryptographic primitives (
Aes,
RSA,
HMAC,
RandomNumberGenerator,
Rfc2898DeriveBytes...).
This rule expresses an
architectural policy rather than a defect: the few places allowed to touch such APIs are known, reviewed and kept small, so that argument escaping, path validation, parameterization and key management are implemented once. Every other call site is a bypass to review. The rule ships
inactive: edit the
reviewedTypes and
reviewedNamespaces lists at the top of the query with the names of your wrappers, trim the API families you don't care about, then activate it. Until the lists are filled, every direct call would be reported.
Flagged Codepublic class ReportExporter {
public void OpenInViewer(string reportPath) {
Process.Start("viewer.exe", reportPath); // flagged: direct call outside wrappers
}
}
Fixed Codepublic class ReportExporter {
public void OpenInViewer(string reportPath) {
_processRunner.Run("viewer.exe", reportPath); // reviewed wrapper listed in the rule
}
}
namespace MyCompany.Infrastructure.Security {
public sealed class ProcessRunner { // validates and escapes arguments once
public void Run(string exe, params string[] args) { /* ArgumentList, no shell */ }
}
}
How to Fix:Route the call through the reviewed wrapper that centralizes the safety checks for that API family. When the calling type is itself a legitimate wrapper, add its full name to the
reviewedTypes list (or its namespace to
reviewedNamespaces) in the rule source.
When an API family is not a concern for the application, remove it from the
sensitiveMembers definition rather than suppressing issues one by one.
// <Name>Restrict sensitive APIs to reviewed components</Name>
// <Id>ND3138:RestrictSensitiveApisToReviewedComponents</Id>
warnif count > 0
//------------------------------------------------------------------------------------
// CONFIGURATION: name here the reviewed components (types or namespaces) that are
// allowed to call the sensitive APIs directly. Every other application method calling
// one of these APIs is reported. Activate the rule once this list is filled.
//------------------------------------------------------------------------------------
let reviewedTypes = Application.Types.WithFullNameIn(
"MyCompany.Infrastructure.Security.ProcessRunner",
"MyCompany.Infrastructure.Security.PluginLoader",
"MyCompany.Infrastructure.Security.CryptoService").ToHashSetEx()
let reviewedNamespaces = Application.Namespaces.WithNameIn(
"MyCompany.Infrastructure.Data").ToHashSetEx()
//------------------------------------------------------------------------------------
// SENSITIVE APIS: each family can be trimmed or extended.
// Third-party members are part of the code model only when the application uses them.
//------------------------------------------------------------------------------------
let sensitiveMembers = ThirdParty.Methods.Where(m =>
// Process execution
(m.ParentType.FullName == "System.Diagnostics.Process" && m.SimpleName == "Start") ||
(m.ParentType.FullName == "System.Diagnostics.ProcessStartInfo" && m.IsConstructor) ||
// Dynamic code loading and late-bound instantiation
(m.ParentType.FullName == "System.Reflection.Assembly" && m.SimpleName.StartsWithAny("Load", "ReflectionOnlyLoad", "UnsafeLoadFrom")) ||
(m.ParentType.FullName == "System.Runtime.Loader.AssemblyLoadContext" && m.SimpleName.StartsWith("LoadFrom")) ||
(m.ParentType.FullName == "System.Activator" && m.SimpleName.StartsWith("CreateInstance") && m.Name.Contains("String")) ||
(m.ParentType.FullName == "System.Type" && m.SimpleName == "GetType" && m.Name.Contains("String")) ||
// Raw SQL
(m.ParentType.FullName.EndsWithAny("DbCommand", "SqlCommand", "OracleCommand", "NpgsqlCommand", "MySqlCommand", "SqliteCommand") &&
(m.SimpleName == "set_CommandText" || (m.IsConstructor && m.Name.Contains("String")))) ||
m.SimpleNameEqualsAny("FromSqlRaw", "ExecuteSqlRaw", "ExecuteSqlRawAsync", "SqlQueryRaw", "ExecuteSqlCommand", "ExecuteSqlCommandAsync") ||
(m.ParentType.FullName.StartsWith("Dapper.") && m.SimpleName.StartsWithAny("Query", "Execute")) ||
// Cryptographic primitives
(m.ParentType.FullName.StartsWith("System.Security.Cryptography.") &&
m.ParentType.SimpleNameEqualsAny("Aes", "AesGcm", "AesCcm", "ChaCha20Poly1305", "TripleDES", "DES", "RC2", "Rijndael",
"RSA", "ECDsa", "ECDiffieHellman", "DSA", "HMAC", "HashAlgorithm", "SymmetricAlgorithm",
"AsymmetricAlgorithm", "RandomNumberGenerator", "ProtectedData", "Rfc2898DeriveBytes") &&
(m.IsConstructor || (m.IsStatic && m.SimpleName.StartsWithAny("Create", "Protect", "Unprotect", "Pbkdf2")))))
.ToHashSetEx()
where sensitiveMembers.Any()
from m in Application.Methods.UsingAny(sensitiveMembers)
// The method's type (or the reviewed type it is nested in: lambda closures, nested helpers)
// or its namespace is a reviewed component.
let reviewedTypesAndNested = reviewedTypes.Concat(reviewedTypes.SelectMany(t => t.NestedTypes)).ToHashSetEx()
where !reviewedTypesAndNested.Contains(m.ParentType) &&
!reviewedNamespaces.Contains(m.ParentNamespace)
let sensitiveMembersUsed = m.MethodsCalled.Intersect(sensitiveMembers).ToArray()
select new {
m,
sensitiveMembersUsed,
Debt = (10 + 5 * sensitiveMembersUsed.Length).ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The method *{0}* calls the sensitive API *{1}* directly, outside of the
// reviewed components listed in the rule. Route the call through the
// reviewed wrapper, or add the declaring type to the list once it has
// been reviewed.
//</Expl>
// <Desc>
// Matches application methods that call a sensitive API directly, while
// not being declared in one of the reviewed components named in the
// rule source: process execution (*Process.Start()*), dynamic loading
// (*Assembly.Load()*, *Activator.CreateInstance(string)*,
// *Type.GetType(string)*), raw SQL (*DbCommand.CommandText*, EF Core
// *Raw* entry points, Dapper) and cryptographic primitives (*Aes*, *RSA*,
// *HMAC*, *RandomNumberGenerator*, *Rfc2898DeriveBytes*...).
//
// This rule expresses an **architectural policy** rather than a defect:
// the few places allowed to touch such APIs are known, reviewed and kept
// small, so that argument escaping, path validation, parameterization
// and key management are implemented once. Every other call site is a
// bypass to review. The rule ships **inactive**: edit the *reviewedTypes*
// and *reviewedNamespaces* lists at the top of the query with the names
// of your wrappers, trim the API families you don't care about, then
// activate it. Until the lists are filled, every direct call would be
// reported.
//
// **Flagged Code**
// ```csharp
// public class ReportExporter {
// public void OpenInViewer(string reportPath) {
// Process.Start("viewer.exe", reportPath); // flagged: direct call outside wrappers
// }
// }
// ```
//
// **Fixed Code**
// ```csharp
// public class ReportExporter {
// public void OpenInViewer(string reportPath) {
// _processRunner.Run("viewer.exe", reportPath); // reviewed wrapper listed in the rule
// }
// }
// namespace MyCompany.Infrastructure.Security {
// public sealed class ProcessRunner { // validates and escapes arguments once
// public void Run(string exe, params string[] args) { /* ArgumentList, no shell */ }
// }
// }
// ```
// </Desc>
// <HowToFix>
// Route the call through the reviewed wrapper that centralizes the
// safety checks for that API family. When the calling type is itself a
// legitimate wrapper, add its full name to the *reviewedTypes* list (or
// its namespace to *reviewedNamespaces*) in the rule source.
//
// When an API family is not a concern for the application, remove it
// from the *sensitiveMembers* definition rather than suppressing issues
// one by one.
// </HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Methods that could have a lower visibility - Visibility
Warns about methods declarable with a lower visibility (private < internal < public) without compilation error. Narrowing visibility promotes encapsulation by minimizing access scope.
Publicly visible methods are not matched (the rule cannot know if public was intentional); matched public methods are declared in non-public types. Public methods that could be internal in a non-public type are caught by Avoid public methods not publicly visible instead.
Methods tagged CannotDecreaseVisibilityAttribute or IsNotDeadCodeAttribute (from NDepend.API.dll, or declared in your own code) are not matched.
How to Fix:
Declare each matched method with the optimal visibility shown in the CouldBeDeclared column.
If you publish an API, many matched public methods should stay public: either add && !m.IsPubliclyVisible to the rule (coarse), or tag each method with CannotDecreaseVisibilityAttribute (finer).
// <Name>Methods that could have a lower visibility</Name>
// <Id>ND1800:MethodsThatCouldHaveALowerVisibility</Id>
warnif count > 0 from m in JustMyCode.Methods where
m.Visibility != m.OptimalVisibility &&
// Don't match potentially dead methods, candidate for removal
m.MethodsCallingMe.Any() &&
// Eliminate public methods visible outside of their assembly
// because the rule cannot determine if the developer left the method public
// intentionally or not.
!m.IsPubliclyVisible &&
// Avoid matching public methods declared in a non-public type,
// that could have the visibility internal, because
// such situation is caught by the rule 'Avoid public methods not publicly visible'.
!(m.Visibility == Visibility.Public &&
m.ParentType.Visibility != Visibility.Public &&
m.OptimalVisibility == Visibility.Internal) &&
!m.HasAttribute("NDepend.Attributes.CannotDecreaseVisibilityAttribute".AllowNoMatch()) &&
!m.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) &&
!m.HasAttribute("JetBrains.Annotations.UsedImplicitlyAttribute".AllowNoMatch()) &&
// If you don't want to link NDepend.API.dll, you can use your own attributes
// and adapt the source code of this rule.
// methods of serializable type must remain public.
!m.ParentType.TypesUsed.Any(t1 => t1.IsAttributeClass &&
(t1.ParentNamespace.Name == "Newtonsoft.Json" || t1.ParentNamespace.Name == "System.Text.Json")
) &&
!m.ParentType.HasAttribute("System.Runtime.Serialization.DataContractAttribute".AllowNoMatch()) &&
!m.ParentType.HasAttribute("System.Xml.Serialization.XmlRootAttribute".AllowNoMatch()) &&
!m.ParentType.HasAttribute("System.SerializableAttribute".AllowNoMatch()) &&
// ASP.NET MVC Controller classes and methods are used only by the infrastructure and must be public
!m.ParentType.SimpleName.EndsWith("Controller") &&
!m.ParentType.DeriveFrom("Microsoft.AspNetCore.Mvc.Controller".AllowNoMatch()) &&
!m.ParentType.DeriveFrom("Microsoft.AspNetCore.Mvc.ControllerBase".AllowNoMatch()) &&
!m.ParentType.HasAttribute("Microsoft.AspNetCore.Mvc.ControllerAttribute".AllowNoMatch()) &&
// Such class is instantiated dynamically
!(m.IsConstructor && m.ParentType.DeriveFrom("System.ComponentModel.EnumConverter".AllowNoMatch())) &&
// Don't advise to reduce visibility of property getters/setters
// of classes that implement INotifyPropertyChanged
!((m.IsPropertyGetter || m.IsPropertySetter) &&
m.ParentType.Implement("System.ComponentModel.INotifyPropertyChanged".AllowNoMatch())) &&
// Don't decrease the visibility of Main() methods.
!m.IsEntryPoint
select new {
m,
m.Visibility ,
CouldBeDeclared = m.OptimalVisibility,
m.MethodsCallingMe,
Debt = 30.ToSeconds().ToDebt(), // It is fast to change the method visibility
Severity = Severity.Medium
}
//<Expl>
// In the context of analyzed code, the *{0}* can be declared as *{2}*.
// Doing so **improves encapsulation** by constraining this method's accessibility
// to the smallest possible scope.
//</Expl>
//<Desc>
// Warns about methods declarable with a lower visibility (*private* <
// *internal* < *public*) without compilation error. **Narrowing visibility**
// **promotes encapsulation** by minimizing access scope.
//
// Publicly visible methods are not matched (the rule cannot know if public
// was intentional); matched public methods are declared in non-public types.
// *Public* methods that could be *internal* in a non-public type are caught
// by *Avoid public methods not publicly visible* instead.
//
// Methods tagged *CannotDecreaseVisibilityAttribute* or
// *IsNotDeadCodeAttribute* (from *NDepend.API.dll*, or declared in your own
// code) are not matched.
//</Desc>
//<HowToFix>
// Declare each matched method with the *optimal visibility* shown in the
// *CouldBeDeclared* column.
//
// If you publish an API, many matched public methods should stay public:
// either add *&& !m.IsPubliclyVisible* to the rule (coarse), or tag each
// method with *CannotDecreaseVisibilityAttribute* (finer).
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Types that could have a lower visibility - Visibility
Warns about types declarable with a lower visibility (private < internal < public) without compilation error. Narrowing visibility promotes encapsulation by minimizing consumption scope.
Publicly visible types are not matched (the rule cannot know if public was intentional); matched public types are nested in non-public types.
Types tagged CannotDecreaseVisibilityAttribute or IsNotDeadCodeAttribute (from NDepend.API.dll, or declared in your own code) are not matched.
How to Fix:
Declare each matched type with the optimal visibility shown in the CouldBeDeclared column.
If you publish an API, many matched public types should stay public: either add && !m.IsPubliclyVisible to the rule (coarse), or tag each type with CannotDecreaseVisibilityAttribute (finer).
// <Name>Types that could have a lower visibility</Name>
// <Id>ND1801:TypesThatCouldHaveALowerVisibility</Id>
warnif count > 0 from t in JustMyCode.Types where
t.Visibility != t.OptimalVisibility &&
// Don't match potentially dead types, candidate for removal
t.TypesUsingMe.Any() &&
// If you don't want to link NDepend.API.dll, you can use your own attributes
// and adapt the source code of this rule.
!t.HasAttribute("NDepend.Attributes.CannotDecreaseVisibilityAttribute".AllowNoMatch()) &&
!t.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) &&
!t.HasAttribute("JetBrains.Annotations.UsedImplicitlyAttribute".AllowNoMatch()) &&
// JSON, XML and binary serialized type must remain public.
!t.IsUsing("Newtonsoft.Json".MatchNamespace().AllowNoMatch()) &&
!t.IsUsing("System.Text.Json".MatchNamespace().AllowNoMatch()) &&
!t.HasAttribute("System.Xml.Serialization.XmlRootAttribute".AllowNoMatch()) &&
!t.HasAttribute("System.SerializableAttribute".AllowNoMatch()) &&
// Eliminate public types visible outside of their assembly
// because the rule cannot know if the developer left the type public
// intentionally or not.
!t.IsPubliclyVisible &&
// Static types that define only const fields cannot be seen as used in IL code.
// They don't have to be tagged with CannotDecreaseVisibilityAttribute.
!( t.IsStatic &&
!t.Methods.Any(m => !m.IsClassConstructor) &&
!t.Fields.Any(f => !f.IsLiteral && !(f.IsStatic && f.IsInitOnly))) &&
// A type used by an interface that has the same visibility
// cannot have its visibility decreased, else a compilation error occurs!
!t.TypesUsingMe.Any(tUser =>
tUser.IsInterface &&
tUser.Visibility == t.Visibility) &&
// Don't change the visibility of a type that contain an entry point method.
!t.Methods.Any(m =>m.IsEntryPoint)
select new {
t,
t.Visibility ,
CouldBeDeclared = t.OptimalVisibility,
t.TypesUsingMe,
Debt = 30.ToSeconds().ToDebt(), // It is fast to change the method visibility
Severity = Severity.Medium
}
//<Expl>
// In the context of analyzed code, the *{0}* can be declared as *{2}*.
// Doing so **improves encapsulation** by constraining this type's accessibility
// to the smallest possible scope.
//</Expl>
//<Desc>
// Warns about types declarable with a lower visibility (*private* <
// *internal* < *public*) without compilation error. **Narrowing visibility**
// **promotes encapsulation** by minimizing consumption scope.
//
// Publicly visible types are not matched (the rule cannot know if public
// was intentional); matched public types are nested in non-public types.
//
// Types tagged *CannotDecreaseVisibilityAttribute* or
// *IsNotDeadCodeAttribute* (from *NDepend.API.dll*, or declared in your own
// code) are not matched.
//</Desc>
//<HowToFix>
// Declare each matched type with the *optimal visibility* shown in the
// *CouldBeDeclared* column.
//
// If you publish an API, many matched public types should stay public:
// either add *&& !m.IsPubliclyVisible* to the rule (coarse), or tag each
// type with *CannotDecreaseVisibilityAttribute* (finer).
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Fields that could have a lower visibility - Visibility
Warns about fields declarable with a lower visibility (private < internal < public) without compilation error. Narrowing visibility promotes encapsulation by minimizing consumption scope.
Publicly visible fields are not matched (the rule cannot know if public was intentional); matched public fields are declared in non-public types.
Fields tagged CannotDecreaseVisibilityAttribute or IsNotDeadCodeAttribute (from NDepend.API.dll, or declared in your own code) are not matched.
How to Fix:
Declare each matched field with the optimal visibility shown in the CouldBeDeclared column.
If you publish an API, some matched public fields should stay public: either add && !m.IsPubliclyVisible to the rule (coarse), or tag each field with CannotDecreaseVisibilityAttribute (finer).
// <Name>Fields that could have a lower visibility</Name>
// <Id>ND1802:FieldsThatCouldHaveALowerVisibility</Id>
warnif count > 0 from f in JustMyCode.Fields where
f.Visibility != f.OptimalVisibility &&
// Eliminate public fields visible outside of their assembly
// because the rule cannot know if the developer left the field public
// intentionally or not.
!f.IsPubliclyVisible &&
// Don't match potentially dead fields, candidate for removal
f.MethodsUsingMe.Any() &&
!f.HasAttribute("NDepend.Attributes.CannotDecreaseVisibilityAttribute".AllowNoMatch()) &&
!f.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) &&
!f.HasAttribute("JetBrains.Annotations.UsedImplicitlyAttribute".AllowNoMatch()) &&
// If you don't want to link NDepend.API.dll, you can use your own attributes
// and adapt the source code of this rule.
// JSON and XML serialized fields must remain public.
!f.ParentType.TypesUsed.Any(t1 => t1.IsAttributeClass &&
(t1.ParentNamespace.Name == "Newtonsoft.Json" || t1.ParentNamespace.Name == "System.Text.Json")
) &&
!f.HasAttribute("System.Xml.Serialization.XmlElementAttribute".AllowNoMatch()) &&
!f.HasAttribute("System.Xml.Serialization.XmlAttributeAttribute".AllowNoMatch()) &&
!f.HasAttribute("System.Xml.Serialization.XmlArrayAttribute".AllowNoMatch()) &&
!f.HasAttribute("System.Xml.Serialization.XmlArrayItemAttribute".AllowNoMatch()) &&
// Don't check for serialized fields visibility
!f.HasAttribute("System.Runtime.Serialization.DataMemberAttribute".AllowNoMatch()) &&
// Fields of classes that derive from Unity classes might be used implicitly by the Unity engine
!f.ParentType.BaseClasses.Any(bc => bc.ParentNamespace.Name.StartsWithAny("UnityEngine","UnityEditor"))
select new {
f,
f.Visibility ,
CouldBeDeclared = f.OptimalVisibility,
f.MethodsUsingMe,
Debt = 30.ToSeconds().ToDebt(), // It is fast to change the field visibility
Severity = Severity.Medium
}
//<Expl>
// In the context of analyzed code, the *{0}* can be declared as *{2}*.
// Doing so **improves encapsulation** by constraining this field's accessibility
// to the smallest possible scope.
//</Expl>
//<Desc>
// Warns about fields declarable with a lower visibility (*private* <
// *internal* < *public*) without compilation error. **Narrowing visibility**
// **promotes encapsulation** by minimizing consumption scope.
//
// Publicly visible fields are not matched (the rule cannot know if public
// was intentional); matched public fields are declared in non-public types.
//
// Fields tagged *CannotDecreaseVisibilityAttribute* or
// *IsNotDeadCodeAttribute* (from *NDepend.API.dll*, or declared in your own
// code) are not matched.
//</Desc>
//<HowToFix>
// Declare each matched field with the *optimal visibility* shown in the
// *CouldBeDeclared* column.
//
// If you publish an API, some matched public fields should stay public:
// either add *&& !m.IsPubliclyVisible* to the rule (coarse), or tag each
// field with *CannotDecreaseVisibilityAttribute* (finer).
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Types that could be declared as private, nested in a parent type - Visibility
Matches types that could be nested and declared private in another type: the parent type is the only consumer, and both are in the same namespace. This improves encapsulation by minimizing the type's consumption scope. Classes with extension methods cannot be nested and are not matched.
How to Fix:
Nest each matched type into the specified parent type as private. Nested private types are hardly testable though: skip types consumed directly by tests.
// <Name>Types that could be declared as private, nested in a parent type</Name>
// <Id>ND1803:TypesThatCouldBeDeclaredAsPrivateNestedInAParentType</Id>
warnif count > 0
from t in JustMyCode.Types
where !t.IsGeneratedByCompiler &&
!t.IsNested &&
!t.IsPubliclyVisible &&
!t.IsEnumeration &&
// Don't warn on types declared with 'file' visibility
!t.IsVisibleOnlyInFile &&
// Only one type user… TypesUsingMe spans the test code too: a type consumed by
// a test type as well as by its parent has two users and is NOT proposed for nesting
t.TypesUsingMe.Count() == 1 &&
// Require that t doesn't contain any extension method.
// Types with extension methods cannot be nested.
!t.Methods.Any(m => m.IsExtensionMethod)
// The single user is a test type only when nothing else consumes t and that test lives in
// the same namespace (rare): the advice then names a test class as the parent — accepted.
let couldBeNestedIn = t.TypesUsingMe.Single()
where !couldBeNestedIn.IsGeneratedByCompiler &&
!couldBeNestedIn.IsInterface && // Cannot nest a type in an interface
// …declared in the same namespace
couldBeNestedIn.ParentNamespace == t.ParentNamespace &&
// Don't advise to move a base class
// or an interface into one of its child type.
!couldBeNestedIn.DeriveFrom(t) &&
!couldBeNestedIn.Implement(t)
select new {
t,
couldBeNestedIn,
Debt = 3.ToMinutes().ToDebt(), // It is fast to nest a type into another one
Severity = Severity.Low // This rule proposes advices, not potential problems
}
//<Expl>
// The *{0}* is only used by the *{1}* and seems to be an implementation detail of it.
// Consider declaring this type as *private* and *nested* into its parent type.
// Doing so **improves encapsulation** by constraining this type's accessibility
// to the smallest possible scope.
//</Expl>
//<Desc>
// Matches types that could be *nested* and declared *private* in another
// type: the *parent type* is the only consumer, and both are in the same
// namespace. This **improves encapsulation** by minimizing the type's
// consumption scope. Classes with extension methods cannot be nested and
// are not matched.
//</Desc>
//<HowToFix>
// Nest each matched *type* into the specified *parent type* as private.
// *Nested private types* are hardly testable though: skip types consumed
// directly by tests.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid publicly visible constant fields - Visibility
Warns about constant fields visible outside their assembly: their value gets hard-coded into client assemblies, so changing it requires recompiling all consumers, else the program runs with different in-memory values — bugs are lurking.
Enumeration values share this pitfall but cannot be static readonly, hence the && !f.IsEnumValue condition; keep it unless you banish public enumerations.
How to Fix:
Declare matched fields static readonly instead of constant: the value becomes safely changeable without recompiling clients.
// <Name>Avoid publicly visible constant fields</Name>
// <Id>ND1804:AvoidPubliclyVisibleConstantFields</Id>
warnif count > 0
from f in JustMyCode.Fields
where f.IsLiteral &&
f.IsPubliclyVisible &&
!f.IsEnumValue
select new {
f,
Debt = 30.ToSeconds().ToDebt(), // It is fast to update field declaration
Severity = Severity.Medium
}
//<Expl>
// The constant *{0}* is publicly exposed to other assemblies.
// This situation can lead to binary compatibility problems because
// when the constant value gets changed, clients of the library
// may need to be recompiled to use the updated value.
//</Expl>
//<Desc>
// Warns about constant fields visible outside their assembly: their value
// gets *hard-coded* into client assemblies, so changing it *requires*
// recompiling all consumers, else the program runs with different in-memory
// values — bugs are lurking.
//
// Enumeration values share this pitfall but cannot be *static readonly*,
// hence the **&& !f.IsEnumValue** condition; keep it unless you banish
// public enumerations.
//</Desc>
//<HowToFix>
// Declare matched fields **static readonly** instead of **constant**: the
// value becomes *safely changeable* without recompiling clients.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Fields should be declared as private or protected - Visibility
Matches mutable (non-constant, non-readonly) fields non-private, non-protected. Fields are implementation details and should be private (or protected if derived classes need them).
When a non-private field misbehaves, the culprit can be anywhere; a private field can only be assigned from its own class, usually one source file to look at.
Debt is proportional to the number of methods assigning the field.
How to Fix:
Declare the field private or readonly, refactoring outside usage away if possible. If external access is required, encapsulate it in a property: debug breakpoints on accessors simplify tracking read-write accesses.
// <Name>Fields should be declared as private or protected</Name>
// <Id>ND1805:FieldsShouldBeDeclaredAsPrivate</Id>
warnif count > 0
let isUnityProject = ThirdParty.Assemblies.Any(a => a.Name.StartsWith("UnityEngine"))
from f in JustMyCode.Fields where
// In Unity projects often there is no properties for high-performance reasons
// and public fields are used instead.
!isUnityProject &&
// Valid encapsulation level
!f.IsPrivate && !f.IsProtected && !f.IsProtectedAndInternal &&
// These conditions filter cases where fields
// doesn't represent state that should be encapsulated.
!f.IsGeneratedByCompiler &&
!f.IsSpecialName &&
!f.IsInitOnly &&
!f.IsLiteral &&
!f.IsEnumValue &&
!f.HasAttribute("System.Xml.Serialization.XmlAttributeAttribute".AllowNoMatch()) &&
!f.HasAttribute("System.Runtime.Serialization.DataMemberAttribute".AllowNoMatch()) &&
!f.HasAttribute("System.Runtime.CompilerServices.FixedBufferAttribute".AllowNoMatch())
// A non-private field used from outside its class,
// usually leads to complicated field state management.
let outsideMethodsUsingMe =
f.MethodsUsingMe.Where(m => m.ParentType != f.ParentType)
// Don't report fields assigned from outside its parent type
// since this situation is handled by the rule
// 'A field must not be assigned from outside its parent hierarchy types'
where !f.MethodsAssigningMe.Intersect(outsideMethodsUsingMe).Any()
select new {
f,
f.Visibility,
outsideMethodsUsingMe,
Debt = (60+20*outsideMethodsUsingMe .Count()).ToSeconds().ToDebt(),
// The cost to leave such issue unfixed is higher if the field is publicly visible!
AnnualInterest = Severity.Medium.AnnualInterestThreshold() * (f.IsPubliclyVisible ? 3 : 1)
}
//<Expl>
// The *{0}* is used outside its parent type by the *{2}*.
// A field should be an encapsulated *implementation detail*.
// Consider making it *private* or refactor it as a property
// whose read-write accesses can be better controlled at debug-time.
//</Expl>
//<Desc>
// Matches **mutable (non-constant, non-readonly) fields non-private,
// non-protected**. Fields are **implementation details** and should be
// private (or protected if derived classes need them).
//
// When a *non-private field* misbehaves, the culprit can be anywhere; a
// private field can only be assigned from its own class, usually one source
// file to look at.
//
// Debt is proportional to the number of methods assigning the field.
//</Desc>
//<HowToFix>
// Declare the field *private* or *readonly*, refactoring outside usage away
// if possible. If external access is required, encapsulate it in a property:
// debug breakpoints on accessors simplify tracking read-write accesses.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Constructors of abstract classes should be declared as protected or private - Visibility
Constructors of abstract classes can only be called from derived classes, and instances of an abstract class cannot be created: a public constructor there is wrong design. A private constructor means derived classes must be nested in the abstract class.
How to Fix:
Declare the constructor protected, or don't declare the type abstract.
//<Name>Constructors of abstract classes should be declared as protected or private</Name>
// <Id>ND1806:ConstructorsOfAbstractClassesShouldBeDeclaredAsProtectedOrPrivate</Id>
warnif count > 0
from t in Application.Types where
t.IsClass &&
t.IsAbstract
let ctors = t.Constructors.Where(c => !c.IsProtected &&
!c.IsPrivate &&
!c.IsProtectedAndInternal)
where ctors.Count() > 0
from ctor in ctors
select new {
ctor,
baseClass = t,
Debt = 30.ToSeconds().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// This constructor should be declared as *protected*
// since its parent *{1}* is abstract and can only be
// instantiated from its derived classes.
//</Expl>
//<Desc>
// Constructors of abstract classes can only be called from derived classes,
// and instances of an *abstract* class cannot be created: a public
// constructor there is wrong design. A private constructor means derived
// classes must be nested in the abstract class.
//</Desc>
//<HowToFix>
// Declare the constructor *protected*, or don't declare the type *abstract*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid public methods not publicly visible - Visibility
Warns about
public methods whose parent type is not
public. There
public means
accessible wherever my parent type is visible — elegant to some developers, misleading to others. Deactivate the rule if you disagree; the debate:
http://ericlippert.com/2014/09/15/internal-or-public/
Severity is
Low: more an advice than a problem.
How to Fix:Declare the method
internal if used outside its type, else
private.
//<Name>Avoid public methods not publicly visible</Name>
// <Id>ND1807:AvoidPublicMethodsNotPubliclyVisible</Id>
warnif count > 0
from m in JustMyCode.Methods where
!m.IsPubliclyVisible && m.IsPublic &&
// Filter out virtual methods
!m.IsVirtual &&
// Filter out interface and delegate types
!m.ParentType.IsInterface &&
!m.ParentType.IsDelegate &&
// Don't match potentially dead methods, candidate for removal
m.MethodsCallingMe.Any() &&
// Eliminate default constructor from the result.
// Whatever the visibility of the declaring class,
// default constructors are public and introduce noise
// in the current rule.
!( m.IsConstructor && m.IsPublic && m.NbParameters == 0) &&
// Filter out operators that must be declared public
!m.IsOperator &&
// Filter out methods generated by compiler, except auto-property getter/setter
(!m.IsGeneratedByCompiler || m.IsPropertyGetter || m.IsPropertySetter) &&
// Don't advise to reduce visibility of property getters/setters
// of classes that implement INotifyPropertyChanged
!((m.IsPropertyGetter || m.IsPropertySetter) &&
m.ParentType.Implement("System.ComponentModel.INotifyPropertyChanged".AllowNoMatch()))
let calledOutsideParentType =
m.MethodsCallingMe.FirstOrDefault(mCaller => mCaller.ParentType != m.ParentType) != null
// If parent type declared as public, it must be a nested type not publicly visible
let parentTypeVisibility = m.ParentType.Visibility != Visibility.Public ?
"declared as *" + m.ParentType.Visibility.ToString().ToLower() +"*" :
"not publicly visible"
select new {
m,
parentTypeVisibility,
declareMethodAs = (Visibility) (calledOutsideParentType ? Visibility.Internal : Visibility.Private),
methodsCaller = m.MethodsCallingMe,
Debt = 30.ToSeconds().ToDebt(),
Severity = Severity.Low
}
//<Expl>
// The *{0}* is declared as *public* but remains inaccessible beyond its assembly due to its parent type being {1}.
// Consider declaring this method as *{2}*, which makes its actual accessibility scope explicit.
//</Expl>
//<Desc>
// Warns about *public* methods whose parent type is not *public*. There
// *public* means *accessible wherever my parent type is visible* — elegant
// to some developers, misleading to others. Deactivate the rule if you
// disagree; the debate: http://ericlippert.com/2014/09/15/internal-or-public/
//
// Severity is **Low**: more an advice than a problem.
//</Desc>
//<HowToFix>
// Declare the method *internal* if used outside its type, else *private*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Event handler methods should be declared as private or protected - Visibility
An event handler (like OnClickButton()) should be private and only called on event firing — or protected, as some designers (e.g. ASP.NET) generate it so sub-classes can call it.
How to Fix:
If another class needs to call the handler, find a structure matching the actual concept: the other class doesn't want to click a button, it wants to do what clicking the button also does.
// <Name>Event handler methods should be declared as private or protected</Name>
// <Id>ND1808:EventHandlerMethodsShouldBeDeclaredAsPrivateOrProtected</Id>
warnif count > 0
from m in Application.Methods where
!(m.IsPrivate || m.IsProtected) &&
!m.IsGeneratedByCompiler &&
// A method is considered as an event handler if…
m.NbParameters == 2 && // … it has two parameters …
m.Name.Contains("Object") && // … of types Object …
m.Name.Contains("EventArgs") && // … and EventArgs
// Discard special cases
!m.ParentType.IsDelegate &&
!m.IsGeneratedByCompiler
select new {
m,
m.Visibility,
Debt = 2.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* is an event handler. It should be declared as *private*
// or *protected* as it is intended to be used exclusively within its
// own class or class-hierarchy for event handling and should not be
// invoked in other context.
//</Expl>
//<Desc>
// An event handler (like *OnClickButton()*) should be *private* and only
// called on event firing — or *protected*, as some designers (e.g. ASP.NET)
// generate it so sub-classes can call it.
//</Desc>
//<HowToFix>
// If another class needs to call the handler, find a structure matching the
// actual concept: the other class doesn't want to click a button, it wants
// to do what clicking the button also does.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Wrong usage of CannotDecreaseVisibilityAttribute - Visibility
NDepend.Attributes.CannotDecreaseVisibilityAttribute (from NDepend.API.dll, or declared in your own code) marks elements whose visibility is not optimal but shouldn't be modified — e.g. elements consumed through reflection, mocking, serialization, designers, COM, or reached by test code. It silences the not-optimal-visibility rules and expresses: Don't change the visibility else something will be broken.
This rule matches elements tagged with this attribute whose visibility is nevertheless already optimal.
When an element is used by test code, its optimal visibility is recomputed without taking test code users into account. If this visibility is more restrictive than the actual one, the element is exposed on purpose for tests: the attribute usage is valid and the element is not matched.
How to Fix:
Remove the now-useless CannotDecreaseVisibilityAttribute tagging of matched elements. Severity is Low: more an advice than a problem.
// <Name>Wrong usage of CannotDecreaseVisibilityAttribute</Name>
// <Id>ND1809:WrongUsageOfCannotDecreaseVisibilityAttribute</Id>
warnif count > 0
let tAttr = Types
.WithFullName("NDepend.Attributes.CannotDecreaseVisibilityAttribute")
.FirstOrDefault()
where tAttr != null
// Rank visibilities: the higher the rank, the more restrictive.
let rank = new Func<Visibility, int>(v =>
v == Visibility.Private ? 4 :
v == Visibility.ProtectedAndInternal ? 3 :
v == Visibility.Internal || v == Visibility.Protected ? 2 :
v == Visibility.ProtectedOrInternal ? 1 : 0)
// Estimate the optimal visibility of an element declared in the type pt
// (null for non-nested types) and in the assembly asm, given the types
// of its non-test users.
let optimal = new Func<IType, IAssembly, IType[], Visibility>(
(pt, asm, users) =>
pt != null && users.All(u => u == pt || u.ParentType == pt ||
pt.NestedTypes.Contains(u)) ?
Visibility.Private :
pt != null && users.All(u => u == pt || u.DeriveFrom(pt)) ?
Visibility.Protected :
users.All(u => u.ParentAssembly == asm) ? Visibility.Internal :
Visibility.Public)
// The attribute usage is valid when the element is used by test code
// and its visibility could be decreased if test code users are ignored.
// userTypes are the types of all users, test code users included.
let validForTests = new Func<IMember, IEnumerable<IType>, bool>(
(x, userTypes) =>
userTypes.Any(u => u.IsTestCode) &&
rank(optimal(x.ParentType, x.ParentAssembly,
userTypes.Where(u => !u.IsTestCode).ToArray()))
> rank(x.Visibility))
// Only iterate the elements tagged with the attribute: O(#tags)
let tagged = tAttr.TagsWithMeAsAttribute
.Select(tag => tag.CodeElementTagged)
.Where(x => !x.IsTestCode && !x.IsThirdParty)
.ToArray()
// Get types that do a wrong usage of CannotDecreaseVisibilityAttribute
let types = from t in tagged.Where(x => x.IsType).Select(x => x.AsType)
where
( t.Visibility == t.OptimalVisibility &&
!validForTests(t, t.TypesUsingMe.Where(u => u != t)) ) ||
// Static types that define only const fields cannot be seen as
// used in IL code. They don't need to be tagged with the attribute.
(t.IsStatic && t.NbMethods == 0 && !t.Fields.Any(f => !f.IsLiteral))
select t
// Get methods that do a wrong usage of CannotDecreaseVisibilityAttribute
let methods = from m in tagged.Where(x => x.IsMethod)
.Select(x => x.AsMethod)
where m.Visibility == m.OptimalVisibility &&
!validForTests(m, m.MethodsCallingMe.Select(c => c.ParentType)
.Distinct())
select m
// Get fields that do a wrong usage of CannotDecreaseVisibilityAttribute
let fields = from f in tagged.Where(x => x.IsField).Select(x => x.AsField)
where f.Visibility == f.OptimalVisibility &&
!validForTests(f, f.MethodsUsingMe.Select(u => u.ParentType)
.Distinct())
select f
from member in types.Cast<IMember>().Concat(methods).Concat(fields)
select new {
member,
Debt = 30.ToSeconds().ToDebt(),
Severity = Severity.Low
}
//<Expl>
// The *{0}* shouldn't be tagged with *CannotDecreaseVisibilityAttribute*
// because its visibility is already optimal, even when its test code
// users are not taken into account.
//</Expl>
//<Desc>
// **NDepend.Attributes.CannotDecreaseVisibilityAttribute** (from
// *NDepend.API.dll*, or declared in your own code) marks elements whose
// visibility is not optimal but shouldn't be modified — e.g. elements
// consumed through reflection, mocking, serialization, designers, COM,
// or reached by test code. It silences the not-optimal-visibility rules
// and expresses: **Don't change the visibility else something will be
// broken**.
//
// This rule matches elements tagged with this attribute whose visibility
// is nevertheless already optimal.
//
// When an element is used by test code, its optimal visibility is
// recomputed without taking test code users into account. If this
// visibility is more restrictive than the actual one, the element is
// exposed on purpose for tests: the attribute usage is valid and the
// element is not matched.
//</Desc>
//<HowToFix>
// Remove the now-useless *CannotDecreaseVisibilityAttribute* tagging of
// matched elements. Severity is **Low**: more an advice than a problem.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Exception classes should be declared as public - Visibility
Custom exception classes offer finer details than standard exceptions, but catching code must have access to the exception class to use them.
How to Fix:
Declare custom exception classes public, catchable in every context.
// <Name>Exception classes should be declared as public</Name>
// <Id>ND1810:ExceptionClassesShouldBeDeclaredAsPublic</Id>
warnif count > 0
from t in JustMyCode.Types
where t.IsExceptionClass &&
!t.IsNested &&
!t.IsPublic
select new {
t,
Debt = 2.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The exception {0} should be declared as public.
//</Expl>
//<Desc>
// Custom exception classes offer finer details than standard exceptions,
// but catching code must have access to the exception class to use them.
//</Desc>
//<HowToFix>
// Declare custom exception classes public, catchable in every context.
//</HowToFix>
./rules/assets/img/Info32x32.png
Methods that should be declared as 'public' in C#, 'Public' in VB.NET - Visibility
Lists
internal methods consumed outside their assembly thanks to
System.Runtime.CompilerServices.InternalsVisibleToAttribute, via the property
NDepend.CodeModel.IMember.ShouldBePublic:
https://www.ndepend.com/api/webframe.html#NDepend.API~NDepend.CodeModel.IMember~ShouldBePublic.html
A query (not a rule): it informs that the code relies on the peculiar
InternalsVisibleToAttribute behavior, not that methods should be public.
// <Name>Methods that should be declared as 'public' in C#, 'Public' in VB.NET</Name>
from m in Application.Methods where
m.ShouldBePublic
let usedInAssemblies = m.MethodsCallingMe.ParentAssemblies().Except(m.ParentAssembly)
select new {
m,
m.ParentAssembly,
usedInAssemblies,
m.MethodsCallingMe
}
//<Desc>
// Lists *internal* methods consumed outside their assembly thanks to
// *System.Runtime.CompilerServices.InternalsVisibleToAttribute*, via the
// property *NDepend.CodeModel.IMember.ShouldBePublic*:
// https://www.ndepend.com/api/webframe.html#NDepend.API~NDepend.CodeModel.IMember~ShouldBePublic.html
//
// A query (not a rule): it informs that the code relies on the peculiar
// *InternalsVisibleToAttribute* behavior, not that methods should be public.
//</Desc>
./rules/assets/img/StatusBigOk32x32.png
Fields should be marked as ReadOnly when possible - Immutability
Warns about instance and static fields declarable as
readonly, based on
IField.IsImmutable:
https://www.ndepend.com/api/webframe.html#NDepend.API~NDepend.CodeModel.IField~IsImmutable.html An
IsImmutable field is only assigned by constructors of its class: its value stays constant through the object lifetime (instance field) or the program lifetime (static field).
readonly enhances robustness and readability: it defines a contract that the field is not altered after initialization, and enforces it.
How to Fix:Declare the field with the C#
readonly keyword (
ReadOnly in VB.NET).
// <Name>Fields should be marked as ReadOnly when possible</Name>
// <Id>ND1900:FieldsShouldBeMarkedAsReadOnlyWhenPossible</Id>
warnif count > 0
from f in JustMyCode.Fields where
f.IsImmutable &&
!f.IsInitOnly && // The condition IsInitOnly matches fields that
// are marked with the C# readonly keyword
// (ReadOnly in VB.NET).
!f.IsGeneratedByCompiler &&
!f.IsEventDelegateObject &&
!f.ParentType.IsEnumeration &&
!f.IsLiteral &&
// Don't warn if a method using the field is also calling a method that has 'ref' and 'out' parameters.
// This could lead to false positive. A field used in a 'ref' or 'out' parameter cannot be set as read-only.
f.MethodsUsingMe.SelectMany(m => m.MethodsCalled).FirstOrDefault(m => m.Name.Contains("&")) == null &&
// Don't warn for fields tagged with these Unity specific attribute
!f.HasAttribute("UnityEngine.SerializeField".AllowNoMatch()) &&
!f.HasAttribute("UnityEngine.SerializeReference".AllowNoMatch()) &&
// Don't warn about InlineArrayAttribute's field
!f.ParentType.HasAttribute("System.Runtime.CompilerServices.InlineArrayAttribute".AllowNoMatch())
select new {
f,
f.MethodsReadingMeButNotAssigningMe,
f.MethodsAssigningMe,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* can be declared as **readonly**.
// Doing so explicitly conveys that the field
// should not be altered after initialization
// and enforces this constraint.
//</Expl>
//<Desc>
// Warns about instance and static fields declarable as **readonly**, based
// on *IField.IsImmutable*:
// https://www.ndepend.com/api/webframe.html#NDepend.API~NDepend.CodeModel.IField~IsImmutable.html
// An *IsImmutable* field is only assigned by constructors of its class: its
// value stays constant through the object lifetime (instance field) or the
// program lifetime (static field).
//
// *readonly* enhances robustness and readability: it defines a contract
// that the field is not altered after initialization, and enforces it.
//</Desc>
//<HowToFix>
// Declare the field with the C# *readonly* keyword (*ReadOnly* in VB.NET).
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid non-readonly static fields - Immutability
Warns about static fields not declared read-only.
Instance fields are the natural OOP artifact for mutable state; mutable static fields create
confusion about runtime state and impair testability (the same mutable state is re-used per test). More:
https://blog.ndepend.com/the-proper-usages-of-the-keyword-static-in-c/How to Fix:If assigned once per program lifetime, declare the field
readonly and assign it inline or in the static constructor. Otherwise refactor to hold the state in an instance field.
// <Name>Avoid non-readonly static fields</Name>
// <Id>ND1901:AvoidNonReadOnlyStaticFields</Id>
warnif count > 0
from f in JustMyCode.Fields
where f.IsStatic &&
!f.IsEnumValue &&
!f.IsGeneratedByCompiler &&
!f.IsLiteral &&
!f.IsInitOnly
let methodAssigningField = f.MethodsAssigningMe
select new {
f,
methodAssigningField,
Debt = (2+8*methodAssigningField.Count()).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *static {0}* should be declared as *readonly* because changing it can lead to runtime confusion and hinder code testability.
// In *Object-Oriented Programming*, *instance fields* are the preferred choice for holding modifiable states.
//</Expl>
//<Desc>
// Warns about static fields not declared read-only. **Instance fields** are
// the natural OOP artifact for mutable state; mutable static fields create
// *confusion* about runtime state and impair testability (the same mutable
// state is re-used per test). More:
// https://blog.ndepend.com/the-proper-usages-of-the-keyword-static-in-c/
//</Desc>
//<HowToFix>
// If assigned once per program lifetime, declare the field *readonly* and
// assign it inline or in the static constructor. Otherwise refactor to hold
// the state in an instance field.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid static fields with a mutable field type - Immutability
Warns about static fields with a mutable field type: they hold modifiable state.
Instance fields are the natural OOP artifact for mutable state; such static fields create
confusion about runtime state. More:
https://blog.ndepend.com/the-proper-usages-of-the-keyword-static-in-c/How to Fix:Hold mutable states in objects passed
explicitly wherever consumed, rather than in a static field modifiable from everywhere in the program.
// <Name>Avoid static fields with a mutable field type</Name>
// <Id>ND1902:AvoidStaticFieldsWithAMutableFieldType</Id>
warnif count > 0
from f in Application.Fields
where f.IsStatic &&
!f.IsEnumValue &&
!f.IsGeneratedByCompiler &&
!f.IsLiteral
let fieldType = f.FieldType
where fieldType != null &&
!fieldType.IsThirdParty &&
!fieldType.IsInterface &&
!fieldType.IsDelegate &&
!fieldType.IsImmutable
select new {
f,
mutableFieldType = fieldType ,
isFieldImmutable = f.IsImmutable ? "Immutable" : "Mutable",
isFieldReadOnly = f.IsInitOnly ? "ReadOnly" : "Not ReadOnly",
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *static {0}* is typed with the *{1}* that is mutable: its states can be modified at runtime.
// This can lead to runtime confusion and hinder code testability.
// In *Object-Oriented Programming*, *instance fields* are the preferred choice for holding modifiable states.
//</Expl>
//<Desc>
// Warns about static fields with a mutable field type: they hold modifiable
// state. **Instance fields** are the natural OOP artifact for mutable state;
// such static fields create *confusion* about runtime state. More:
// https://blog.ndepend.com/the-proper-usages-of-the-keyword-static-in-c/
//</Desc>
//<HowToFix>
// Hold mutable states in objects passed **explicitly** wherever consumed,
// rather than in a static field modifiable from everywhere in the program.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Structures should be immutable - Immutability
A structure or class is immutable if its instance fields are only assigned inline or from constructor(s).
Structures are value types: passing or assigning them works on copies, unlike classes (references). Modifying copies of mutable structures leads to confusion and errors. More:
https://blog.ndepend.com/class-vs-struct-in-c-making-informed-choices/How to Fix:Make matched structures immutable: all
passed by value copies then hold the same values, no surprises. Since C#7.2 the
readonly keyword on a struct declaration enforces immutability. Changing a value then means consciously creating a new instance with the modified data.
// <Name>Structures should be immutable</Name>
// <Id>ND1903:StructuresShouldBeImmutable</Id>
warnif count > 0 from t in JustMyCode.Types where
t.IsStructure &&
!t.IsImmutable &&
!t.IsRecord // record struct with primary constructor is not immutable
let mutableFields = t.Fields.Where(f => !f.IsImmutable)
// Don't warn about structure with a fixed field that cannot be marked as readonly
where !t.Fields.Any(f => f.HasAttribute("System.Runtime.CompilerServices.FixedBufferAttribute".AllowNoMatch()))
select new {
t,
t.NbLinesOfCode,
mutableFields,
Debt = (3+2*mutableFields.Count()).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* should be declared as *readonly* because
// its instances are values, not objects.
// Mutable values are unexpected by the users.
// Making it *readonly* ensures predictable behavior,
// thread safety and efficient copying.
//</Expl>
//<Desc>
// A structure or class is immutable if its instance fields are only
// assigned inline or from constructor(s).
//
// **Structures are value types**: passing or assigning them works on
// copies, unlike classes (references). Modifying copies of mutable
// structures leads to confusion and errors. More:
// https://blog.ndepend.com/class-vs-struct-in-c-making-informed-choices/
//</Desc>
//<HowToFix>
// Make matched structures immutable: all *passed by value* copies then hold
// the same values, no surprises. Since C#7.2 the *readonly* keyword on a
// struct declaration enforces immutability. Changing a value then means
// consciously creating a new instance with the modified data.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Immutable struct should be declared as readonly - Immutability
Declare immutable structures as
readonly struct: the compiler and JIT learn the immutability intent and can optimize, e.g. passing/returning instances by-reference instead of copying them (no mutation risk). More:
https://learn.microsoft.com/en-us/dotnet/csharp/write-safe-efficient-code#declare-immutable-structs-as-readonlyHow to Fix:Declare matched structures as
readonly struct.
// <Name>Immutable struct should be declared as readonly</Name>
// <Id>ND1914:ImmutableStructShouldBeDeclaredAsReadonly</Id>
warnif count > 0
from t in JustMyCode.Types
// Match structures that are immutable but not declared as 'readonly struct'
where t.IsStructure &&
t.IsImmutable &&
!t.IsReadOnly &&
!t.IsGeneratedByCompiler
select new {
t,
Debt = 2.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* should be declared as *readonly*.
// This way not only the intent is clearly expressed and some optimizations
// can be performed by the compiler and the runtime.
//</Expl>
//<Desc>
// Declare immutable structures as **readonly struct**: the compiler and JIT
// learn the immutability intent and can optimize, e.g. passing/returning
// instances by-reference instead of copying them (no mutation risk). More:
// https://learn.microsoft.com/en-us/dotnet/csharp/write-safe-efficient-code#declare-immutable-structs-as-readonly
//</Desc>
//<HowToFix>
// Declare matched structures as **readonly struct**.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Property Getters should be pure - Immutability
A property getter with side-effects is unexpected and counterintuitive for callers: getters should be pure, assigning no field.
To avoid matching lazy initialization at first access (an invisible implementation detail for clients), getters assigning a field only assigned by themselves and the corresponding setter are not matched, nor are fields with a name similar to the property name.
How to Fix:
Make matched property getters assign no field.
Estimated Debt: 2 minutes plus 5 minutes per field assigned and 5 minutes per other method assigning such field.
// <Name>Property Getters should be pure</Name>
// <Id>ND1904:PropertyGettersShouldBeImmutable</Id>
warnif count > 0 from m in Application.Methods where
!m.IsGeneratedByCompiler &&
m.IsPropertyGetter &&
m.ParentProperty != null &&
( ( !m.IsStatic && m.ChangesObjectState) ||
( m.IsStatic && m.ChangesTypeState) )
let propertyName = m.ParentProperty.SimpleName
let fieldsAssigned = m.FieldsAssigned.Where(f =>
// Don't count field that have a name similar to the property name
// to avoid matching lazy initialization situations.
!(propertyName.Length >= 4 &&
f.SimpleName.Length >= 4 &&
// Don't count the first 3 characters of the field name,
// to avoid special field name formatting like 'm_X' or '_x'
propertyName.EndsWith(f.SimpleName.Substring(3, f.SimpleName.Length -3)))
&&
// Don't count field that are assigned only by the property getter and the related property setter.
f.MethodsAssigningMe.Any(m1 => m1 != m &&
!(m1.IsPropertySetter && m1.ParentProperty == m1.ParentProperty)))
where fieldsAssigned.Any()
let otherMethodsAssigningSameFields = fieldsAssigned.SelectMany(f => f.MethodsAssigningMe.Where(m1 => m1 != m))
select new {
m,
m.NbLinesOfCode,
fieldsAssigned,
otherMethodsAssigningSameFields,
Debt = (2 + 5*fieldsAssigned.Count() + 5*otherMethodsAssigningSameFields.Count() ).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* is assigning the *{2}*.
// From the caller perspective, it's generally unexpected and
// counterintuitive for a property's getter to have *side-effect*s
//</Expl>
//<Desc>
// A property getter with *side-effects* is unexpected and counterintuitive
// for callers: getters should be *pure*, assigning no field.
//
// To avoid matching *lazy initialization at first access* (an invisible
// implementation detail for clients), getters assigning a field only
// assigned by themselves and the corresponding setter are not matched, nor
// are fields with a name similar to the property name.
//</Desc>
//<HowToFix>
// Make matched property getters assign no field.
//
// Estimated Debt: 2 minutes plus 5 minutes per field assigned and 5 minutes
// per other method assigning such field.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
A field must not be assigned from outside its parent hierarchy types - Immutability
Related to Fields should be declared as private: matches public or internal, mutable fields assigned from outside their parent class and subclasses. Fields are implementation details and should be private.
When a non-private field misbehaves, the culprit can be anywhere; a private field can only be assigned from its own class, usually one source file to look at.
How to Fix:
Declare matched fields protected or better private. A field exclusively referencing immutable states can stay accessible but must be declared readonly.
Estimated Debt: 5 minutes per method outside the parent hierarchy assigning the field.
// <Name>A field must not be assigned from outside its parent hierarchy types</Name>
// <Id>ND1905:AFieldMustNotBeAssignedFromOutsideItsParentHierarchyTypes</Id>
warnif count > 0
from f in JustMyCode.Fields.Where(f =>
(f.IsInternal || f.IsPublic) &&
!f.IsGeneratedByCompiler &&
!f.IsImmutable &&
!f.IsEnumValue)
let methodsAssignerOutsideOfMyType = f.MethodsAssigningMe.Where(
m =>!m.IsGeneratedByCompiler &&
m.ParentType != f.ParentType &&
!m.ParentType.DeriveFrom(f.ParentType) )
where methodsAssignerOutsideOfMyType.Any()
select new {
f,
methodsAssignerOutsideOfMyType,
Debt = (5*methodsAssignerOutsideOfMyType.Count()).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* is assigned outside its parent type by the *{1}*.
// A field should be an encapsulated *implementation detail*.
// Consider making it *private* or refactor it as a property
// whose accesses can be better controlled at debug-time.
//</Expl>
//<Desc>
// Related to *Fields should be declared as private*: matches **public or
// internal, mutable fields** assigned from outside their parent class and
// subclasses. Fields are **implementation details** and should be private.
//
// When a *non-private field* misbehaves, the culprit can be anywhere; a
// private field can only be assigned from its own class, usually one source
// file to look at.
//</Desc>
//<HowToFix>
// Declare matched fields *protected* or better *private*. A field
// exclusively referencing immutable states can stay accessible but must be
// declared readonly.
//
// Estimated Debt: 5 minutes per method outside the parent hierarchy
// assigning the field.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't assign a field from many methods - Immutability
A field assigned from many methods is a symptom of bug-prone code (constructors assigning the field are not counted). Threshold: 4+ assigning methods for instance fields; 3+ for static fields, whose global random accesses are even worse. Such anarchically assigned fields make runtime state hard to anticipate: code hard to read, debug and maintain, with hard-to-solve corrupted-state bugs.
With multiple threads there are alarming chances the code is unmaintainable and bugged — the rule of thumb is immutable objects. Corrupted reference-type state can yield NullReferenceException; corrupted value-type state can yield silently wrong results.
How to Fix:
No straight advice. Sometimes it is simple: e.g. an indentation field assigned by many writers just needs IndentPlus()/IndentMinus() methods. Sometimes a complex algorithm must be rethought and rewritten: the field may become a local variable or closure, or rethinking the parent object's lifetime and role can make it immutable (constructor-assigned only).
Estimated Debt: 4 minutes plus 5 minutes per method assigning the instance field, or 10 minutes per method assigning the static field.
// <Name>Don't assign a field from many methods</Name>
// <Id>ND1906:DontAssignAFieldFromManyMethods</Id>
warnif count > 0
from f in JustMyCode.Fields where
!f.IsEnumValue &&
!f.IsImmutable &&
!f.IsInitOnly &&
!f.IsGeneratedByCompiler &&
!f.IsEventDelegateObject
let methodsAssigningMe = f.MethodsAssigningMe.Where(m => !m.IsConstructor)
// The thresholds 4 and 3 are arbitrary and it should avoid matching too many fields.
// Threshold is even lower for static fields because this reveals situations even more complex.
where methodsAssigningMe.Count() >= (!f.IsStatic ? 4 : 3)
select new {
f,
methodsAssigningMe,
f.MethodsReadingMeButNotAssigningMe,
f.MethodsUsingMe,
staticOrInst = f.IsStatic ? "static" : "instance",
Debt = (4+(f.IsStatic ? 10 : 5)).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{4} {0}* is assigned from the *{1}*.
// Do reduce the number of methods that assign it.
// By now there is no clear and predictable pattern
// for how this state changes at runtime.
//</Expl>
//<Desc>
// A field assigned from many methods is a symptom of **bug-prone code**
// (constructors assigning the field are not counted). Threshold: 4+
// assigning methods for *instance fields*; 3+ for *static fields*, whose
// global random accesses are even worse. Such *anarchically assigned*
// fields make runtime state hard to anticipate: code hard to read, debug
// and maintain, with hard-to-solve corrupted-state bugs.
//
// With multiple threads there are **alarming chances** the code is
// unmaintainable and bugged — the rule of thumb is immutable objects.
// Corrupted reference-type state can yield *NullReferenceException*;
// corrupted value-type state can yield silently wrong results.
//</Desc>
//<HowToFix>
// No straight advice. Sometimes it is simple: e.g. an indentation field
// assigned by many writers just needs *IndentPlus()/IndentMinus()* methods.
// Sometimes a complex algorithm must be rethought and rewritten: the field
// may become a local variable or *closure*, or rethinking the parent
// object's lifetime and role can make it immutable (constructor-assigned
// only).
//
// Estimated Debt: 4 minutes plus 5 minutes per method assigning the
// instance field, or 10 minutes per method assigning the static field.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Do not declare read only fields with mutable reference types - Immutability
Violated when a public type contains a public read-only field of a mutable reference type. Misleading: readonly suggests a constant object, but only the reference is fixed — the referenced object's state can still change.
How to Fix:
Replace the field type with an immutable type, or declare the field private. Severity is Low: more an advice than a problem.
// <Name>Do not declare read only fields with mutable reference types</Name>
// <Id>ND1907:DoNotDeclareReadOnlyMutableReferenceTypes</Id>
warnif count > 0
from f in JustMyCode.Fields where
!f.IsStatic &&
f.IsInitOnly &&
f.IsPublic &&
f.FieldType != null &&
f.FieldType.IsClass &&
!f.FieldType.IsThirdParty &&
!f.FieldType.IsImmutable
select new {
f,
f.FieldType,
FieldVisibility = f.Visibility,
Debt = 3.ToMinutes().ToDebt(),
Severity = Severity.Low
}
//<Expl>
// The *public readonly {0}* is typed with the *{1}* that is mutable: its states can be modified at runtime.
// This situation can be misleading: the keyword *readonly* mistakenly suggests that the object remains constant,
// while in fact, the field reference remains fixed but the referenced object's state can still change.
// Replace the field type with an immutable class or declare the field as *private*.
//</Expl>
//<Desc>
// Violated when a *public* type contains a *public* read-only field of a
// mutable reference type. Misleading: *readonly* suggests a constant
// object, but only the reference is fixed — the referenced object's state
// can still change.
//</Desc>
//<HowToFix>
// Replace the field type with an immutable type, or declare the field
// *private*. Severity is **Low**: more an advice than a problem.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Public read only array fields can be modified - Immutability
Violated when a publicly visible read-only field holds an array: the reference cannot change, but the array elements can. Code relying on the elements of a publicly accessible read-only array might contain an exploitable security vulnerability.
How to Fix:
Don't rely on the contents of a publicly accessible read-only array. Either:
- Replace the array with an unchangeable strongly typed collection, e.g. System.Collections.Generic.IReadOnlyList<T> ; System.Collections.Generic.IReadOnlyCollection<T> ; System.Collections.ReadOnlyCollectionBase
- Or replace the public field with a method returning a clone of a private array: modified clone elements are harmless.
Severity is Low: more an advice than a problem.
// <Name>Public read only array fields can be modified</Name>
// <Id>ND1908:ArrayFieldsShouldNotBeReadOnly</Id>
warnif count > 0
from f in Application.Fields where
f.IsInitOnly &&
f.IsPubliclyVisible &&
f.FieldType != null &&
f.FieldType.FullName == "System.Array"
select new {
f,
FieldVisibility = f.Visibility,
Debt = 3.ToMinutes().ToDebt(),
Severity = Severity.Low
}
//<Expl>
// The *public {0}* is declared as readonly and holds an array.
// The slots within the array can be assigned by malicious client code
// potentially exposing a security vulnerability.
//</Expl>
//<Desc>
// Violated when a publicly visible read-only field holds an array: the
// reference cannot change, but the array elements can. Code relying on the
// elements of a publicly accessible read-only array might contain an
// exploitable *security vulnerability*.
//</Desc>
//<HowToFix>
// Don't rely on the contents of a publicly accessible read-only array.
// Either:
//
// - Replace the array with an unchangeable strongly typed collection, e.g.
// *System.Collections.Generic.IReadOnlyList<T>* ;
// *System.Collections.Generic.IReadOnlyCollection<T>* ;
// *System.Collections.ReadOnlyCollectionBase*
//
// - Or replace the public field with a method returning a clone of a
// private array: modified clone elements are harmless.
//
// Severity is **Low**: more an advice than a problem.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Types tagged with ImmutableAttribute must be immutable - Immutability
A type is immutable if its instance fields are only assigned inline or from constructor(s). NDepend.Attributes.ImmutableAttribute (from NDepend.API.dll, or your own attribute, adapting the rule) expresses in code that a type is immutable; this rule continuously checks it and warns when a tagged type is no longer immutable.
How to Fix:
Find which modification broke immutability (the culpritFields list helps), then refactor the type back to immutability.
Estimated Debt: 5 minutes plus 10 minutes per culprit field.
// <Name>Types tagged with ImmutableAttribute must be immutable</Name>
// <Id>ND1909:TypesTaggedWithImmutableAttributeMustBeImmutable</Id>
warnif count > 0
from t in Application.Types where
t.HasAttribute ("NDepend.Attributes.ImmutableAttribute".AllowNoMatch()) &&
!t.IsImmutable
let culpritFields = t.Fields.Where(f => !f.IsStatic && !f.IsImmutable)
select new {
t,
culpritFields,
Debt = (5+10*culpritFields.Count()).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* is tagged with the *ImmutableAttribute* yet it is mutable:
// its *{1}* can be assigned.
// This attribute indicates a commitment to keep this type immutable
// so make sure its instance states cannot be changed after construction.
//</Expl>
//<Desc>
// A type is immutable if its instance fields are only assigned inline or
// from constructor(s). **NDepend.Attributes.ImmutableAttribute** (from
// *NDepend.API.dll*, or your own attribute, adapting the rule) expresses in
// code that a type is immutable; this rule continuously checks it and warns
// when a tagged type is no longer immutable.
//</Desc>
//<HowToFix>
// Find which modification broke immutability (the *culpritFields* list
// helps), then refactor the type back to immutability.
//
// Estimated Debt: 5 minutes plus 10 minutes per culprit field.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Types immutable should be tagged with ImmutableAttribute - Immutability
A type is immutable if its instance fields are only assigned inline or from constructor(s). This query lists immutable types not tagged with NDepend.Attributes.ImmutableAttribute. The attribute expresses in source code the intention that a class is and should remain immutable — an intention continuously checked by the rule Types tagged with ImmutableAttribute must be immutable.
How to Fix:
Tag matched types with ImmutableAttribute from NDepend.API.dll, or your own attribute (adapting this query).
// <Name>Types immutable should be tagged with ImmutableAttribute</Name>
// <Id>ND1910:TypesImmutableShouldBeTaggedWithImmutableAttribute</Id>
// warnif count > 0 <-- not a code rule per default
from t in Application.Types where
!t.HasAttribute ("NDepend.Attributes.ImmutableAttribute".AllowNoMatch()) &&
t.IsImmutable &&
// Don't need to tag record classes with [Immutable]
!t.IsRecord &&
!t.IsStructure // An immutable structure must be marked as readonly
select new {
t,
t.NbLinesOfCode,
Debt = 3.ToMinutes().ToDebt(),
Severity = Severity.Low
}
//<Expl>
// The *{0}* is immutable which makes it easy to work with, especially
// in concurrent scenarios.
// Create an *ImmutableAttribute* attribute and tag this type with it.
// This way the intent to keep it immutable is clearly
// expressed and regularly checked through the rule
// *Types tagged with ImmutableAttribute must be immutable*.
//</Expl>
//<Desc>
// A type is immutable if its instance fields are only assigned inline or
// from constructor(s). This query lists immutable types not tagged with
// **NDepend.Attributes.ImmutableAttribute**. The attribute expresses in
// source code the intention that a class is and should remain immutable —
// an intention continuously checked by the rule *Types tagged with
// ImmutableAttribute must be immutable*.
//</Desc>
//<HowToFix>
// Tag matched types with **ImmutableAttribute** from *NDepend.API.dll*, or
// your own attribute (adapting this query).
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Methods tagged with PureAttribute must be pure - Immutability
A pure method changes no instance or static field: a function producing a result from inputs, simplifying code by limiting side-effects. A PureAttribute expresses purity in code; this rule continuously checks it and warns when a tagged method is no longer pure.
Use NDepend.Attributes.PureAttribute (from NDepend.API.dll), System.Diagnostics.Contracts.PureAttribute (honored by the compiler only with Microsoft Code Contract enabled), or your own PureAttribute (adapting the rule).
How to Fix:
Find which modification broke the method purity, then refactor it back to purity.
// <Name>Methods tagged with PureAttribute must be pure</Name>
// <Id>ND1911:MethodsTaggedWithPureAttributeMustBePure</Id>
warnif count > 0
from m in Application.Methods where
( m.HasAttribute ("NDepend.Attributes.PureAttribute".AllowNoMatch()) ||
m.HasAttribute ("System.Diagnostics.Contracts.PureAttribute".AllowNoMatch()) ) &&
( m.ChangesObjectState || m.ChangesTypeState ) &&
m.NbLinesOfCode > 0
let fieldsAssigned = m.FieldsAssigned
select new {
m,
m.NbLinesOfCode,
fieldsAssigned,
Debt = 15.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* is tagged with the *PureAttribute* yet it assigns the *{2}*.
// This attribute indicates a commitment to keep this method pure
// so make sure it doesn't change any state at runtime.
//</Expl>
//<Desc>
// A pure method changes no instance or static field: a **function**
// producing a result from inputs, simplifying code by **limiting
// side-effects**. A **PureAttribute** expresses purity in code; this rule
// continuously checks it and warns when a tagged method is no longer pure.
//
// Use *NDepend.Attributes.PureAttribute* (from *NDepend.API.dll*),
// *System.Diagnostics.Contracts.PureAttribute* (honored by the compiler
// only with Microsoft Code Contract enabled), or your own *PureAttribute*
// (adapting the rule).
//</Desc>
//<HowToFix>
// Find which modification broke the method purity, then refactor it back
// to purity.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Pure methods should be tagged with PureAttribute - Immutability
A pure method changes no instance or static field, simplifying code by limiting side-effects. This query lists pure methods not tagged with a PureAttribute. The attribute expresses in source code that a method is and should remain pure — an intention continuously checked by the rule Methods tagged with PureAttribute must be pure.
Not a rule by default: many methods would match, and fixing plus maintaining that may counter-balance the benefits.
How to Fix:
Tag matched methods with NDepend.Attributes.PureAttribute (from NDepend.API.dll), System.Diagnostics.Contracts.PureAttribute (honored by the compiler only with Microsoft Code Contract enabled), or your own attribute (adapting this query).
// <Name>Pure methods should be tagged with PureAttribute</Name>
// <Id>ND1912:PureMethodsShouldBeTaggedWithPureAttribute</Id>
// warnif count > 0 <-- not a code rule per default
from m in Application.Methods where
!m.IsGeneratedByCompiler &&
!m.HasAttribute ("NDepend.Attributes.PureAttribute".AllowNoMatch()) &&
!m.HasAttribute ("System.Diagnostics.Contracts.PureAttribute".AllowNoMatch()) &&
!m.ChangesObjectState && !m.ChangesTypeState &&
m.NbLinesOfCode > 0
select new {
m,
m.NbLinesOfCode,
Debt = 3.ToMinutes().ToDebt(),
Severity = Severity.Low
}
//<Expl>
// The *{0}* is pure which makes it easy to work with, especially
// in concurrent scenarios.
// Tag this method with *System.Diagnostics.Contracts.PureAttribute*.
// This way the intent to keep it pure is clearly
// expressed and regularly checked through the rule
// *Methods tagged with PureAttribute must be pure*.
//</Expl>
//<Desc>
// A pure method changes no instance or static field, simplifying code by
// **limiting side-effects**. This query lists pure methods not tagged with
// a **PureAttribute**. The attribute expresses in source code that a method
// is and should remain pure — an intention continuously checked by the rule
// *Methods tagged with PureAttribute must be pure*.
//
// Not a rule by default: many methods would match, and fixing plus
// maintaining that may *counter-balance* the benefits.
//</Desc>
//<HowToFix>
// Tag matched methods with *NDepend.Attributes.PureAttribute* (from
// *NDepend.API.dll*), *System.Diagnostics.Contracts.PureAttribute* (honored
// by the compiler only with Microsoft Code Contract enabled), or your own
// attribute (adapting this query).
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Record should be immutable - Immutability
C#9
record is meant for
value-based semantic:
value-based equality and
immutability, explained here:
https://blog.ndepend.com/c9-records-immutable-classes/
A property setter not declared
init still allows mutable state, which disrupts the value-based semantics: mutating a record used as a hash table key can corrupt the table, and equality results become unexpected — bugs difficult to spot and fix.
How to Fix:Make the record immutable: transform mutable property setters (
culpritSetters) into
init property initializers, and refactor their callers (
methodsCallingCulpritSetters) to stop changing record states.
Estimated Debt: 8 minutes plus 3 minutes per mutable setter and per method calling one.
// <Name>Record should be immutable</Name>
// <Id>ND1913:RecordClassesShouldBeImmutable</Id>
warnif count > 0
from t in Application.Types
where t.IsRecord &&
// Don't match record struct because when declared
// with primary constructor it is not immutable
t.IsClass &&
!t.IsImmutable
let culpritSetters = t.InstanceMethods.Where(
m => m.IsPropertySetter && !m.IsPropertyInit)
let methodsCallingCulpritSetters = culpritSetters.SelectMany(
m => m.MethodsCallingMe.Where(mc => mc.ParentType != t) )
select new { t,
culpritSetters,
methodsCallingCulpritSetters ,
Debt = (8 + 3*methodsCallingCulpritSetters.Count()
+ 3*methodsCallingCulpritSetters.Count())
.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *record {0}* is mutable because of the *{1}*.
// It is recommended to refrain from including mutable states
// in a record as it disrupts the *value-based semantics*.
//</Expl>
//<Desc>
// C#9 **record** is meant for **value-based semantic**: **value-based
// equality** and **immutability**, explained here:
// https://blog.ndepend.com/c9-records-immutable-classes/
//
// A property setter not declared *init* still allows mutable state, which
// disrupts the value-based semantics: mutating a record used as a hash
// table key can corrupt the table, and equality results become unexpected —
// bugs difficult to spot and fix.
//</Desc>
//<HowToFix>
// Make the record immutable: transform mutable property setters
// (*culpritSetters*) into *init* property initializers, and refactor their
// callers (*methodsCallingCulpritSetters*) to stop changing record states.
//
// Estimated Debt: 8 minutes plus 3 minutes per mutable setter and per
// method calling one.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Instance fields naming convention - Naming Conventions
Supports the 3 most used instance field naming conventions (adaptable to your own):
- name starting with a lower-case letter
- name starting with "_" followed by a lower-case letter
- name starting with "m_" followed by an upper-case letter
A
static field behaves completely differently from an
instance field, so differentiating them at a glance (e.g.
m_ prefix) is valuable. Related discussion:
https://blog.ndepend.com/on-hungarian-notation-for-fields-in-csharp/How to Fix:After adapting the rule to your naming convention, name all matched instance fields adequately.
// <Name>Instance fields naming convention</Name>
// <Id>ND2000:InstanceFieldsNamingConvention</Id>
warnif count > 0
let isUnityProject = ThirdParty.Assemblies.Any(a => a.Name.StartsWith("UnityEngine"))
from f in JustMyCode.Fields where
!f.IsStatic &&
!f.IsLiteral &&
!f.IsGeneratedByCompiler &&
!f.IsSpecialName &&
!f.IsEventDelegateObject &&
!(
// Instance field name starting with a lower-case letter
(f.Name.Length >= 1 && char.IsLetter(f.Name[0]) && char.IsLower(f.Name[0])) ||
// Instance field name starting with "_" followed with a lower-case letter
(f.Name.Length >= 2 && f.Name.StartsWith("_") && char.IsLetter(f.Name[1]) && char.IsLower(f.Name[1])) ||
// Instance field name starting with "m_" followed with an upper-case letter
(f.Name.Length >= 3 && f.Name.StartsWith("m_") && char.IsLetter(f.Name[2]) && char.IsUpper(f.Name[2])) ||
// In Unity projects often there is no properties for high-performance reasons and fields are named like properties.
(isUnityProject && char.IsLetter(f.Name[0]) && char.IsUpper(f.Name[0]))
) &&
// Don't check naming convention on serializable fields
!f.HasAttribute("System.Xml.Serialization.XmlAttributeAttribute".AllowNoMatch()) &&
!f.HasAttribute("System.Runtime.Serialization.DataMemberAttribute".AllowNoMatch()) &&
// Don't warn if a method using the field is also calling a method that has 'ref' and 'out' parameters.
// This could lead to false positive. A field used in a 'ref' or 'out' parameter cannot be set as read-only.
f.MethodsUsingMe.SelectMany(m => m.MethodsCalled).FirstOrDefault(m => m.Name.Contains("&")) == null
select new {
f,
f.SizeOfInst,
Debt = 3.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *instance {0}* name does not adhere to usual .NET field naming conventions.
// It does not start with a lowercase letter,
// an underscore character followed by a lowercase letter
// or an *m_* expression followed by an uppercase letter.
//</Expl>
//<Desc>
// Supports the 3 most used instance field naming conventions (adaptable to
// your own):
//
// - name starting with a lower-case letter
//
// - name starting with "_" followed by a lower-case letter
//
// - name starting with "m_" followed by an upper-case letter
//
// A *static field* behaves completely differently from an *instance field*,
// so differentiating them at a glance (e.g. **m_** prefix) is valuable.
// Related discussion:
// https://blog.ndepend.com/on-hungarian-notation-for-fields-in-csharp/
//</Desc>
//<HowToFix>
// After adapting the rule to your naming convention, name all matched
// instance fields adequately.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Static fields naming convention - Naming Conventions
Supports the 3 most used static field naming conventions (adaptable to your own):
- name starting with an upper-case letter
- name starting with "_" followed by an upper-case letter
- name starting with "s_" followed by an upper-case letter
A
static field behaves completely differently from an
instance field, so differentiating them at a glance (e.g.
s_ prefix) is valuable. Related discussion:
https://blog.ndepend.com/on-hungarian-notation-for-fields-in-csharp/How to Fix:After adapting the rule to your naming convention, name all matched static fields adequately.
// <Name>Static fields naming convention</Name>
// <Id>ND2001:StaticFieldsNamingConvention</Id>
warnif count > 0
from f in JustMyCode.Fields where
f.IsStatic &&
!f.IsLiteral &&
!f.IsGeneratedByCompiler &&
!f.IsSpecialName &&
!f.IsEventDelegateObject &&
!(
// Static field name starting with an upper-case letter
(f.Name.Length >= 1 && char.IsLetter(f.Name[0]) && char.IsUpper(f.Name[0])) ||
// Static field name starting with "_" followed with an upper-case letter
(f.Name.Length >= 2 && f.Name.StartsWith("_") && char.IsLetter(f.Name[1]) && char.IsUpper(f.Name[1])) ||
// Static field name starting with "s_" followed with an upper-case letter
(f.Name.Length >= 3 && f.Name.StartsWith("s_") && char.IsLetter(f.Name[2]) && char.IsUpper(f.Name[2]))
)
select new {
f,
f.SizeOfInst,
Debt = 3.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *static {0}* name does not adhere to usual .NET field naming conventions.
// It does not start with an uppercase letter,
// an underscore followed by an uppercase letter
// or a *s_* expression followed by an uppercase letter.
//</Expl>
//<Desc>
// Supports the 3 most used static field naming conventions (adaptable to
// your own):
//
// - name starting with an upper-case letter
//
// - name starting with "_" followed by an upper-case letter
//
// - name starting with "s_" followed by an upper-case letter
//
// A *static field* behaves completely differently from an *instance field*,
// so differentiating them at a glance (e.g. **s_** prefix) is valuable.
// Related discussion:
// https://blog.ndepend.com/on-hungarian-notation-for-fields-in-csharp/
//</Desc>
//<HowToFix>
// After adapting the rule to your naming convention, name all matched
// static fields adequately.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Interface name should begin with a 'I' - Naming Conventions
.NET interface names are commonly prefixed with an upper case I; this widely accepted convention is recommended. COM interfaces typically don't follow it, so interfaces tagged ComVisibleAttribute are not matched.
How to Fix:
Prefix matched interfaces names with an upper I.
// <Name>Interface name should begin with a 'I'</Name>
// <Id>ND2002:InterfaceNameShouldBeginWithAI</Id>
// Don't query all Application.Types because some interface generated might not start with an 'I'
// like for example when adding a WSDL reference to a project.
warnif count > 0 from t in JustMyCode.Types where
t.IsInterface &&
// Don't apply this rule for COM interfaces.
!t.HasAttribute("System.Runtime.InteropServices.ComVisibleAttribute".AllowNoMatch())
// Discard outer type(s) name prefix for nested types
let name = !t.IsNested ?
t.Name :
t.Name.Substring(t.Name.LastIndexOf('+') + 1, t.Name.Length - t.Name.LastIndexOf('+') - 1)
where name[0] != 'I'
select new {
t,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}'s name* must begin with a **I**.
//</Expl>
//<Desc>
// .NET interface names are commonly prefixed with an upper case **I**; this
// widely accepted convention is recommended. COM interfaces typically don't
// follow it, so interfaces tagged *ComVisibleAttribute* are not matched.
//</Desc>
//<HowToFix>
// Prefix matched interfaces names with an upper **I**.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Abstract base class should be suffixed with 'Base' - Naming Conventions
Warns about abstract classes not suffixed with Base, a common .NET practice. Only base classes deriving directly from System.Object are matched (not abstract classes in the middle of a hierarchy chain).
How to Fix:
Suffix the names of matched base classes with Base.
// <Name>Abstract base class should be suffixed with 'Base'</Name>
// <Id>ND2003:AbstractBaseClassShouldBeSuffixedWithBase</Id>
warnif count > 0 from t in Application.Types where
t.IsAbstract &&
t.IsClass &&
t.BaseClass != null &&
t.BaseClass.FullName == "System.Object" &&
((!t.IsGeneric && !t.NameLike (@"Base$")) ||
( t.IsGeneric && !t.NameLike (@"Base<")))
select new {
t,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *abstract base {0}'s name* should be suffixed with *Base*.
//</Expl>
//<Desc>
// Warns about *abstract classes* not suffixed with **Base**, a common .NET
// practice. Only base classes deriving directly from *System.Object* are
// matched (not abstract classes in the middle of a hierarchy chain).
//</Desc>
//<HowToFix>
// Suffix the names of matched base classes with **Base**.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Exception class name should be suffixed with 'Exception' - Naming Conventions
Warns about exception classes not suffixed with Exception, a common .NET practice. ExceptionBase is also accepted for base classes.
How to Fix:
Suffix the names of matched exception classes with Exception.
// <Name>Exception class name should be suffixed with 'Exception'</Name>
// <Id>ND2004:ExceptionClassNameShouldBeSuffixedWithException</Id>
warnif count > 0 from t in Application.Types where
t.IsExceptionClass &&
// We use SimpleName, because in case of generic Exception type
// SimpleName suppresses the generic suffix (like <T>).
!t.SimpleNameLike(@"Exception$") &&
!t.SimpleNameLike(@"ExceptionBase$") // Allow the second suffix Base
// for base exception classes.
select new {
t,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *exception {0}'s name* should be suffixed with *Exception*.
//</Expl>
//<Desc>
// Warns about *exception classes* not suffixed with **Exception**, a common
// .NET practice. **ExceptionBase** is also accepted for base classes.
//</Desc>
//<HowToFix>
// Suffix the names of matched exception classes with **Exception**.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Attribute class name should be suffixed with 'Attribute' - Naming Conventions
Warns about attribute classes not suffixed with Attribute, a common .NET practice.
How to Fix:
Suffix the names of matched attribute classes with Attribute.
// <Name>Attribute class name should be suffixed with 'Attribute'</Name>
// <Id>ND2005:AttributeClassNameShouldBeSuffixedWithAttribute</Id>
warnif count > 0 from t in Application.Types where
t.IsAttributeClass &&
// We use SimpleName, because in case of generic Attribute type
// SimpleName suppresses the generic suffix (like <T>).
!t.SimpleNameLike(@"Attribute$") &&
!t.SimpleNameLike(@"AttributeBase$") // Allow the second suffix Base
// for base attribute classes.!t.SimpleName.EndsWith(@"Attribute")
select new {
t,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *attribute {0}'s name* should be suffixed with *Attribute*.
//</Expl>
//<Desc>
// Warns about *attribute classes* not suffixed with **Attribute**, a common
// .NET practice.
//</Desc>
//<HowToFix>
// Suffix the names of matched attribute classes with **Attribute**.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Types name should begin with an Upper character - Naming Conventions
Warns about types whose names don't start with an Upper character. .NET commonly uses Pascal Casing Style (first letter of the identifier and of each concatenated word capitalized, e.g. BackColor).
How to Fix:
Pascal Case the names of matched types.
// <Name>Types name should begin with an Upper character</Name>
// <Id>ND2006:TypesNameShouldBeginWithAnUpperCharacter</Id>
warnif count > 0
let isAspNetApp = ThirdParty.Assemblies.WithName("System.Web").Any()
from t in JustMyCode.Types where
// The name of a type should begin with an Upper letter.
!t.SimpleNameLike (@"^[A-Z]") &&
// Except if it is generated by compiler.
!t.IsSpecialName &&
!t.IsGeneratedByCompiler &&
// Special default ASP.NET type named "_Default"
!(isAspNetApp && t.SimpleNameEquals("_Default"))
select new {
t,
// We show the type simple name
// that doesn't include the parent type name
// for nested types.
t.SimpleName,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}'s name* should begin with an uppercase letter.
//</Expl>
//<Desc>
// Warns about *types* whose names don't start with an Upper character.
// .NET commonly uses **Pascal Casing Style** (first letter of the
// identifier and of each concatenated word capitalized, e.g. *BackColor*).
//</Desc>
//<HowToFix>
// *Pascal Case* the names of matched types.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Methods name should begin with an Upper character - Naming Conventions
Warns about methods whose names don't start with an Upper character. .NET commonly uses Pascal Casing Style (first letter of the identifier and of each concatenated word capitalized, e.g. ComputeSize).
How to Fix:
Pascal Case the names of matched methods.
// <Name>Methods name should begin with an Upper character</Name>
// <Id>ND2007:MethodsNameShouldBeginWithAnUpperCharacter</Id>
warnif count > 0
from m in JustMyCode.Methods where
!m.NameLike (@"^[A-Z]") &&
!m.IsSpecialName &&
!m.IsGeneratedByCompiler &&
!m.IsExplicitInterfaceImpl && // This can generate unexpected method name.
// For records some generated methods are not marked as generated
!(m.SimpleName.Length > 0 && m.SimpleName[0] == '<') &&
// Some extension property accessors must start with these
!m.SimpleName.StartsWithAny("get_", "set_")
select new {
m,
Debt = 3.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}'s name* should begin with an uppercase letter.
//</Expl>
//<Desc>
// Warns about *methods* whose names don't start with an Upper character.
// .NET commonly uses **Pascal Casing Style** (first letter of the
// identifier and of each concatenated word capitalized, e.g. *ComputeSize*).
//</Desc>
//<HowToFix>
// *Pascal Case* the names of matched methods.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Do not name enum values 'Reserved' - Naming Conventions
An enum member named "Reserved" is presumably a placeholder to rename or remove later — a breaking change. Users can't be expected to ignore it or read documentation, and it causes confusion in object browsers and IDEs. Instead, add a new member in the future version: usually not a breaking change as long as original members' values don't change.
How to Fix:
Remove or rename the member.
// <Name>Do not name enum values 'Reserved'</Name>
// <Id>ND2008:DoNotNameEnumValuesReserved</Id>
warnif count > 0
from f in Application.Fields where
f.IsEnumValue &&
f.NameLike (@"Reserved")
select new {
f,
Debt = 3.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}'s name* shouldn't be *Reserved*.
// This might break consummer code in the future
// in case of change.
//</Expl>
//<Desc>
// An enum member named **"Reserved"** is presumably a placeholder to rename
// or remove later — a breaking change. Users can't be expected to ignore it
// or read documentation, and it causes confusion in object browsers and
// IDEs. Instead, add a new member in the future version: usually not a
// breaking change as long as original members' values don't change.
//</Desc>
//<HowToFix>
// Remove or rename the member.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid types with name too long - Naming Conventions
Matches types with names longer than 40 characters: less readable, and possibly a sign the type does too much.
How to Fix:
Rename the type with a shorter name or split it into more fine-grained types.
// <Name>Avoid types with name too long</Name>
// <Id>ND2009:AvoidTypesWithNameTooLong</Id>
warnif count > 0
from t in JustMyCode.Types
where !t.IsGeneratedByCompiler
where t.SimpleName.Length > 40
orderby t.SimpleName.Length descending
select new {
t,
t.SimpleName,
NameLength = t.SimpleName.Length,
Debt = 3.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}'s name* is excessively long, which tends to reduce code readability.
//</Expl>
//<Desc>
// Matches types with names longer than 40 characters: less readable, and
// possibly a sign the type does too much.
//</Desc>
//<HowToFix>
// Rename the type with a shorter name or split it into more fine-grained
// types.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid methods with name too long - Naming Conventions
Matches methods with names longer than 40 characters: less readable, and possibly a sign the method does too much. Very expressive names are good practice for unit tests though, so test methods and methods of test classes are not matched.
How to Fix:
Rename the method with a shorter name equally conveying its behavior, or split it into smaller methods.
// <Name>Avoid methods with name too long</Name>
// <Id>ND2010:AvoidMethodsWithNameTooLong</Id>
warnif count > 0
from m in JustMyCode.Methods where
// Explicit Interface Implementation methods are
// discarded because their names are prefixed
// with the interface name.
!m.IsExplicitInterfaceImpl &&
!m.IsGeneratedByCompiler &&
((!m.IsSpecialName && m.SimpleName.Length > 50) ||
// Property getter/setter are prefixed with "get_" "set_" of length 4.
( m.IsSpecialName && m.SimpleName.Length - 4 > 50)) &&
!m.SimpleName.Contains("Test") &&
!m.ParentType.SimpleName.Contains("Test") &&
!m.ParentNamespace.Name.Contains("Test")
orderby m.SimpleName.Length descending
select new {
m,
m.SimpleName,
NameLength = m.SimpleName.Length - (m.IsSpecialName ? 4 : 0),
Debt = 3.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}'s name* is excessively long, which tends to reduce code readability.
//</Expl>
//<Desc>
// Matches methods with names longer than 40 characters: less readable, and
// possibly a sign the method does too much. Very expressive names are good
// practice for unit tests though, so test methods and
// methods of test classes are not matched.
//</Desc>
//<HowToFix>
// Rename the method with a shorter name equally conveying its behavior, or
// split it into smaller methods.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid fields with name too long - Naming Conventions
Matches fields with names longer than 40 characters: less readable.
How to Fix:
Rename the field with a shorter name conveying the same information.
// <Name>Avoid fields with name too long</Name>
// <Id>ND2011:AvoidFieldsWithNameTooLong</Id>
warnif count > 0 from f in JustMyCode.Fields where
!f.IsGeneratedByCompiler &&
f.Name.Length > 50
orderby f.Name descending
select new {
f,
NameLength = f.Name.Length,
Debt = 3.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}'s name* is excessively long, which tends to reduce code readability.
//</Expl>
//<Desc>
// Matches fields with names longer than 40 characters: less readable.
//</Desc>
//<HowToFix>
// Rename the field with a shorter name conveying the same information.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid having different types with same name - Naming Conventions
Warns about multiple same-named types in different application or third-party namespaces or assemblies: confusion and naming collisions in source files using several of them.
How to Fix:
Rename concerned types.
// <Name>Avoid having different types with same name</Name>
// <Id>ND2012:AvoidHavingDifferentTypesWithSameName</Id>
warnif count > 0
// Special type names for which multiple types
// with such name are allowed.
let nonReportedTypeName = new [] {
"Program", "NamespaceDoc", "Initial", "Startup",
"SwaggerConfig", "Constants", "MainWindow", "App", "Index",
"Create", "Read", "Update", "Delete", "Edit", "Details",
"View" }
// Special type name suffixes for which multiple types
// with such suffix are allowed.
let nonReportedTypeNameSuffix = new [] {
"Service", "Model", "Controller", "Page", "Pages" }
// This rule matches also collisions between
// application and third-party types sharing a same name.
let groups = JustMyCode.Types.Union(ThirdParty.Types)
// Discard nested types, whose name is
// prefixed with the parent type name.
.Where(t => !t.IsNested &&
!nonReportedTypeName.Contains(t.Name) &&
!t.Name.EndsWithAny(nonReportedTypeNameSuffix))
// Group types by name.
.GroupBy(t => t.Name)
from @group in groups
where @group.Count() > 1
// Let's see if types with the same name are declared
// in different namespaces.
// (t.FullName is {namespaceName}.{typeName} )
let groupsFullName = @group.GroupBy(t => t.FullName)
where groupsFullName.Count() > 1
// If several types with same name are declared in different
// namespaces eliminate the case where all types are declared
// in third-party assemblies.
let types = groupsFullName.SelectMany(g => g)
where types.Any(t => !t.IsThirdParty)
// Uncomment this line, to only gets naming collision involving
// both application and third-party types.
// && types.Any(t => t.IsThirdParty)
// Discard the group if at least two types are declared in
// the same source file(s): it means such files are shared
// amongst projects and it is not a problem.
let filePaths = types
.Where(t => t.SourceFileDeclAvailable)
.SelectMany(t => t.SourceDecls
.Select(d => d.SourceFile.FilePathString)
.Distinct(StringComparer.OrdinalIgnoreCase))
.ToArray()
where filePaths.Length == filePaths
.Distinct(StringComparer.OrdinalIgnoreCase).Count()
orderby types.Count() descending
// Don't match types containing extension methods
// whose name is less relevant
where !types.All(t => t.IsStatic &&
t.Methods.Any(m => m.IsExtensionMethod))
// Order types by parent namespace and assembly name, this way
// we always get the same type across sessions. Without this
// astute, the same issue would be seen as added/removed when
// the first type choosen in the group was not always the same,
// a situation that actually happens. Also make sure that the
// chosen type is not a third-party one.
let tMatched = types.OrderBy(t =>
(t.IsThirdParty ? "1" : "0") +
t.ParentNamespace.Name + t.ParentAssembly.Name).First()
select new {
t = tMatched,
// In the 'types' column, make sure to group matched types
// by parent assemblies and parent namespaces, to get a
// result more readable.
types,
namespaces = types.Except(tMatched)
.Select(t => t.ParentNamespace).Distinct(n => n.Name),
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// {2?A type:Several types} with the same name as the *{0}*
// {2?is:are} defined in the *{2}*.
// This leads to confusion and also naming collision.
//</Expl>
//<Desc>
// Warns about multiple same-named types in different
// *application* or *third-party* namespaces or assemblies:
// confusion and naming collisions in source files using
// several of them.
//</Desc>
//<HowToFix>
// Rename concerned types.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid prefixing type name with parent namespace name - Naming Conventions
Warns when the parent namespace name prefixes a contained type name (e.g. "RuntimeEnvironment" in namespace "Foo.Runtime" should be "Environment"): redundant naming without readability improvement. Types are not matched when several of them would end up with the same name once the prefix is removed (e.g. "MetricInfo" and "QualityGateInfo" both becoming "Info"). This rule doesn't warn about situations where two or more types end up with the same name.
How to Fix:
Remove the prefix from the type name.
// <Name>Avoid prefixing type name with parent namespace name</Name>
// <Id>ND2013:AvoidPrefixingTypeNameWithParentNamespaceName</Id>
warnif count > 0
// Candidate types whose name starts with the parent namespace
// name, followed by an upper-case letter. This way we avoid
// matching false-positive where namespace name is "Stat" and
// type name is "Statistic".
let candidates = (
from t in JustMyCode.Types
where !t.IsNested &&
!t.IsGeneratedByCompiler
let ns = t.ParentNamespace.SimpleName
where ns.Length > 0 &&
t.Name.Length > ns.Length &&
char.IsUpper(t.Name[ns.Length]) &&
t.Name.StartsWith(ns)
select new {
t,
namespaceName = ns,
suffix = t.Name.Substring(ns.Length)
}
).ToArray()
// Suffixes shared by several candidate types. Removing the
// prefix from all of them would end up with several types
// sharing the same name (e.g. MetricInfo and QualityGateInfo
// would both become Info). Don't warn in this situation.
let ambiguousSuffixes = candidates
.GroupBy(c => c.suffix)
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.ToHashSetEx()
from c in candidates
where !ambiguousSuffixes.Contains(c.suffix)
select new {
c.t,
c.namespaceName,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}'s name* starts with the parent namespace name *{1}*.
// This results in redundant naming without any improvement in
// readability.
//</Expl>
//<Desc>
// Warns when the parent namespace name prefixes a contained type
// name (e.g. "RuntimeEnvironment" in namespace "Foo.Runtime"
// should be "Environment"): redundant naming without readability
// improvement. Types are not matched when several of them would
// end up with the same name once the prefix is removed (e.g.
// "MetricInfo" and "QualityGateInfo" both becoming "Info").
// This rule doesn't warn about situations where two or more
// types end up with the same name.
//</Desc>
//<HowToFix>
// Remove the prefix from the type name.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid naming types and namespaces with the same identifier - Naming Conventions
Warns when a type and a namespace share the same name (e.g. type Environment and namespace Foo.Environment): tedious compiler resolution collisions and less readable code.
How to Fix:
Rename the concerned type or namespace.
// <Name>Avoid naming types and namespaces with the same identifier</Name>
// <Id>ND2014:AvoidNamingTypesAndNamespacesWithTheSameIdentifier</Id>
warnif count > 0
let hashsetShortNames = JustMyCode.Namespaces.Where(n => n.Name.Length > 0).Select(n => n.SimpleName).ToHashSetEx()
from t in JustMyCode.Types
where hashsetShortNames.Contains(t.Name)
select new {
t,
namespaces = Namespaces.Where(n => n.SimpleNameEquals(t.Name)),
Debt = 12.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* shares the same name as the *{1}*.
// This situation is ambiguous and can lead to complex compilation errors.
//</Expl>
//<Desc>
// Warns when a type and a namespace share the same name (e.g. type
// *Environment* and namespace *Foo.Environment*): tedious compiler
// resolution collisions and less readable code.
//</Desc>
//<HowToFix>
// Rename the concerned type or namespace.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't call your method Dispose - Naming Conventions
The identifier Dispose should be kept for System.IDisposable implementations. Warns when a method named Dispose() belongs to a type not implementing System.IDisposable.
How to Fix:
Either implement System.IDisposable on the parent type, or rename the method, e.g. Close() Terminate() Finish() Quit() Exit() Unlock() ShutDown()…
// <Name>Don't call your method Dispose</Name>
// <Id>ND2015:DontCallYourMethodDispose</Id>
warnif count > 0
// Activate this rule only when IDisposable is resolved in third-party code
// else this rule might produce false positives.
let thirdPartyDisposable = ThirdParty.Types.WithFullName("System.IDisposable").FirstOrDefault()
where thirdPartyDisposable != null
from m in JustMyCode.Methods.WithSimpleName("Dispose")
where !m.ParentType.Implement("System.IDisposable".AllowNoMatch())
&& m.OverriddensBase.Count() == 0 // Can't change the name of an override
&& !m.IsNewSlot // new slot also means override of an interface method
select new {
m,
Debt = 15.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The parent type of the *{0}* doesn't implement the *IDisposable* interface.
// You should either implement it or rename this method using a different
// identifier, such as:: *Close() Terminate() Finish() Quit() Exit() Unlock() ShutDown()*…
//</Expl>
//<Desc>
// The identifier *Dispose* should be kept for *System.IDisposable*
// implementations. Warns when a method named *Dispose()* belongs to a type
// not implementing *System.IDisposable*.
//</Desc>
//<HowToFix>
// Either implement *System.IDisposable* on the parent type, or rename the
// method, e.g. *Close() Terminate() Finish() Quit() Exit() Unlock()
// ShutDown()*…
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Methods prefixed with 'Try' should return a boolean - Naming Conventions
A method prefixed with Try is expected to return a boolean (or Task<bool> if async) reflecting success or failure, usually with the result in an out parameter, e.g. System.Int32.TryParse(int,out string):bool
How to Fix:
Rename the method, or transform it into an operation that can fail.
// <Name>Methods prefixed with 'Try' should return a boolean</Name>
// <Id>ND2016:MethodsPrefixedWithTryShouldReturnABoolean</Id>
warnif count > 0
from m in Application.Methods where
m.SimpleNameLike("^Try") &&
m.ReturnType != null &&
// Accept boolean or value tuple (eventually nullable) as return type
!m.ReturnType.FullNameEqualsAny("System.Boolean","System.Nullable<T>") &&
m.ReturnType.SimpleName != "ValueTuple" &&
!m.ReturnType.Name.Contains("Task")
select new {
m,
m.ReturnType,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* returns the *{1}*.
// Usually a method whose name starts with *Try* returns a *bool* value
// to allow client code to test whether its execution succeeded or not.
//</Expl>
//<Desc>
// A method prefixed with **Try** is expected to return a *boolean* (or
// *Task<bool>* if async) reflecting *success* or *failure*, usually with
// the result in an *out parameter*, e.g.
// *System.Int32.TryParse(int,out string):bool*
//</Desc>
//<HowToFix>
// Rename the method, or transform it into an operation that can fail.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Properties and fields that represent a collection of items should be named Items. - Naming Conventions
Properties and fields typed with a collection of items should be named with the plural form of Items (e.g. NewDirectories, Words, Values), for readability and predictability.
No warning when any word in the identifier ends with s (TasksToRun, KeysDisabled, ListOfElementsInResult… are valid), nor when the identifier contains Empty (common pattern for a shared immutable empty collection).
Collection types are those implementing IEnumerable<T>, except:
- Non generic types: rarely seen as collections (e.g. System.String implements IEnumerable<char> but strings are named like FirstName).
- Dictionaries: mappings rather than collections, commonly suffixed Map, Table or Dictionary (though names like PersonsByNames often satisfy this rule anyway).
How to Fix:
Rename by making plural the word best describing the items:
- ListOfDir can be renamed Directories.
- Children can be renamed ChildrenItems
- QueueForCache can be renamed QueueOfItemsForCache
// <Name>Properties and fields that represent a collection of items should be named Items.</Name>
// <Id>ND2017:PropertiesAndFieldsThatRepresentACollectionOfItemsShouldBeNamedItems</Id>
warnif count > 0
let collectionTypes = Types.Where(t =>
t.Implement("System.Collections.Generic.IEnumerable<T>".AllowNoMatch()) &&
t.IsGeneric &&
t.FullName != "System.Collections.Generic.IDictionary<TKey,TValue>" &&
!t.Implement("System.Collections.Generic.IDictionary<TKey,TValue>".AllowNoMatch()) &&
t.FullName != "System.Collections.Generic.IReadOnlyDictionary<TKey,TValue>" &&
!t.Implement("System.Collections.Generic.IReadOnlyDictionary<TKey,TValue>".AllowNoMatch())).ToHashSetEx()
let properties = from p in JustMyCode.Properties
where p.PropertyType != null && collectionTypes.Contains(p.PropertyType)
select new { Member = (IMember)p, Type = p.PropertyType }
let fields = from f in JustMyCode.Fields
where f.FieldType != null && collectionTypes.Contains(f.FieldType)
select new { Member = (IMember)f, Type = f.FieldType }
from pair in properties.Concat(fields)
let identifier = pair.Member.SimpleName
where !identifier.EndsWith("s") &&
!identifier.Contains('<') // Remove potential generated fields like backing fields, and also potential generated properties accessors
let words = identifier.GetWords()
where !words.Any(word =>
word.EndsWith("s") || // Don't warn if any word ends with an s
word == "Empty") // Don't warn if any word in the identifier is Empty
select new { pair.Member, pair.Type }
//<Expl>
// The *{0}* is typed with the *collection {1}*.
// Its name should contain the plural form of *Items*
// in the collection to enhance code readability.
//</Expl>
//<Desc>
// Properties and fields typed with a collection of *items* should be named
// with the plural form of *Items* (e.g. *NewDirectories*, *Words*,
// *Values*), for readability and predictability.
//
// No warning when any word in the identifier ends with *s* (**TasksToRun**,
// **KeysDisabled**, **ListOfElementsInResult**… are valid), nor when the
// identifier contains **Empty** (common pattern for a shared immutable
// empty collection).
//
// Collection types are those implementing *IEnumerable<T>*, except:
//
// - Non generic types: rarely seen as collections (e.g. *System.String*
// implements *IEnumerable<char>* but strings are named like *FirstName*).
//
// - Dictionaries: mappings rather than collections, commonly suffixed
// *Map*, *Table* or *Dictionary* (though names like *PersonsByNames*
// often satisfy this rule anyway).
//</Desc>
//<HowToFix>
// Rename by making plural the word best describing the *items*:
//
// - **ListOfDir** can be renamed **Directories**.
//
// - **Children** can be renamed **ChildrenItems**
//
// - **QueueForCache** can be renamed **QueueOfItemsForCache**
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
DDD ubiquitous language check - Naming Conventions
Identifiers in the core domain should use the Domain Model language — the ubiquitous language of Domain Driven Design (DDD): naming rigor, since software doesn't cope well with ambiguity.
Disabled per default: customize the core domain namespace name and the domain terms list. Mention singular and plural forms both if needed (e.g. Seat and Seats); see also the related rule Properties and fields that represent a collection of items should be named Items.
Relies on the NDepend API extension method ExtensionMethodsString.GetWords(this string identifier) that smartly extracts terms from identifiers.
How to Fix:
The rule lists the words not in vocabulary per violation. Either rename the code element or add the missing term(s) to the vocabulary list in the rule source.
// <Name>DDD ubiquitous language check</Name>
// <Id>ND2018:DDDUbiquitousLanguageCheck</Id>
warnif count > 0
// Update to your core domain namespace(s)
let coreDomainNamespaces = Application.Namespaces.WithNameLike("TrainTrain.Domain")
// Update your vocabulary list
let vocabulary = new [] {
"Train", "Coach", "Coaches", "Seat", "Seats",
"Reservation", "Fulfilled", "Booking", "Book", "Reserve", "Confirm"
}.ToHashSetEx()
let technicalWords = new [] { "get", "set", "Get", "Set", "Add" }.ToHashSetEx()
let multiWordsVocabulary = vocabulary.Where(w => w.GetWords().Length >= 2)
// Append multi-words words in vocabulary
let multiWordsWords = multiWordsVocabulary.SelectMany(w => w.GetWords())
let vocabulary2 = vocabulary.Concat(multiWordsWords).ToHashSetEx()
let vocabulary3 = vocabulary2.Concat(technicalWords).ToHashSetEx()
from ce in coreDomainNamespaces.ChildTypesAndMembers()
let tokens = ce.SimpleName.GetWords().Select(w => w.FirstCharToUpper()).ToArray()
where !(ce.IsMethod && ce.AsMethod.IsConstructor) && // No vocabulary in ctor
!(ce.IsField && ce.AsField.IsGeneratedByCompiler) && // Remove compiler generated backing fields, their vocabulary is in property
!(vocabulary3.Any(vocable => tokens.Contains(vocable)))
let wordsNotInVocabulary = tokens.Where(w => !(vocabulary2.Contains(w) && string.IsNullOrEmpty(w))).ToArray()
select new { ce,
wordsNotInVocabulary = string.Join(", ",wordsNotInVocabulary)
}
//<Expl>
// The *{0}'s name* contains words like *{1}* which are not part
// of the domain vocabulary defined in this rule source code.
//</Expl>
//<Desc>
// Identifiers in the **core domain** should use the **Domain Model**
// language — the **ubiquitous language** of **Domain Driven Design (DDD)**:
// naming rigor, since software doesn't cope well with ambiguity.
//
// Disabled per default: customize the **core domain** namespace name and
// the domain terms list. Mention singular and plural forms both if needed
// (e.g. **Seat** and **Seats**); see also the related rule *Properties and
// fields that represent a collection of items should be named Items*.
//
// Relies on the NDepend API extension method
// **ExtensionMethodsString.GetWords(this string identifier)** that smartly
// extracts terms from identifiers.
//</Desc>
//<HowToFix>
// The rule lists the **words not in vocabulary** per violation. Either
// rename the code element or add the missing term(s) to the vocabulary list
// in the rule source.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid fields with same name in class hierarchy - Naming Conventions
Matches a field whose name is already used in the base class hierarchy. Compilers allow it (even when the base field is visible to derived classes), but it is likely an erroneous attempt to redefine a field.
How to Fix:
If the derived field is indeed a redefinition (same field type; for statics, a single instance suffices), delete it and make the base field protected. Otherwise rename the derived field, carefully renaming all its usages (they might relate to the base field).
// <Name>Avoid fields with same name in class hierarchy</Name>
// <Id>ND2019:AvoidFieldsWithSameNameInClassHierarchy</Id>
warnif count > 0
// Optimization: only consider fields of derived classes
let derivedClasses = Application.Types.Where(t =>
t.IsClass &&
!t.IsStatic &&
t.DepthOfInheritance > 1 &&
t.BaseClasses.ChildFields().Any())
from f in derivedClasses.ChildFields()
where JustMyCode.Contains(f) // Make sure it is fixable by targeting only JustMyCode fields
let fieldsOfBaseClassesWithSameName =
f.ParentType
.BaseClasses
.ChildFields()
.Where(cf => cf.Name == f.Name && JustMyCode.Contains(cf))
where fieldsOfBaseClassesWithSameName.Any()
select new {
f,
fieldsOfBaseClassesWithSameName,
baseClasses = fieldsOfBaseClassesWithSameName.Select(f1 => f1.ParentType).ToArray(),
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// There {2?is:are} {2?a:} field{2?:s} with same name as *{0}* in the base class{2?:es}: *{2}*.
// Investigate this ambiguous situation. It might reveal an incorrect attempt to redeclare a field in a derived class.
//</Expl>
//<Desc>
// Matches a field whose name is already used in the base class hierarchy.
// Compilers allow it (even when the base field is visible to derived
// classes), but it is likely an erroneous attempt to redefine a field.
//</Desc>
//<HowToFix>
// If the derived field is indeed a redefinition (same field type; for
// statics, a single instance suffices), delete it and make the base field
// *protected*. Otherwise rename the derived field, carefully renaming all
// its usages (they might relate to the base field).
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid various capitalizations for method name - Naming Conventions
Matches methods whose names differ only in capitalization (like ProcessOrderId vs ProcessOrderID). Choose a single capitalization application-wide for readability, maintainability and consistency.
Severity goes from Medium to Critical when various capitalizations occur within the same type: not just convention, it is error-prone — clients must investigate which version to call.
How to Fix:
Choose a single application-wide capitalization, or use method names that don't differ only by capitalization.
Debt is proportional to the number of capitalizations found (2 minimum).
// <Name>Avoid various capitalizations for method name</Name>
// <Id>ND2020:AvoidVariousCapitalizationsForMethodName</Id>
warnif count > 0
let lookup = JustMyCode.Methods.ToLookup(m => m.SimpleName, StringComparer.OrdinalIgnoreCase)
from grouping in lookup
where grouping.Count() > 1
let name = grouping.First().SimpleName
let method = grouping.FirstOrDefault(m => m.SimpleName != name)
where method != null
let nbCapitalizations = grouping.ToLookup(m => m.SimpleName).Count
// Increase the severity if various capitalizations are found in same class
let differentCapitalizationInSameType =
grouping.ToLookup(m => m.ParentType)
.Any(groupingPerType => groupingPerType.ToLookup(m => m.SimpleName).Count > 1)
select new {
method,
methods = grouping.Distinct(m => m.SimpleName).ToArray(),
nbCapitalizations,
Debt = (nbCapitalizations * 6).ToMinutes().ToDebt(),
Severity = differentCapitalizationInSameType ? Severity.Critical : Severity.Medium
}
//<Expl>
// Several capitalizations for this method name are used:
// *{1}*. Choose a single capitalization to improve
// overall readability, maintainability, team collaboration
// and consistency.
//</Expl>
//<Desc>
// Matches methods whose names differ only in capitalization (like
// *ProcessOrderId* vs *ProcessOrderID*). Choose a single capitalization
// application-wide for readability, maintainability and consistency.
//
// Severity goes from *Medium* to *Critical* when various capitalizations
// occur within the same type: not just convention, it is error-prone —
// clients must investigate which version to call.
//</Desc>
//<HowToFix>
// Choose a single application-wide capitalization, or use method names that
// don't differ only by capitalization.
//
// Debt is proportional to the number of capitalizations found (2 minimum).
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Controller class name should be suffixed with 'Controller' - Naming Conventions
Warns about
ASP.NET Controller classes not suffixed with
Controller, a recommended practice:
https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/actions ControllerBase is also accepted for base classes.
How to Fix:Suffix the names of matched Controller classes with
Controller.
// <Name>Controller class name should be suffixed with 'Controller'</Name>
// <Id>ND2021:ControllerClassNameShouldBeSuffixedController </Id>
warnif count > 0 from t in Application.Types where
(t.DeriveFrom("Microsoft.AspNetCore.Mvc.Controller".AllowNoMatch()) ||
t.DeriveFrom("Microsoft.AspNetCore.Mvc.ControllerBase".AllowNoMatch()) ||
t.HasAttribute("Microsoft.AspNetCore.Mvc.ControllerAttribute".AllowNoMatch()))
&&
// We use SimpleName, because in case of generic Exception type
// SimpleName suppresses the generic suffix (like <T>).
!t.SimpleNameLike(@"Controller$") &&
!t.SimpleNameLike(@"ControllerBase$") // Allow the second suffix Base
// for base Controller classes.
select new {
t,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *controller {0}'s name* should be suffixed with *Controller*.
// This is required in the *Microsoft* documentation.
//</Expl>
//<Desc>
// Warns about *ASP.NET Controller classes* not suffixed with
// **Controller**, a recommended practice:
// https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/actions
// **ControllerBase** is also accepted for base classes.
//</Desc>
//<HowToFix>
// Suffix the names of matched Controller classes with **Controller**.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Nested class members should not mask outer class' static members - Naming Conventions
The C# compiler lets nested class members share names with static members of the outer class, but this is discouraged: it can be unclear which member the inner class code references.
How to Fix:
Give inner class members unique and descriptive names.
// <Name>Nested class members should not mask outer class' static members</Name>
// <Id>ND2022:NestedClassMembersShouldNotMaskOutClassStaticMembers</Id>
warnif count > 0
from t in JustMyCode.Types
where t.IsNested && t.ParentType != null
let parentTypesMemberNames = t.ParentType.Members
.Where(pm => pm.IsStatic)
.Select(pm => pm.Name).ToHashSetEx()
from m in t.Members
// Don't warn for these methods and backing fields
where !(m.IsMethod &&
(m.AsMethod.IsConstructor || m.AsMethod.IsClassConstructor ||
m.AsMethod.IsPropertyGetter|| m.AsMethod.IsPropertySetter ||
m.Name == "Dispose()" ||
m.Name.EndsWith("GetEnumerator()"))) &&
!(m.IsField && m.Name.Contains("<"))
&& parentTypesMemberNames.Contains(m.Name)
select new {
m,
isOverlapping = t.ParentType.Members.First(pm => pm.Name == m.Name),
declaredIn = t.ParentType,
Debt = 2.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The {0} should be renamed because it masks the {1} declared
// in the outer {2}.
//</Expl>
//<Desc>
// The C# compiler lets nested class members share names with static members
// of the outer class, but this is discouraged: it can be unclear which
// member the inner class code references.
//</Desc>
//<HowToFix>
// Give inner class members unique and descriptive names.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid referencing source file out of the project directory - Source Files Organization
Matches types whose source files are outside the Visual Studio project file's directory (and sub-directories). Such files, added via > Add > Existing Items… > Add As Link, share type definitions across assemblies, duplicating types at binary level: degraded maintainability and subtle versioning bugs.
Tolerable for some types shared across executables, e.g. startup concerns like assembly resolving handlers or .NET Framework version checks.
How to Fix:
Reference from a VS project only source files under the project file location. Severity is Low: more an advice than a problem.
// <Name>Avoid referencing source file out of the project directory</Name>
// <Id>ND2100:AvoidReferencingSourceFileOutOfVisualStudioProjectDirectory</Id>
warnif count > 0
from a in Application.Assemblies
where a.VisualStudioProjectFilePath != null
let vsProjDirPath = a.VisualStudioProjectFilePath.ParentDirectoryPath
let vsProjDirPathStr = vsProjDirPath.ToString()
from t in a.ChildTypes
where JustMyCode.Contains(t) && t.SourceFileDeclAvailable
from decl in t.SourceDecls
let sourceFilePath = decl.SourceFile.FilePath
let sourceFilePathStr = sourceFilePath.ToString()
where sourceFilePathStr.IndexOf(vsProjDirPathStr, StringComparison.OrdinalIgnoreCase) != 0 &&
// Check that both absolute paths are on same volume before emitting an issue
sourceFilePath.OnSameVolumeThan(vsProjDirPath)
select new {
t,
sourceFilePath,
projectFilePath = a.VisualStudioProjectFilePath,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.Low
}
//<Expl>
// The source file *{1}* is referenced by the project file *{2}*
// but is located out of the project's directory or children directories.
//</Expl>
//<Desc>
// Matches types whose source files are outside the Visual Studio project
// file's directory (and sub-directories). Such files, added via
// *> Add > Existing Items… > Add As Link*, share type definitions across
// assemblies, duplicating types at binary level: degraded maintainability
// and subtle versioning bugs.
//
// Tolerable for some types shared across executables, e.g. startup concerns
// like assembly resolving handlers or .NET Framework version checks.
//</Desc>
//<HowToFix>
// Reference from a VS project only source files under the project file
// location. Severity is **Low**: more an advice than a problem.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid duplicating a type definition across assemblies - Source Files Organization
Warns about sharing code across assemblies via linked source files (> Add > Existing Items… > Add As Link), duplicating types at binary level: degraded maintainability and subtle versioning bugs. Each match is a duplicated type; unfold typesDefs to see the definitions.
Tolerable for some types shared across executables, e.g. startup concerns like assembly resolving handlers or .NET Framework version checks.
How to Fix:
Share types through DLLs, or rename same-full-name types that differ.
// <Name>Avoid duplicating a type definition across assemblies</Name>
// <Id>ND2101:AvoidDuplicatingATypeDefinitionAcrossAssemblies</Id>
warnif count > 0
let groups = Application.Assemblies
// Don't match types in ASP.NET Core generated assemblies
// whose name is suffixed with '.Views'
.Where(a => !a.Name.EndsWith(".views", StringComparison.OrdinalIgnoreCase))
.ChildTypes()
.Where(t => !t.IsGeneratedByCompiler &&
// Types created by the test infrastructure
t.FullName != "AutoGeneratedProgram" &&
// Allow to have multiple 'file' types in the same source file
!t.IsVisibleOnlyInFile &&
JustMyCode.Contains(t))
.GroupBy(t => t.FullName)
from @group in groups
where @group.Count() > 1
// Tricky: This rule is executed on both current snapshot and baseline snapshot (if any).
// Taking t as @group.First() could return any type in the group, and potentially
// it can return different types for current and baseline snapshot.
// As a result issues wouldn't corresponds (since types are different) and the user
// would see issues of this rule as a couple of issues added and removed.
// Ordering types by parent assembly name and then taking the first type, discards this risk.
let types = @group.OrderBy(t => t.ParentAssembly.Name).ToArray()
select new {
t = types[0],
// In the 'types' column, make sure to group matched types by parent assemblies.
typesDefs = types,
assemblies = types.Select(t => t.ParentAssembly),
tFullName = types[0].FullName,
tCount = types.Length,
Debt = (4 + 3*types.Length).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// There are {4} types with full name *{3}* declared in *{2}*.
// This leads to confusion and also naming collision.
//</Expl>
//<Desc>
// Warns about sharing code across assemblies via linked source files
// (*> Add > Existing Items… > Add As Link*), duplicating types at binary
// level: degraded maintainability and subtle versioning bugs. Each match is
// a duplicated type; unfold **typesDefs** to see the definitions.
//
// Tolerable for some types shared across executables, e.g. startup concerns
// like assembly resolving handlers or .NET Framework version checks.
//</Desc>
//<HowToFix>
// Share types through DLLs, or rename same-full-name types that differ.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid defining multiple types in a source file - Source Files Organization
Multiple types in one source file decrease readability (developers expect one type per file in the Solution Explorer) and lead to files with too many lines. Each match is such a source file, indexed by one of its types (preferably the one named like the file).
Nested types, enumerations and file-visibility types are exempt: it is acceptable to declare them alongside their principal user type.
How to Fix:
Create a source file per type. Severity is Low: more an advice than a problem.
// <Name>Avoid defining multiple types in a source file</Name>
// <Id>ND2102:AvoidDefiningMultipleTypesInASourceFile</Id>
warnif count > 0
// Build a lookup indexed by source files, values being a sequence of types defined in the source file.
let lookup = JustMyCode.Types
// When a source file is referenced by several assemblies,
// type(s) contained in the source file are seen as distinct types, both by the CLR and by NDepend.
// This Distinct clause based on type full-name (type name prefixed with namespace)
// avoids matching the multiple versions of such type.
.Distinct(t => t.FullName)
.Where(t => t.SourceFileDeclAvailable &&
// except enumerations, nested types and types generated by compilers!
!t.IsEnumeration &&
!t.IsNested &&
!t.IsGeneratedByCompiler &&
// Allow to have multiple 'file' types in the same source file
!t.IsVisibleOnlyInFile)
// We use multi-key, since a type can be declared in multiple source files.
.ToMultiKeyLookup(t => t.SourceDecls.Select(d => d.SourceFile))
from @group in lookup where @group.Count() > 1
let sourceFile = @group.Key
// CQLinq doesn't let indexing result with sourceFile
// so we choose a typeIndex in types,
// preferably the type that has the file name.
let typeWithSourceFileName = @group.FirstOrDefault(t => t.SimpleNameEquals(sourceFile.FileNameWithoutExtension))
let typeIndex = typeWithSourceFileName ?? @group.First()
// Don't warn for types with at least one declaration in a generated file
// like the UWP generated file 'App.g.i.cs'
where sourceFile.FilePathString.IndexOf(".g.", StringComparison.OrdinalIgnoreCase) == -1
select new {
typeIndex,
TypesInSourceFile = @group as IEnumerable<IType>,
SourceFilePathString = sourceFile.FilePathString,
SourceFileNameString = sourceFile.FileName,
Debt = 3.ToMinutes().ToDebt(),
Severity = Severity.Low
}
//<Expl>The source file *{3}* should contain only one type declaration instead of *{1}*.</Expl>
//<Desc>
// Multiple types in one source file decrease readability (developers expect
// one type per file in the *Solution Explorer*) and lead to files with too
// many lines. Each match is such a source file, indexed by one of its types
// (preferably the one named like the file).
//
// Nested types, enumerations and *file*-visibility types are exempt: it is
// acceptable to declare them alongside their principal user type.
//</Desc>
//<HowToFix>
// Create a source file per type. Severity is **Low**: more an advice than a
// problem.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Namespace name should correspond to file location - Source Files Organization
The namespaces hierarchy should be synchronized with the source files directory hierarchy — a widely accepted convention; misalignment makes code less maintainable and harder to navigate. Matches namespaces whose source file locations don't align with the namespace name.
Comparisons are case-sensitive; the justACaseSensitiveIssue flag indicates a pure casing mismatch (slightly lower debt).
Source files out of the project directory are ignored: such linked files are shared across projects and already matched by the rule Avoid referencing source file out of the project directory.
How to Fix:
Align the namespace with the directory sub-paths containing the source files.
// <Name>Namespace name should correspond to file location</Name>
// <Id>ND2103:NamespaceNameShouldCorrespondToFileLocation</Id>
warnif count > 0
from a in Application.Assemblies
let assemblyName = a.Name
// Source files out of the project directory are linked files shared across
// projects: they follow no single project's folder layout (ND2100 matches them)
let projectDir = a.VisualStudioProjectFilePath != null ? a.VisualStudioProjectFilePath.ParentDirectoryPath : null
from n in a.ChildNamespaces
where n.SourceFileDeclAvailable
let namespaceName = n.Name
// Build the dirShouldContain string
// and then we check which source file path contains dirShouldContain or not
let dirShouldContain = (
// namespaceName starts with assembly name => gets only components after the assembly name
namespaceName.StartsWith(assemblyName)
? namespaceName.Substring(assemblyName.Length)
// namespaceName contains assembly name => gets only components after the assembly name
: namespaceName.Contains("." + assemblyName)
? namespaceName.Substring(n.Name.IndexOf("." + assemblyName))
// namespaceName has more than one part => gets only components after the first part
: namespaceName.Contains(".")
? namespaceName.Substring(n.Name.IndexOf("."))
: namespaceName)
// Replace dots and underscore by spaces in namespace name
.Replace('.', ' ')
.Replace('_', ' ')
where dirShouldContain.Length > 0
// Obtain a sourceDecl which path doesn't contains dirShouldContain
let sourceDecl = n.SourceDecls
.OrderBy(d => d.SourceFile.FilePathString)
.Where(d => d.SourceFile.CodeElements.All(e => JustMyCode.Contains(e)) &&
(projectDir == null || d.SourceFile.FilePath.IsChildOf(projectDir)))
.FirstOrDefault(decl=>
// Replace dots, path separators, minus and underscores
// by spaces in source files names
!decl.SourceFile.FilePathString
.Replace('.',' ').Replace('\\',' ')
.Replace('-',' ').Replace('_',' ')
.Contains(dirShouldContain)
)
where sourceDecl != null
let dir = sourceDecl.SourceFile.FilePath.ParentDirectoryPath
let justACaseSensitiveIssue = dir.ToString()
.Replace('.',' ').Replace('\\',' ')
.Replace('-',' ').Replace('_',' ')
.IndexOf(dirShouldContain, StringComparison.OrdinalIgnoreCase) >= 0
// Get distinct source files contained in dir through the types thay contain
let typesConcerned = n.ChildTypes
.Where(t => t.SourceFileDeclAvailable)
.Distinct(t => t.SourceDecls.First().SourceFile.FilePathString)
.Where(t => t.SourceDecls.First().SourceFile.FilePath.ParentDirectoryPath.Equals(dir))
.ToArray()
select new {
n,
dirShouldContain,
dir = dir.ToString(),
justACaseSensitiveIssueStr = justACaseSensitiveIssue ? " Please note that this is solely a matter of casing." : "",
typesConcerned,
Debt = (justACaseSensitiveIssue ? 2 : 3).ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The source file{4?:s} containing the declaration{4?:s}
// of the *{0}* {4?is:are} located in the directory
// *{2}* that does not align with the namespace name.{3}
//</Expl>
//<Desc>
// The namespaces hierarchy should be synchronized with the source files
// directory hierarchy — a widely accepted convention; misalignment makes
// code less maintainable and harder to navigate. Matches namespaces whose
// source file locations don't align with the namespace name.
//
// Comparisons are *case-sensitive*; the *justACaseSensitiveIssue* flag
// indicates a pure casing mismatch (slightly lower debt).
//
// Source files out of the project directory are ignored: such linked files
// are shared across projects and already matched by the rule *Avoid
// referencing source file out of the project directory*.
//</Desc>
//<HowToFix>
// Align the namespace with the directory sub-paths containing the source
// files.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Types with source files stored in the same directory, should be declared in the same namespace - Source Files Organization
The namespaces hierarchy should be synchronized with the source files directory hierarchy — a widely accepted convention; misalignment makes code less maintainable and harder to navigate. Hence types with source files in the same directory should share the same namespace.
For each directory where most types are in one namespace (the main namespace) and a few are not, this rule matches the main namespace, typesOutOfMainNamespace (declared in the directory but outside it), and typesInMainNamespace (informational).
Source files out of the project directory are ignored: such linked files are shared across projects and already matched by the rule Avoid referencing source file out of the project directory.
How to Fix:
Violations are the types in typesOutOfMainNamespace. Typically such type…
- … is in the wrong namespace, right directory: move it to the main namespace.
- … is in the right namespace, wrong directory: move its source file to the proper namespace directory.
- … is declared in source files in different directories: prefer a single directory.
Estimated Debt: 2 minutes plus 5 minutes per type in typesOutOfMainNamespace.
// <Name>Types with source files stored in the same directory, should be declared in the same namespace</Name>
// <Id>ND2104:TypesWithSourceFilesStoredInTheSameDirectoryShouldBeDeclaredInTheSameNamespace</Id>
warnif count > 0
// Group JustMyCode types in a lookup
// where groups are keyed with directories that contain the types' source file(s).
// Note that a type can be contained in several groups
// if it is declared in several source files stored in different directories.
let lookup = JustMyCode.Types.Where(t => t.SourceFileDeclAvailable)
.ToMultiKeyLookup(
t => t.SourceDecls
// Source files out of the project directory are linked files shared across
// projects: they follow no single project's folder layout (ND2100 matches them)
.Where(decl => t.ParentAssembly.VisualStudioProjectFilePath == null ||
decl.SourceFile.FilePath.IsChildOf(t.ParentAssembly.VisualStudioProjectFilePath.ParentDirectoryPath))
.Select(decl => decl.SourceFile.FilePath.ParentDirectoryPath).Distinct()
)
from groupOfTypes in lookup
let parentNamespaces = groupOfTypes.ParentNamespaces()
// Select group of types (with source files stored in the same directory) …
// … but contained in several namespaces
where parentNamespaces.Count() > 1
// mainNamespaces is the namespace that contains many types
// declared in the directory groupOfTypes .key
let mainNamespace = groupOfTypes
.ToLookup(t => t.ParentNamespace)
.OrderByDescending(g => g.Count()).First().Key
// Select types with source files stored in the same directory,
// but contained in namespaces different than mainNamespace.
let typesOutOfMainNamespace = groupOfTypes
.Where(t => t.ParentNamespace != mainNamespace &&
t.ParentAssembly == mainNamespace.ParentAssembly)
// Filter types declared on several source files that contain generated methods
// because typically such type contains one or several partial definitions generated.
// These partially generated types would be false positive for the present rule.
.Where(t => t.SourceDecls.Count() == 1 ||
t.Methods.Count(m => JustMyCode.Contains(m)) == 0)
where typesOutOfMainNamespace.Count() > 0
let typesInMainNamespace = groupOfTypes.Where(t => t.ParentNamespace == mainNamespace)
select new {
mainNamespace,
typesOutOfMainNamespace,
typesInMainNamespace,
Debt = (2+5*typesOutOfMainNamespace.Count()).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The source file{2?:s} that contain the declaration{2?:s} of the *{0}*
// {2?is:are} located in a directory that also contains the *{1}* declared
// in {1?a:} different namespace{1?:s}.
// {1?Concerning this type:For each of these types}, either change
// its namespace or its source file's directory.
//</Expl>
//<Desc>
// The namespaces hierarchy should be synchronized with the source files
// directory hierarchy — a widely accepted convention; misalignment makes
// code less maintainable and harder to navigate. Hence types with source
// files in the same directory should share the same namespace.
//
// For each directory where most types are in one namespace (the **main
// namespace**) and a few are not, this rule matches the *main namespace*,
// **typesOutOfMainNamespace** (declared in the directory but outside it),
// and *typesInMainNamespace* (informational).
//
// Source files out of the project directory are ignored: such linked files
// are shared across projects and already matched by the rule *Avoid
// referencing source file out of the project directory*.
//</Desc>
//<HowToFix>
// Violations are the types in *typesOutOfMainNamespace*. Typically such
// type…
//
// - … is in the wrong namespace, right directory: move it to the *main
// namespace*.
//
// - … is in the right namespace, wrong directory: move its source file to
// the proper namespace directory.
//
// - … is declared in source files in different directories: prefer a
// single directory.
//
// Estimated Debt: 2 minutes plus 5 minutes per type in
// *typesOutOfMainNamespace*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Types declared in the same namespace, should have their source files stored in the same directory - Source Files Organization
The namespaces hierarchy should be synchronized with the source files directory hierarchy — a widely accepted convention; misalignment makes code less maintainable and harder to navigate. Hence types in the same namespace should have their source files in the same directory.
For each namespace with source files in several directories, the main directory is inferred (preferably the one named like the namespace). The rule matches the namespace, typesDeclaredOutOfMainDir (source files outside the main directory), the main directory, and typesDeclaredInMainDir (informational).
Source files out of the project directory are ignored: such linked files are shared across projects and already matched by the rule Avoid referencing source file out of the project directory.
How to Fix:
Violations are the types in typesDeclaredOutOfMainDir. Typically such type…
- … is in the wrong namespace, right directory: move it to the namespace corresponding to the parent directory.
- … is in the right namespace, wrong directory: move its source file to the main directory.
- … is declared in source files in different directories: prefer a single directory.
Estimated Debt: 2 minutes plus 5 minutes per type in typesDeclaredOutOfMainDir.
// <Name>Types declared in the same namespace, should have their source files stored in the same directory</Name>
// <Id>ND2105:TypesDeclaredInTheSameNamespaceShouldHaveTheirSourceFilesStoredInTheSameDirectory</Id>
warnif count > 0
from @namespace in Application.Namespaces
// Group types of @namespace in a lookup
// where groups are keyed with directories that contain the types' source file(s).
// Note that a type can be contained in several groups
// if it is declared in several source files stored in different directories.
let lookup = @namespace.ChildTypes.Where(
t => t.SourceFileDeclAvailable &&
JustMyCode.Contains(t) &&
// Don't match ASP.NET application types declared in Global.asax file
// that typically are in the root directory of the VS project.
!string.Equals(
t.SourceDecls.First().SourceFile.FileName,
"global.asax",
StringComparison.OrdinalIgnoreCase))
.ToMultiKeyLookup(
t => t.SourceDecls
// Source files out of the project directory are linked files shared across
// projects: they follow no single project's folder layout (ND2100 matches them)
.Where(decl => t.ParentAssembly.VisualStudioProjectFilePath == null ||
decl.SourceFile.FilePath.IsChildOf(t.ParentAssembly.VisualStudioProjectFilePath.ParentDirectoryPath))
.Select(decl => decl.SourceFile.FilePath.ParentDirectoryPath).Distinct()
)
// Are types of @namespaces declared in more than one directory?
where lookup.Count > 1
// Infer the main folder, preferably the one that has the same name as the namespace.
let dirs = lookup.Select(types => types.Key)
let mainDirNullable = dirs.Where(d => d.DirectoryName == @namespace.SimpleName).FirstOrDefault()
let mainDir = mainDirNullable ?? dirs.First()
// Types declared out of mainDir, are types in group of types declared in a directory different than mainDir!
let typesDeclaredOutOfMainDir =
lookup.Where(types => types.Key != mainDir)
.SelectMany(types => types)
// Filter types declared on several source files that contain generated methods
// because typically such type contains one or several partial definitions generated.
// These partially generated types would be false positive for the present rule.
.Where(t => t.SourceDecls.Count() == 1 ||
t.Methods.Count(m => JustMyCode.Contains(m)) == 0)
where typesDeclaredOutOfMainDir.Count() > 0 &&
// Don't warn for types with at least one declaration in a generated file
// like the UWP generated file 'App.g.i.cs'
!typesDeclaredOutOfMainDir
.Where(t => t.SourceFileDeclAvailable)
.SelectMany(t => t.SourceDecls.Select(sd => sd.SourceFile.FilePathString))
.Any(fps => fps.IndexOf(".g.", StringComparison.OrdinalIgnoreCase) >= 0)
let typesDeclaredInMainDir =
lookup.Where(types => types.Key == mainDir)
.SelectMany(types => types)
select new {
@namespace,
typesDeclaredOutOfMainDir,
mainDir = mainDir.ToString(),
typesDeclaredInMainDir,
Debt = (2+5*typesDeclaredOutOfMainDir.Count()).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* declarations are present in source files spread across multiple directories.
// *{1}* {1?is:are} declared in source files out of *{2}*.
//</Expl>
//<Desc>
// The namespaces hierarchy should be synchronized with the source files
// directory hierarchy — a widely accepted convention; misalignment makes
// code less maintainable and harder to navigate. Hence types in the same
// namespace should have their source files in the same directory.
//
// For each namespace with source files in several directories, the **main
// directory** is inferred (preferably the one named like the namespace).
// The rule matches the namespace, **typesDeclaredOutOfMainDir** (source
// files outside the *main directory*), the *main directory*, and
// *typesDeclaredInMainDir* (informational).
//
// Source files out of the project directory are ignored: such linked files
// are shared across projects and already matched by the rule *Avoid
// referencing source file out of the project directory*.
//</Desc>
//<HowToFix>
// Violations are the types in **typesDeclaredOutOfMainDir**. Typically such
// type…
//
// - … is in the wrong namespace, right directory: move it to the namespace
// corresponding to the parent directory.
//
// - … is in the right namespace, wrong directory: move its source file to
// the *main directory*.
//
// - … is declared in source files in different directories: prefer a
// single directory.
//
// Estimated Debt: 2 minutes plus 5 minutes per type in
// *typesDeclaredOutOfMainDir*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Mark ISerializable types with SerializableAttribute - System
The CLR recognizes types as serializable only when marked with SerializableAttribute, even with custom serialization via ISerializable. Matches ISerializable types not tagged with it.
How to Fix:
Tag the matched type with SerializableAttribute.
// <Name>Mark ISerializable types with SerializableAttribute</Name>
// <Id>ND2200:MarkISerializableTypesWithSerializableAttribute</Id>
warnif count > 0
from t in Application.Types where
t.IsPublic &&
!t.IsDelegate &&
!t.IsExceptionClass && // Don't match exceptions, since the Exception class
// implements ISerializable, this would generate
// too many false positives.
t.Implement ("System.Runtime.Serialization.ISerializable".AllowNoMatch()) &&
!t.HasAttribute ("System.SerializableAttribute".AllowNoMatch())
select new {
t,
t.NbLinesOfCode,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* should be tagged with *SerializableAttribute*
// to be recognized by the runtime as serializable.
//</Expl>
//<Desc>
// The CLR recognizes types as serializable only when marked with
// *SerializableAttribute*, even with custom serialization via
// *ISerializable*. Matches *ISerializable* types not tagged with it.
//</Desc>
//<HowToFix>
// Tag the matched type with *SerializableAttribute*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Mark assemblies with CLSCompliant (deprecated) - System
Deprecated, disabled by default; re-enable if it fits your environment.
The Common Language Specification (CLS) defines rules assemblies must follow for cross-language use. Assemblies should explicitly indicate CLS compliance with CLSCompliantAttribute (absent = not compliant); a compliant assembly may still contain non-compliant members. Matches assemblies not tagged with System.CLSCompliantAttribute.
How to Fix:
Tag the assembly with CLSCompliantAttribute. Rather than marking the whole assembly non-compliant, mark the specific non-compliant members, ideally providing CLS-compliant alternatives for the widest audience.
// <Name>Mark assemblies with CLSCompliant (deprecated)</Name>
// <Id>ND2201:MarkAssembliesWithCLSCompliant</Id>
warnif count > 0 from a in Application.Assemblies where
!a.HasAttribute ("System.CLSCompliantAttribute".AllowNoMatch())
select new {
a,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* should be tagged with *CLSCompliantAttribute*.
//</Expl>
//<Desc>
// Deprecated, disabled by default; re-enable if it fits your environment.
//
// The *Common Language Specification* (CLS) defines rules assemblies must
// follow for cross-language use. Assemblies should explicitly indicate CLS
// compliance with **CLSCompliantAttribute** (absent = not compliant); a
// compliant assembly may still contain non-compliant members. Matches
// assemblies not tagged with **System.CLSCompliantAttribute**.
//</Desc>
//<HowToFix>
// Tag the assembly with *CLSCompliantAttribute*. Rather than marking the
// whole assembly non-compliant, mark the specific non-compliant members,
// ideally providing CLS-compliant alternatives for the widest audience.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Mark assemblies with ComVisible (deprecated) - System
Deprecated, disabled by default; re-enable if it fits your environment.
ComVisibleAttribute determines how COM clients access managed code; assemblies should indicate COM visibility explicitly (absent = visible to COM clients), overridable per type and member. Matches assemblies not tagged with System.Runtime.InteropServices.ComVisibleAttribute.
How to Fix:
Tag the assembly with ComVisibleAttribute, value false if it shouldn't be visible to COM clients.
// <Name>Mark assemblies with ComVisible (deprecated)</Name>
// <Id>ND2202:MarkAssembliesWithComVisible</Id>
warnif count > 0 from a in Application.Assemblies where
!a.HasAttribute ("System.Runtime.InteropServices.ComVisibleAttribute".AllowNoMatch())
select new {
a,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* should be tagged with *CLSCompliantAttribute*.
//</Expl>
//<Desc>
// Deprecated, disabled by default; re-enable if it fits your environment.
//
// **ComVisibleAttribute** determines how COM clients access managed code;
// assemblies should indicate COM visibility explicitly (absent = visible to
// COM clients), overridable per type and member. Matches assemblies not
// tagged with **System.Runtime.InteropServices.ComVisibleAttribute**.
//</Desc>
//<HowToFix>
// Tag the assembly with *ComVisibleAttribute*, value **false** if it
// shouldn't be visible to COM clients.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Mark attributes with AttributeUsageAttribute - System
Mark custom attributes with AttributeUsageAttribute to indicate where they can be applied, per their meaning (e.g. a type-maintainer attribute belongs on classes, enumerations and interfaces, not methods or properties). Valid targets come from the System.AttributeTargets enumeration; omitting the attribute means All targets.
Not matched: abstract attribute classes (usage can be deferred to derived classes) and classes whose base is tagged (usage is inherited).
How to Fix:
Specify targets via AttributeUsageAttribute with the proper AttributeTargets values.
// <Name>Mark attributes with AttributeUsageAttribute</Name>
// <Id>ND2203:MarkAttributesWithAttributeUsageAttribute</Id>
warnif count > 0
from t in JustMyCode.Types where
t.DeriveFrom ("System.Attribute".AllowNoMatch())
&& !t.HasAttribute ("System.AttributeUsageAttribute".AllowNoMatch())
// AttributeUsageAttribute can be deferred to classes that derive from an abstract attribute class
&& !t.IsAbstract
// AttributeUsageAttribute can de inherited from a base attribute class (that is not System.Attribute)
&& !t.BaseClasses.Any(c =>
c.FullName != "System.Attribute" &&
c.HasAttribute ("System.AttributeUsageAttribute".AllowNoMatch()))
select new {
t,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *attribute {0}* should be annotated with *AttributeUsageAttribute*
// to specify where in the source code the custom attribute can be applied.
//</Expl>
//<Desc>
// Mark custom attributes with **AttributeUsageAttribute** to indicate where
// they can be applied, per their meaning (e.g. a type-maintainer attribute
// belongs on classes, enumerations and interfaces, not methods or
// properties). Valid targets come from the **System.AttributeTargets**
// enumeration; omitting the attribute means **All** targets.
//
// Not matched: abstract attribute classes (usage can be deferred to derived
// classes) and classes whose base is tagged (usage is inherited).
//</Desc>
//<HowToFix>
// Specify targets via *AttributeUsageAttribute* with the proper
// *AttributeTargets* values.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Remove calls to GC.Collect() - System
Calling
GC.Collect() directly can lead to performance issues. More:
http://blogs.msdn.com/ricom/archive/2004/11/29/271829.aspx Matches methods calling any
GC.Collect() overload.
How to Fix:Remove matched calls to
GC.Collect().
// <Name>Remove calls to GC.Collect()</Name>
// <Id>ND2204:RemoveCallsToGCCollect</Id>
warnif count > 0
let gcCollectMethods = ThirdParty.Methods.WithFullNameWildcardMatch(
"System.GC.Collect(*)").ToHashSetEx()
from m in Application.Methods.UsingAny(gcCollectMethods)
select new {
m,
gcCollectMethodCalled = m.MethodsCalled.Intersect(gcCollectMethods),
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* shouldn't call *GC.Collect()*
// because doing so can lead to performance issues.
//</Expl>
//<Desc>
// Calling **GC.Collect()** directly can lead to performance issues. More:
// http://blogs.msdn.com/ricom/archive/2004/11/29/271829.aspx
// Matches methods calling any *GC.Collect()* overload.
//</Desc>
//<HowToFix>
// Remove matched calls to *GC.Collect()*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't call GC.Collect() without calling GC.WaitForPendingFinalizers() - System
Avoid direct GC.Collect() calls (see Remove calls to GC.Collect()). If you call it anyway, do:
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
so finalizers execute and finalizable objects get cleaned properly. Matches methods calling GC.Collect() without GC.WaitForPendingFinalizers().
How to Fix:
If GC.Collect() is really needed, call GC.WaitForPendingFinalizers() properly.
// <Name>Don't call GC.Collect() without calling GC.WaitForPendingFinalizers()</Name>
// <Id>ND2205:DontCallGCCollectWithoutCallingGCWaitForPendingFinalizers</Id>
warnif count > 0
let gcCollectMethods = ThirdParty.Methods.WithFullNameWildcardMatch(
"System.GC.Collect(*)").ToHashSetEx()
from m in Application.Methods.UsingAny(gcCollectMethods) where
!m.IsUsing ("System.GC.WaitForPendingFinalizers()".AllowNoMatch())
select new {
m,
gcCollectMethodCalled = m.MethodsCalled.Intersect(gcCollectMethods),
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* calls *GC.Collect()*. If you really want
// to maintain this call, it should also call *GC.WaitForPendingFinalizers()*.
//</Expl>
//<Desc>
// Avoid direct **GC.Collect()** calls (see *Remove calls to GC.Collect()*).
// If you call it anyway, do:
//
// GC.Collect();
//
// GC.WaitForPendingFinalizers();
//
// GC.Collect();
//
// so finalizers execute and finalizable objects get cleaned properly.
// Matches methods calling *GC.Collect()* without
// *GC.WaitForPendingFinalizers()*.
//</Desc>
//<HowToFix>
// If *GC.Collect()* is really needed, call
// *GC.WaitForPendingFinalizers()* properly.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Enum Storage should be Int32 - System
Enumerations store constants as System.Int32 by default. Changing the underlying type is rarely necessary or recommended: smaller types bring no significant performance improvement. If needed, use CLS-compliant integral types (Byte, Int16, Int32, Int64). Matches enumerations whose underlying type is not System.Int32.
How to Fix:
Use Int32 unless size or compatibility issues exist; Int64 if values don't fit, Byte or Int16 if backward compatibility requires it.
// <Name>Enum Storage should be Int32</Name>
// <Id>ND2206:EnumStorageShouldBeInt32</Id>
warnif count > 0 from f in JustMyCode.Fields where
f.ParentType.IsEnumeration &&
f.Name == @"value__" &&
f.FieldType != null &&
f.SizeOfInst > 0 && f.SizeOfInst < 4 &&
!f.IsThirdParty
select new {
f,
f.SizeOfInst,
f.FieldType,
Debt = 7.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* should be *Int32* and not the *{2}*.
// Using a data type smaller than *Int32* doesn't result
// in a significant performance improvement.
//</Expl>
//<Desc>
// Enumerations store constants as **System.Int32** by default. Changing the
// underlying type is rarely necessary or recommended: smaller types bring
// no significant performance improvement. If needed, use CLS-compliant
// integral types (*Byte*, *Int16*, *Int32*, *Int64*). Matches enumerations
// whose underlying type is not *System.Int32*.
//</Desc>
//<HowToFix>
// Use *Int32* unless size or compatibility issues exist; *Int64* if values
// don't fit, *Byte* or *Int16* if backward compatibility requires it.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Do not raise too general exception types - System
These exception types are too general to inform the user:
- System.Exception
- System.ApplicationException
- System.SystemException
Throwing them from a library compels clients to catch all exceptions, including unknown ones they can't handle. Matches methods instantiating such exceptions.
How to Fix:
Throw a more derived framework exception type, or your own type deriving from System.Exception.
Estimated Debt: 15 minutes per method plus 5 minutes per too general exception type instantiated.
// <Name>Do not raise too general exception types</Name>
// <Id>ND2207:DoNotRaiseTooGeneralExceptionTypes</Id>
warnif count > 0
let tooGeneralExceptionTypes = ThirdParty.Types.WithFullNameIn(
"System.Exception",
"System.ApplicationException",
"System.SystemException")
from m in JustMyCode.Methods.ThatCreateAny(tooGeneralExceptionTypes)
// Make sure we don't match constructor of exception types
// that actually instantiate System.Exception.
where !m.IsConstructor || tooGeneralExceptionTypes.All(t => !m.ParentType.DeriveFrom(t))
let exceptionsCreated = tooGeneralExceptionTypes.Where(t => m.IsUsing(t))
select new {
m,
exceptionsCreated,
Debt = (15 + 5*exceptionsCreated.Count()).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* creates an instance of the *{1}*. Throwing such exception compels client code
// to catch all exceptions, including unknown exceptions that they
// do not know how to handle.
//</Expl>
//<Desc>
// These exception types are too general to inform the user:
//
// - System.Exception
//
// - System.ApplicationException
//
// - System.SystemException
//
// Throwing them from a library compels clients to catch all exceptions,
// including unknown ones they can't handle. Matches methods instantiating
// such exceptions.
//</Desc>
//<HowToFix>
// Throw a more derived framework exception type, or your own type deriving
// from *System.Exception*.
//
// Estimated Debt: 15 minutes per method plus 5 minutes per too general
// exception type instantiated.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Do not raise reserved exception types - System
These reserved exception types should be thrown only by the runtime:
- System.ExecutionEngineException
- System.IndexOutOfRangeException
- System.OutOfMemoryException
- System.StackOverflowException
- System.InvalidProgramException
- System.AccessViolationException
- System.CannotUnloadAppDomainException
- System.BadImageFormatException
- System.DataMisalignedException
Matches methods instantiating such reserved exceptions.
How to Fix:
Throw a specific non-reserved exception type instead.
For System.NullReferenceException, throw System.ArgumentNullException instead, or use a contract (MS Code Contracts or Debug.Assert()) meaning a null there can only be a bug. More generally, on detected corrupted state / bug consequence, a declarative assertion beats throwing: continuing is potentially harmful, the process should shut down and the failure be reported as a bug.
// <Name>Do not raise reserved exception types</Name>
// <Id>ND2208:DoNotRaiseReservedExceptionTypes</Id>
warnif count > 0
let reservedExceptions = ThirdParty.Types.WithFullNameIn(
"System.ExecutionEngineException",
"System.IndexOutOfRangeException",
"System.OutOfMemoryException",
"System.StackOverflowException",
"System.InvalidProgramException",
"System.AccessViolationException",
"System.CannotUnloadAppDomainException",
"System.BadImageFormatException",
"System.DataMisalignedException")
from m in JustMyCode.Methods.ThatCreateAny(reservedExceptions)
where !m.IsGeneratedByCompiler
let reservedExceptionsCreated = reservedExceptions.Where(t => m.IsUsing(t))
select new {
m,
reservedExceptionsCreated,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* creates an instance of the *{1}*. This is a reserved exception
// that should be thrown only by the runtime.
//</Expl>
//<Desc>
// These reserved exception types should be thrown only by the runtime:
//
// - System.ExecutionEngineException
//
// - System.IndexOutOfRangeException
//
// - System.OutOfMemoryException
//
// - System.StackOverflowException
//
// - System.InvalidProgramException
//
// - System.AccessViolationException
//
// - System.CannotUnloadAppDomainException
//
// - System.BadImageFormatException
//
// - System.DataMisalignedException
//
// Matches methods instantiating such reserved exceptions.
//</Desc>
//<HowToFix>
// Throw a specific non-reserved exception type instead.
//
// For *System.NullReferenceException*, throw
// *System.ArgumentNullException* instead, or use a contract (MS Code
// Contracts or *Debug.Assert()*) meaning a null there can only be a bug.
// More generally, on detected *corrupted state / bug consequence*, a
// declarative assertion beats throwing: continuing is potentially harmful,
// the process should shut down and the failure be reported as a bug.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Uri fields or properties should be of type System.Uri - System
Fields and properties named ending with 'Uri' or 'Url' represent a Uniform Resource Identifier or Locator and should be typed System.Uri. Matches those that are not.
How to Fix:
Rename the field/property, or type it System.Uri. Severity is Low: more an advice than a problem.
// <Name>Uri fields or properties should be of type System.Uri</Name>
// <Id>ND2209:UriFieldsShouldBeOfTypeSystemUri</Id>
warnif count > 0 from m in JustMyCode.Members where
(m.IsField &&
m.AsField.Name.EndsWithAny("Uri", "Url") &&
m.AsField.FieldType != null &&
m.AsField.FieldType.FullName != "System.Uri") ||
(m.IsProperty &&
m.AsProperty.Name.EndsWithAny("Uri", "Url") &&
m.AsProperty.PropertyType != null &&
m.AsProperty.PropertyType.FullName != "System.Uri")
select new {
m,
type = m.IsField ? m.AsField.FieldType : m.AsProperty.PropertyType,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.Low
}
//<Expl>
// The *{0}* should be typed with *System.Uri*.
//</Expl>
//<Desc>
// Fields and properties named ending with *'Uri'* or *'Url'* represent a
// *Uniform Resource Identifier or Locator* and should be typed
// **System.Uri**. Matches those that are not.
//</Desc>
//<HowToFix>
// Rename the field/property, or type it *System.Uri*. Severity is **Low**:
// more an advice than a problem.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Types should not derive from System.ApplicationException - System
Deriving exceptions from ApplicationException was recommended in .NET Framework 1; the recommendation is now to derive from System.Exception or one of its System namespace subclasses. Matches exception classes deriving from ApplicationException.
How to Fix:
Derive matched exception types from System.Exception or one of its System namespace subclasses.
// <Name>Types should not derive from System.ApplicationException</Name>
// <Id>ND2210:TypesShouldNotExtendSystemApplicationException</Id>
warnif count > 0 from t in Application.Types where
t.DeriveFrom("System.ApplicationException".AllowNoMatch())
select new {
t,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* should not derive from *System.ApplicationException*
// but from *System.Exception* instead.
//</Expl>
//<Desc>
// Deriving exceptions from *ApplicationException* was recommended in .NET
// Framework 1; the recommendation is now to derive from **System.Exception**
// or one of its *System* namespace subclasses. Matches exception classes
// deriving from *ApplicationException*.
//</Desc>
//<HowToFix>
// Derive matched exception types from **System.Exception** or one of its
// *System* namespace subclasses.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't Implement ICloneable - System
System.ICloneable is a bad API: it predates generics, so
Clone() returns an
object needing a
downcast; it doesn't say whether cloning is
deep or
shallow, a fundamental difference (
https://stackoverflow.com/a/184745/27194); and classes deriving from an
ICloneable implementer
must implement
Clone(), easy to forget.
Classes deriving from an
ICloneable implementer are not matched: the fix belongs in the base class (maybe unfixable if third-party).
How to Fix:Stop implementing this interface. Rename remaining
Clone() methods to
DeepClone() or
ShallowClone() with typed results, or define
IDeepCloneable<T> (
DeepClone():T) and
IShallowCloneable<T> (
ShallowClone():T). Custom NDepend rules can enforce that derived classes override virtual clone methods.
// <Name>Don't Implement ICloneable</Name>
// <Id>ND2211:DontImplementICloneable</Id>
warnif count > 0
from t in Application.Types
where t.Implement("System.ICloneable".AllowNoMatch())
// Don't warn for classes that derive from a class that implements ICloneable
&& !t.BaseClasses.Any(c => c.Implement("System.ICloneable".AllowNoMatch()))
select new {
t,
Debt = 1.ToHours().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* should not implement the interface *System.ICloneable*
// because it can be confusing for client code.
//</Expl>
//<Desc>
// *System.ICloneable* is a bad API: it predates generics, so *Clone()*
// returns an *object* needing a *downcast*; it doesn't say whether cloning
// is **deep** or **shallow**, a fundamental difference
// (https://stackoverflow.com/a/184745/27194); and classes deriving from an
// *ICloneable* implementer **must** implement *Clone()*, easy to forget.
//
// Classes deriving from an *ICloneable* implementer are not matched: the
// fix belongs in the base class (maybe unfixable if third-party).
//</Desc>
//<HowToFix>
// Stop implementing this interface. Rename remaining *Clone()* methods to
// *DeepClone()* or *ShallowClone()* with typed results, or define
// *IDeepCloneable<T>* (*DeepClone():T*) and *IShallowCloneable<T>*
// (*ShallowClone():T*). Custom NDepend rules can enforce that derived
// classes override virtual clone methods.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Collection properties should be read only - System.Collections
A writable collection property lets users replace the whole collection; a read-only one still allows setting individual members. If replacement is needed, prefer clear and re-populate methods (see Clear() and AddRange() on System.Collections.Generic.List<T>).
Binary and XML serialization support read-only collection properties (XmlSerializer has specific requirements for ICollection / IEnumerable types). Matches property setters assigning a collection.
How to Fix:
Make the property read-only, adding clear and re-populate methods if the design requires it.
// <Name>Collection properties should be read only</Name>
// <Id>ND2300:CollectionPropertiesShouldBeReadOnly</Id>
warnif count > 0
// First find collectionTypes
let collectionInterfaces = ThirdParty.Types.WithFullNameIn(
"System.Collections.ICollection",
"System.Collections.Generic.ICollection<T>")
let collectionTypes = Types.ThatImplementAny(collectionInterfaces)
.Union(collectionInterfaces)
.Union(Types.WithFullName("System.Array"))
.ToHashSetEx()
where collectionTypes.Count() > 0
// Find all Read/Write property that is typed with a collection type
from property in JustMyCode.Properties.Where(
p => p.IsReadWrite &&
p.PropertyType != null &&
collectionTypes.Contains(p.PropertyType))
let propSetter = property.SetMethod
where propSetter != null &&
!propSetter.IsPrivate && // Ignore private setters since this is private implementation detail
!propSetter.ParentType.IsPrivate && // Ignore setters of private types
// Ignore properties of serializable types
!propSetter.ParentType.TypesUsed.Any(t1 => t1.IsAttributeClass &&
(t1.ParentNamespace.Name == "Newtonsoft.Json" || t1.ParentNamespace.Name == "System.Text.Json")
) &&
!propSetter.ParentType.HasAttribute("System.Runtime.Serialization.DataContractAttribute".AllowNoMatch()) &&
!propSetter.ParentType.HasAttribute("System.Xml.Serialization.XmlRootAttribute".AllowNoMatch()) &&
!propSetter.ParentType.HasAttribute("System.SerializableAttribute".AllowNoMatch())
select new {
property,
CollectionType = property.PropertyType,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* should be read-only because it is typed with the collection *{1}*.
// Doing so favors *encapsulation*, *predictable behavior* and *consistency*.
//</Expl>
//<Desc>
// A writable collection property lets users replace the whole collection; a
// read-only one still allows setting individual members. If replacement is
// needed, prefer clear and re-populate methods (see *Clear()* and
// *AddRange()* on *System.Collections.Generic.List<T>*).
//
// Binary and XML serialization support read-only collection properties
// (*XmlSerializer* has specific requirements for *ICollection* /
// *IEnumerable* types). Matches property setters assigning a collection.
//</Desc>
//<HowToFix>
// Make the property read-only, adding clear and re-populate methods if the
// design requires it.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't use .NET 1.x HashTable and ArrayList - System.Collections
Deprecated, disabled by default; re-enable if it fits your environment. Warns about methods using non-generic collections: ArrayList, HashTable, Queue, Stack or SortedList.
How to Fix:
Prefer generic collections: strongly typed elements, faster with value types (no boxing):
- List<T> over ArrayList.
- Dictionary<K,V> over HashTable.
- Queue<T> over Queue.
- Stack<T> over Stack.
- SortedDictionary<K,V> or SortedList<K,V> over SortedList.
Being forced to use non-generic collections (third-party code, .NET 1.x) should question moving to newer .NET updates.
// <Name>Don't use .NET 1.x HashTable and ArrayList</Name>
// <Id>ND2301:DontUseDotNET1HashTableAndArrayList</Id>
warnif count > 0
let forbiddenTypes = ThirdParty.Types.WithFullNameIn(
"System.Collections.HashTable",
"System.Collections.ArrayList",
"System.Collections.Queue",
"System.Collections.Stack",
"System.Collections.SortedList")
where forbiddenTypes.Count() > 0
from m in Application.Methods.ThatCreateAny(forbiddenTypes)
let forbiddenTypesUsed = m.MethodsCalled.Where(m1 => m1.IsConstructor && forbiddenTypes.Contains(m1.ParentType)).ParentTypes()
select new {
m,
forbiddenTypesUsed,
Debt = 15.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* shouldn't use the *{1}* because there {1?is a:are}
// better generic alternative{1?:s} in the .NET Base Class Library.
//</Expl>
//<Desc>
// Deprecated, disabled by default; re-enable if it fits your environment.
// Warns about methods using non-generic collections: **ArrayList**,
// **HashTable**, **Queue**, **Stack** or **SortedList**.
//</Desc>
//<HowToFix>
// Prefer generic collections: strongly typed elements, faster with value
// types (no boxing):
//
// - **List<T>** over **ArrayList**.
//
// - **Dictionary<K,V>** over **HashTable**.
//
// - **Queue<T>** over **Queue**.
//
// - **Stack<T>** over **Stack**.
//
// - **SortedDictionary<K,V>** or **SortedList<K,V>** over **SortedList**.
//
// Being forced to use non-generic collections (third-party code, .NET 1.x)
// should question moving to newer .NET updates.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Caution with List.Contains() - System.Collections
Matches calls to List<T>.Contains(), an O(N) operation. For large lists or frequent calls, prefer System.Collections.Generic.HashSet<T> whose Contains() is constant time.
Not a rule: O(N) Contains() is usually not a mistake; this query just points out a potential performance pitfall.
// <Name>Caution with List.Contains()</Name>
// <Id>ND2302:CautionWithListContains</Id>
// warnif count > 0 // This query is n ot a rule per default
let containsMethods = ThirdParty.Methods.WithFullNameIn(
"System.Collections.Generic.List<T>.Contains(T)",
"System.Collections.Generic.IList<T>.Contains(T)",
"System.Collections.ArrayList.Contains(Object)")
from m in Application.Methods.UsingAny(containsMethods)
select new {
m,
methodsCalled = m.MethodsCalled.Intersect(containsMethods),
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.Low
}
//<Expl>
// The *{0}* calls the *{1}* that operate{1?s:} in *O(N)*
// which could potentially lead to performance degradation.
//</Expl>
//<Desc>
// Matches calls to *List<T>.Contains()*, an *O(N)* operation. For large
// lists or frequent calls, prefer *System.Collections.Generic.HashSet<T>*
// whose *Contains()* is constant time.
//
// Not a rule: *O(N) Contains()* is usually not a mistake; this query just
// points out a potential performance pitfall.
//</Desc>
./rules/assets/img/StatusBigOk32x32.png
Prefer return collection abstraction instead of implementation - System.Collections
Matches methods returning a collection implementation (List<T>, HashSet<T>, Dictionary<TKey,TValue>). Clients rarely need the exact implementation: prefer returning an interface like IList<T>, ICollection<T>, IEnumerable<T> or IDictionary<TKey,TValue>.
Not a rule, since the interface isn't preferable in all cases.
// <Name>Prefer return collection abstraction instead of implementation</Name>
// <Id>ND2303:PreferReturnCollectionAbstractionInsteadOfImplementation</Id>
// warnif count > 0 // This query is not a rule per default
let implTypes = ThirdParty.Types.WithFullNameIn(
"System.Collections.Generic.List<T>",
"System.Collections.Generic.HashSet<T>",
"System.Collections.Generic.Dictionary<TKey,TValue>")
from m in Application.Methods.WithReturnTypeIn(implTypes)
select new {
m,
m.ReturnType,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.Low
}
//<Expl>
// The *{0}* should return a collection abstraction like *IList<T>*
// instead of returning the concrete *{1}*.
//</Expl>
//<Desc>
// Matches methods returning a collection implementation (*List<T>*,
// *HashSet<T>*, *Dictionary<TKey,TValue>*). Clients rarely need the exact
// implementation: prefer returning an interface like *IList<T>*,
// *ICollection<T>*, *IEnumerable<T>* or *IDictionary<TKey,TValue>*.
//
// Not a rule, since the interface isn't preferable in all cases.
//</Desc>
./rules/assets/img/StatusBigOk32x32.png
P/Invokes should be static and not be publicly visible - System.Runtime.InteropServices
Methods marked DllImportAttribute (or VB Declare) access unmanaged code via Platform Invocation Services. Keep them private or internal so your library can't be used to breach security by giving callers access to unmanaged APIs. Matches DllImportAttribute methods declared public or non-static.
How to Fix:
Change the method's access level and/or declare it static.
// <Name>P/Invokes should be static and not be publicly visible</Name>
// <Id>ND2400:PInvokesShouldBeStaticAndNotBePubliclyVisible</Id>
warnif count > 0 from m in Application.Methods where
!m.IsThirdParty &&
(m.HasAttribute ("System.Runtime.InteropServices.DllImportAttribute".AllowNoMatch())) &&
( m.IsPubliclyVisible ||
!m.IsStatic)
select new {
m,
m.Visibility,
m.IsStatic,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *P/Invoke {0}* should be declared as *static*
// and should not be made *public*.
//</Expl>
//<Desc>
// Methods marked **DllImportAttribute** (or VB **Declare**) access
// unmanaged code via **Platform Invocation Services**. Keep them *private*
// or *internal* so your library can't be used to breach security by giving
// callers access to unmanaged APIs. Matches *DllImportAttribute* methods
// declared *public* or *non-static*.
//</Desc>
//<HowToFix>
// Change the method's access level and/or declare it static.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Move P/Invokes to NativeMethods class - System.Runtime.InteropServices
Platform Invocation methods (tagged DllImportAttribute or VB Declare) should be in one of these static internal classes with static internal methods:
- NativeMethods - no SuppressUnmanagedCodeSecurityAttribute: stack walks are performed, methods usable anywhere.
- SafeNativeMethods - SuppressUnmanagedCodeSecurityAttribute applied: harmless methods safe for anyone to call, no security review needed.
- UnsafeNativeMethods - SuppressUnmanagedCodeSecurityAttribute applied: potentially dangerous methods, callers must perform a full security review since no stack walk is performed.
Matches P/Invoke methods not declared in such classes.
How to Fix:
Move the method to the appropriate NativeMethods class; for most applications a new class named NativeMethods is enough.
// <Name>Move P/Invokes to NativeMethods class</Name>
// <Id>ND2401:MovePInvokesToNativeMethodsClass</Id>
warnif count > 0 from m in Application.Methods where
m.HasAttribute ("System.Runtime.InteropServices.DllImportAttribute".AllowNoMatch()) &&
m.ParentType.SimpleName != "NativeMethods"
select new {
m,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *P/Invoke {0}* should be moved to a dedicated class named *NativeMethods*.
//</Expl>
//<Desc>
// **Platform Invocation methods** (tagged **DllImportAttribute** or VB
// **Declare**) should be in one of these *static internal* classes with
// *static internal* methods:
//
// - **NativeMethods** - no *SuppressUnmanagedCodeSecurityAttribute*: stack
// walks are performed, methods usable anywhere.
//
// - **SafeNativeMethods** - *SuppressUnmanagedCodeSecurityAttribute*
// applied: harmless methods safe for anyone to call, no security review
// needed.
//
// - **UnsafeNativeMethods** - *SuppressUnmanagedCodeSecurityAttribute*
// applied: potentially dangerous methods, callers must perform a full
// security review since no stack walk is performed.
//
// Matches *P/Invoke* methods not declared in such classes.
//</Desc>
//<HowToFix>
// Move the method to the appropriate **NativeMethods** class; for most
// applications a new class named **NativeMethods** is enough.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
NativeMethods class should be static and internal - System.Runtime.InteropServices
As explained in Move P/Invokes to NativeMethods class, NativeMethods classes hosting P/Invoke methods should be static and internal. Warns about those that are not.
How to Fix:
Declare matched NativeMethods classes static and internal.
// <Name>NativeMethods class should be static and internal</Name>
// <Id>ND2402:NativeMethodsClassShouldBeStaticAndInternal</Id>
warnif count > 0 from t in Application.Types.WithNameIn(
@"NativeMethods", "SafeNativeMethods", "UnsafeNativeMethods") where
t.IsPublic || !t.IsStatic
select new {
t,
t.Visibility,
t.IsStatic,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* should only contain *P/Invoke methods*,
// so it should be declared as both *static* and *internal*.
//</Expl>
//<Desc>
// As explained in *Move P/Invokes to NativeMethods class*, *NativeMethods*
// classes hosting *P/Invoke* methods should be *static* and *internal*.
// Warns about those that are not.
//</Desc>
//<HowToFix>
// Declare matched *NativeMethods* classes *static* and *internal*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't create threads explicitly - System.Threading
Warns about methods instantiating
System.Threading.Thread. Threads are costly (~200,000 cycles to create, ~100,000 to destroy, 1MB stack, 2,000-8,000 cycles per context switch): prefer letting the thread pool recycle threads.
How to Fix:Use the
Task Parallel Library (TPL), relying on the CLR thread pool, instead of creating threads. Introduction:
https://msdn.microsoft.com/en-us/library/dd460717(v=vs.110).aspx TPL and the CLR v4 thread pool:
http://www.danielmoth.com/Blog/New-And-Improved-CLR-4-Thread-Pool-Engine.aspx
Severity is
Critical: creating threads can have severe consequences.
// <Name>Don't create threads explicitly</Name>
// <Id>ND2500:DontCreateThreadsExplicitly</Id>
warnif count > 0 from m in Application.Methods where
m.CreateA ("System.Threading.Thread".AllowNoMatch())
select new {
m,
Debt = 40.ToMinutes().ToDebt(),
Severity = Severity.Critical
}
//<Expl>
// The *{0}* creates a *Thread* object.
// It's better to use the thread pool instead because threads
// are expensive resources and can be challenging to manage effectively.
//</Expl>
//<Desc>
// Warns about methods instantiating *System.Threading.Thread*. Threads are
// costly (~200,000 cycles to create, ~100,000 to destroy, 1MB stack,
// 2,000-8,000 cycles per context switch): prefer letting the thread pool
// recycle threads.
//</Desc>
//<HowToFix>
// Use the **Task Parallel Library** *(TPL)*, relying on the CLR thread
// pool, instead of creating threads.
// Introduction: https://msdn.microsoft.com/en-us/library/dd460717(v=vs.110).aspx
// TPL and the CLR v4 thread pool:
// http://www.danielmoth.com/Blog/New-And-Improved-CLR-4-Thread-Pool-Engine.aspx
//
// Severity is **Critical**: creating threads can have severe consequences.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't use dangerous threading methods - System.Threading
// <Name>Don't use dangerous threading methods</Name>
// <Id>ND2501:DontUseDangerousThreadingMethods</Id>
warnif count > 0
let wrongMethods = ThirdParty.Methods.WithFullNameIn(
"System.Threading.Thread.Abort()",
"System.Threading.Thread.Abort(Object)",
"System.Threading.Thread.Sleep(Int32)",
"System.Threading.Thread.Suspend()",
"System.Threading.Thread.Resume()")
from m in Application.Methods.UsingAny(wrongMethods)
select new {
m,
suppressCallsTo = m.MethodsCalled.Intersect(wrongMethods),
Debt = 30.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* shouldn't call the *{1}*.
// Use instead facilities offered by the *Task Parallel Library (TPL)*.
//</Expl>
//<Desc>
// Warns about *Thread.Abort()*, *Sleep()*, *Suspend()* and *Resume()*:
//
// - *Thread.Abort()* is dangerous:
// http://www.interact-sw.co.uk/iangblog/2004/11/12/cancellation
//
// - *Thread.Sleep()* is a sign of flawed design:
// http://msmvps.com/blogs/peterritchie/archive/2007/04/26/thread-sleep-is-a-sign-of-a-poorly-designed-program.aspx
//
// - *Suspend()* and *Resume()* are dangerous and obsolete; workaround:
// http://stackoverflow.com/questions/382173/what-are-alternative-ways-to-suspend-and-resume-a-thread
//</Desc>
//<HowToFix>
// Suppress the calls listed in *suppressCallsTo*; use the **Task Parallel
// Library** *(TPL)* instead:
// https://msdn.microsoft.com/en-us/library/dd460717(v=vs.110).aspx
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Monitor TryEnter/Exit must be both called within the same method - System.Threading
Warns when
System.Threading.Monitor Enter()/
TryEnter() and
Exit() are not called within the same method: harder to locate where
critical sections begin and end, and complex error-prone scenarios.
How to Fix:Refactor so
Monitor critical sections begin and end in the same method. Use the C#
lock keyword for basic scenarios; explicit
Monitor only for advanced ones needing
Wait() and
Pulse(). More:
http://www.codeproject.com/Articles/13453/Practical-NET-and-C-Chapter
// <Name>Monitor TryEnter/Exit must be both called within the same method</Name>
// <Id>ND2502:MonitorTryEnterExitMustBeBothCalledWithinTheSameMethod</Id>
warnif count > 0
let enterMethods = ThirdParty.Methods.WithFullNameWildcardMatchIn(
"System.Threading.Monitor.Enter(*",
"System.Threading.Monitor.TryEnter(*")
from m in Application.Methods.UsingAny(enterMethods)
where
!m.IsUsing ("System.Threading.Monitor.Exit(Object)".AllowNoMatch())
select new {
m,
enterMethodsCalled = m.MethodsCalled.Intersect(enterMethods),
Debt = 15.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* calls *{1}* but doesn't call corresponding *Monitor.(Try)Exit()* method{1?:s}.
// Refactor the code so that a *critical section* is contained within a single method.
//</Expl>
//<Desc>
// Warns when **System.Threading.Monitor** *Enter()*/*TryEnter()* and
// *Exit()* are not called within the same method: harder to locate where
// **critical sections** begin and end, and complex error-prone scenarios.
//</Desc>
//<HowToFix>
// Refactor so *Monitor critical sections* begin and end in the same method.
// Use the C# **lock** keyword for basic scenarios; explicit *Monitor* only
// for advanced ones needing *Wait()* and *Pulse()*. More:
// http://www.codeproject.com/Articles/13453/Practical-NET-and-C-Chapter
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
ReaderWriterLock AcquireLock/ReleaseLock must be both called within the same method - System.Threading
Warns when System.Threading.ReaderWriterLock acquire and release (reader or writer) methods are not called within the same method: harder to locate where critical sections begin and end, and complex error-prone scenarios.
How to Fix:
Refactor so ReaderWriterLock read or write critical sections begin and end in the same method.
// <Name>ReaderWriterLock AcquireLock/ReleaseLock must be both called within the same method</Name>
// <Id>ND2503:ReaderWriterLockAcquireLockReleaseLockMustBeBothCalledWithinTheSameMethod</Id>
warnif count > 0
let acquireLockMethods = ThirdParty.Methods.WithFullNameWildcardMatch(
"System.Threading.ReaderWriterLock.Acquire*Lock(*")
let releaseLockMethods = ThirdParty.Methods.WithFullNameWildcardMatch(
"System.Threading.ReaderWriterLock.Release*Lock(*")
from m in Application.Methods.UsingAny(acquireLockMethods)
.Except(Application.Methods.UsingAny(releaseLockMethods))
select new {
m,
acquireLockMethods = m.MethodsCalled.Intersect(acquireLockMethods),
Debt = 20.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* calls *{1}* but doesn't call corresponding *ReaderWriterLock.ReleaseLock()* method{1?:s}.
// Refactor the code so that a *critical section* is contained within a single method.
//</Expl>
//<Desc>
// Warns when **System.Threading.ReaderWriterLock** acquire and release
// (reader or writer) methods are not called within the same method: harder
// to locate where **critical sections** begin and end, and complex
// error-prone scenarios.
//</Desc>
//<HowToFix>
// Refactor so *ReaderWriterLock read or write critical sections* begin and
// end in the same method.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't tag instance fields with ThreadStaticAttribute - System.Threading
Warns when
System.ThreadStaticAttribute tags
instance fields: per documentation it is exclusively for
static fields.
https://msdn.microsoft.com/en-us/library/system.threadstaticattributeHow to Fix:Ensure all fields tagged
ThreadStaticAttribute are
static.
// <Name>Don't tag instance fields with ThreadStaticAttribute</Name>
// <Id>ND2504:DontTagInstanceFieldsWithThreadStaticAttribute</Id>
warnif count > 0
from f in Application.Fields
where !f.IsStatic &&
f.HasAttribute ("System.ThreadStaticAttribute".AllowNoMatch())
select new {
f,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *instance {0}* shouldn't be tagged with *ThreadStaticAttribute*.
// This attribute is intended to be used exclusively
// with fields that are declared as *static*.
//</Expl>
//<Desc>
// Warns when **System.ThreadStaticAttribute** tags *instance* fields: per
// documentation it is exclusively for *static* fields.
// https://msdn.microsoft.com/en-us/library/system.threadstaticattribute
//</Desc>
//<HowToFix>
// Ensure all fields tagged *ThreadStaticAttribute* are *static*.
//</HowToFix>
./rules/assets/img/Info32x32.png
Method non-synchronized that read mutable states - System.Threading
Lists methods reading mutable state (modifiable instance or static fields) without synchronizing access — a potential state corruption in multi-threaded programs. Not a rule: most matches are not an issue.
// <Name>Method non-synchronized that read mutable states</Name>
// <Id>ND2505:MethodNonSynchronizedThatReadMutableStates</Id>
from m in Application.Methods where
(m.ReadsMutableObjectState || m.ReadsMutableTypeState) &&
!m.IsUsing ("System.Threading.Monitor".AllowNoMatch()) &&
!m.IsUsing ("System.Threading.ReaderWriterLock".AllowNoMatch())
select new {
m,
mutableFieldsUsed = m.FieldsUsed.Where(f => !f.IsImmutable)
}
//<Desc>
// Lists methods reading mutable state (modifiable instance or static
// fields) without synchronizing access — a potential state corruption in
// multi-threaded programs. Not a rule: most matches are not an issue.
//</Desc>
./rules/assets/img/StatusBigOk32x32.png
Method should not return concrete XmlNode - System.Xml
Warns about methods returning System.Xml.XmlNode or a derived type. XmlNode implements System.Xml.Xpath.IXPathNavigable; returning the interface usually abstracts client code from implementation details.
How to Fix:
Return IXPathNavigable instead and refactor client code if possible.
// <Name>Method should not return concrete XmlNode</Name>
// <Id>ND2600:MethodShouldNotReturnConcreteXmlNode</Id>
warnif count > 0
let xmlNode = ThirdParty.Types.WithFullName("System.Xml.XmlNode")
let concreteXmlTypes = ThirdParty.Types.ThatDeriveFromAny(xmlNode)
.Concat(xmlNode)
from m in Application.Methods.WithReturnTypeIn(concreteXmlTypes)
select new {
m,
m.ReturnType,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* should not return *concrete {1}*.
// Return instead the *interface IXPathNavigable*
// to shield client code from specific implementation details.
//</Expl>
//<Desc>
// Warns about methods returning **System.Xml.XmlNode** or a derived type.
// *XmlNode* implements **System.Xml.Xpath.IXPathNavigable**; returning the
// interface usually abstracts client code from implementation details.
//</Desc>
//<HowToFix>
// Return *IXPathNavigable* instead and refactor client code if possible.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Types should not derive from System.Xml.XmlDocument - System.Xml
Warns about subclasses of
System.Xml.XmlDocument: don't subclass it to create an XML view of an underlying object model or data source.
How to Fix:Use the
System.Xml.XPath.IXPathNavigable interface implemented by
XmlDocument, or use
System.Xml.Linq.XDocument (
LINQ2XML). More:
http://stackoverflow.com/questions/1542073/xdocument-or-xmldocument
// <Name>Types should not derive from System.Xml.XmlDocument</Name>
// <Id>ND2601:TypesShouldNotExtendSystemXmlXmlDocument</Id>
warnif count > 0 from t in Application.Types where
t.DeriveFrom("System.Xml.XmlDocument".AllowNoMatch())
select new {
t,
Debt = 20.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* should not derive from *XmlDocument*.
// Instead use the *LINQ2XML* *System.Xml.Linq.XDocument* class.
//</Expl>
//<Desc>
// Warns about subclasses of **System.Xml.XmlDocument**: don't subclass it
// to create an XML view of an underlying object model or data source.
//</Desc>
//<HowToFix>
// Use the **System.Xml.XPath.IXPathNavigable** interface implemented by
// *XmlDocument*, or use **System.Xml.Linq.XDocument** (**LINQ2XML**). More:
// http://stackoverflow.com/questions/1542073/xdocument-or-xmldocument
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Float and Date Parsing must be culture aware - System.Globalization
Warns about
non-globalized overloads (without an
IFormatProvider parameter) of
Parse(),
TryParse() and
ToString() on
DateTime,
float,
double and
decimal — a symptom that the application supporting localized UIs and regional data is
at least partially not globalized.
How to Fix:Use the globalized overloads; culprit callers are listed in
MethodsCallingMe. More on
Creating Globally Aware Applications:
https://msdn.microsoft.com/en-us/library/cc853414(VS.95).aspx
Estimated Debt: 5 minutes per method calling a non-culture aware method, plus 3 minutes per non-culture aware method called.
// <Name>Float and Date Parsing must be culture aware</Name>
// <Id>ND2700:FloatAndDateParsingMustBeCultureAware</Id>
warnif count > 0
let cultureUnawareMethods =
(from m in ThirdParty.Types.WithFullNameIn(
"System.DateTime",
"System.Single",
"System.Double",
"System.Decimal").ChildMethods()
where m.NbParameters > 0 &&
(m.SimpleNameEqualsAny(
"Parse", "TryParse", "ToString")) &&
!m.Name.Contains("IFormatProvider")
select m).ToHashSetEx()
from m in JustMyCode.Methods.UsingAny(cultureUnawareMethods)
let cultureUnawareMethodsCalled = m.MethodsCalled.Intersect(cultureUnawareMethods)
select new {
m,
shouldntCall = cultureUnawareMethodsCalled,
Debt = (5 + 3*cultureUnawareMethodsCalled.Count()).ToMinutes().ToDebt(),
Severity = 5*cultureUnawareMethodsCalled.Count().ToMinutes().ToAnnualInterest()
}
//<Expl>
// The *{0}* calls the *{1}* and should use a culture aware version instead.
// This way your application gets globalized.
//</Expl>
//<Desc>
// Warns about *non-globalized overloads* (without an **IFormatProvider**
// parameter) of **Parse()**, **TryParse()** and **ToString()** on
// **DateTime**, **float**, **double** and **decimal** — a symptom that the
// application supporting localized UIs and regional data is *at least
// partially* not globalized.
//</Desc>
//<HowToFix>
// Use the globalized overloads; culprit callers are listed in
// **MethodsCallingMe**. More on **Creating Globally Aware Applications**:
// https://msdn.microsoft.com/en-us/library/cc853414(VS.95).aspx
//
// Estimated Debt: 5 minutes per method calling a non-culture aware method,
// plus 3 minutes per non-culture aware method called.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Mark assemblies with assembly version - System.Reflection
An assembly identity is: name, version number, culture, public key (if strong-named). .NET uses the version to uniquely identify an assembly and bind to types in strong-named assemblies; by default applications only run with the version they were built with. Matches assemblies not tagged with System.Reflection.AssemblyVersionAttribute.
How to Fix:
Add a version via System.Reflection.AssemblyVersionAttribute.
// <Name>Mark assemblies with assembly version</Name>
// <Id>ND2800:MarkAssembliesWithAssemblyVersion</Id>
warnif count > 0
let isUnityProject = ThirdParty.Assemblies.Any(a => a.Name.StartsWith("UnityEngine"))
from a in Application.Assemblies where
!isUnityProject && // Version is not required on Unity assemblies.
// You can still use the AssemblyVersionAttribute in Unity context
// as explained here https://forum.unity.com/threads/assemblyinfo-cs-solutions.529418/
!a.HasAttribute ("System.Reflection.AssemblyVersionAttribute".AllowNoMatch()) &&
// Make sure the assembly version is not defined through a way different than AssemblyVersionAttribute
(a.Version == null || a.Version.ToString() == "0.0.0.0")
select new {
a,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* should declare a version through the *System.Reflection.AssemblyVersionAttribute*.
//</Expl>
//<Desc>
// An assembly identity is: name, version number, culture, public key (if
// strong-named). .NET uses the version to uniquely identify an assembly
// and bind to types in strong-named assemblies; by default applications
// only run with the version they were built with. Matches assemblies not
// tagged with **System.Reflection.AssemblyVersionAttribute**.
//</Desc>
//<HowToFix>
// Add a version via *System.Reflection.AssemblyVersionAttribute*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Assemblies should have the same version - System.Reflection
Reports assemblies whose version differs from the one shared by most application assemblies. Double check first whether multiple versions are legitimate — typically when assemblies are not
compiled, developed or deployed together.
How to Fix:Use
System.Reflection.AssemblyVersion in a source file shared by the assemblies, typically generated by a dedicated
MSBuild task like:
http://www.msbuildextensionpack.com/help/4.0.5.0/html/d6c3b5e8-00d4-c826-1a73-3cfe637f3827.htm. Versioning advices:
http://stackoverflow.com/a/3905443/27194
Severity is
major: improper versioning can lead to complicated deployment problems.
// <Name>Assemblies should have the same version</Name>
// <Id>ND2801:AssembliesShouldHaveTheSameVersion</Id>
warnif count > 0
let versionsLookup = Application.Assemblies.ToLookup(a => a.Version, a=> a)
let mostRepresentedVersion = versionsLookup.OrderByDescending(v => v.Count()).First().Key
from v in versionsLookup
where v.Key != mostRepresentedVersion
from a in v.ToArray()
select new {
a ,
version = v.Key,
mostRepresentedVersion,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* has version *{1}* while most application assemblies have version *{2}*.
//</Expl>
//<Desc>
// Reports assemblies whose version differs from the one shared by most
// application assemblies. Double check first whether multiple versions are
// legitimate — typically when assemblies are not *compiled, developed or
// deployed* together.
//</Desc>
//<HowToFix>
// Use **System.Reflection.AssemblyVersion** in a source file shared by the
// assemblies, typically generated by a dedicated *MSBuild* task like:
// http://www.msbuildextensionpack.com/help/4.0.5.0/html/d6c3b5e8-00d4-c826-1a73-3cfe637f3827.htm.
// Versioning advices: http://stackoverflow.com/a/3905443/27194
//
// Severity is **major**: improper versioning can lead to complicated
// deployment problems.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Assemblies Referenced in Multiple Versions - System.Reflection
Detects third-party assemblies referenced in multiple versions, which can cause unexpected behavior and bugs. The issue explanation lists the application and third-party assemblies referencing the different versions; the scan is transitive.
How to Fix:Reference each package in a single version via
Transitive pinning in Central Package Management: set
CentralPackageTransitivePinningEnabled to
true in a
Directory.Packages.props or
Directory.Build.props import file. More:
https://blog.ndepend.com/understand-nuget-central-package-management-and-transitive-pinning-with-examples/
Pinning aligns the versions your projects restore and compile against, but it cannot change a reference compiled into a third-party package: when the issue explanation lists a third-party assembly that uses the older version, the issue remains after pinning. Upgrade that package to a release built against the newer version, or remove it if your code barely uses it.
If pinning breaks the restore with a package downgrade error (NU1109), raise the central version of the package named in the error to a stable release that satisfies the graph, or leave pinning off. Never raise it to a prerelease.
// <Name>Assemblies Referenced in Multiple Versions</Name>
// <Id>ND2802:AssembliesReferencedInMultipleVersions</Id>
warnif count > 0
// Use this hashset to avoid to have several issues on a type
let hashset = new HashSet<IType>()
// Build a lookup of all assembly references,
// from both application and third-party assemblies used by the application,
// grouped by the referenced assembly name.
let refs =
(from asm in Assemblies
from asmRef in asm.AssemblyReferences
// Uncomment this line to run the rule on .NET assemblies too
where !asmRef.Name.StartsWithAny("System.","Microsoft.")
select new {
asm,
RefName = asmRef.Name,
RefVersion = asmRef.Version })
.ToLookup(x => x.RefName)
from refsForSameAsm in refs
let variousVersions = refsForSameAsm.Distinct(x => x.RefVersion).ToArray()
// Require multiple versions
where variousVersions.Length > 1
// Resolve the IAssembly from the assembly reference name
let asm = Assemblies.FirstOrDefault(a => a.Name == refsForSameAsm.Key)
where asm != null
orderby asm.Name
orderby variousVersions.Length descending
// The issue appears on the first type using one culprit third-party assembly
let typeUsers = ApplicationAndTestCode.Types.Using(asm)
.Where(t => !t.IsGeneratedByCompiler || t.Name.StartsWith("Program+")).ToArray()
let FirstAppTypeUser = (typeUsers.Length == 0 ? ApplicationAndTestCode.Types : typeUsers ) // Make sure at least one type else we get an exception!
.Aggregate((minElement, nextElement) =>
string.Compare(nextElement.Name, minElement.Name, StringComparison.Ordinal) < 0 && !hashset.Contains(nextElement)
? nextElement
: minElement)
let fillHashset = hashset.Add(FirstAppTypeUser)
select new {
FirstAppTypeUser,
asm,
NbVersions = variousVersions.Length,
Versions = variousVersions.Select(
vv => "" + vv.asm.Name + " uses v" + vv.RefVersion.ToString() + "")
.Aggregate(" ; "),
Severity = Severity.Medium,
Debt = 15.ToMinutes().ToDebt()
}
//<Expl>
// The *{1}* is referenced through *{2}* versions. {3}.
//
// The *{0}* is the first type (alphabetically) in the application that uses this assembly.
//</Expl>
//<Desc>
// Detects third-party assemblies referenced in multiple versions, which can
// cause unexpected behavior and bugs. The issue explanation lists the
// application and third-party assemblies referencing the different
// versions; the scan is transitive.
//</Desc>
//<HowToFix>
// Reference each package in a single version via **Transitive pinning in
// Central Package Management**: set *CentralPackageTransitivePinningEnabled*
// to *true* in a *Directory.Packages.props* or *Directory.Build.props*
// import file. More:
// https://blog.ndepend.com/understand-nuget-central-package-management-and-transitive-pinning-with-examples/
//
// Pinning aligns the versions your projects restore and compile against,
// but it cannot change a reference compiled into a third-party package:
// when the issue explanation lists a third-party assembly that uses the
// older version, the issue remains after pinning. Upgrade that package to
// a release built against the newer version, or remove it if your code
// barely uses it.
//
// If pinning breaks the restore with a package downgrade error (NU1109),
// raise the central version of the package named in the error to a
// stable release that satisfies the graph, or leave pinning off. Never
// raise it to a prerelease.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Public methods returning a reference needs a contract to ensure that a non-null reference is returned - Microsoft.Contracts
Code Contracts decrease caller/callee ambiguity; not ensuring a returned reference is
non-null leaves ambiguity. Matches methods returning a reference type without using
Contract.Ensure() (from
Microsoft Code Contracts for .NET), typically:
Contract.Ensures(Contract.Result<ReturnType>() != null, "returned reference is not null"); https://visualstudiogallery.msdn.microsoft.com/1ec7db13-3363-46c9-851f-1ce455f66970How to Fix:Use
Microsoft Code Contracts for .NET on your API's public surface to remove
null / not null ambiguities. For methods that might not return a result, prefer the
TryXXX() pattern (like
System.Int32.TryParse()) over returning
null.
Estimated Debt: 5 minutes per method possibly returning null plus 3 minutes per caller.
// <Name>Public methods returning a reference needs a contract to ensure that a non-null reference is returned</Name>
// <Id>ND2900:PublicMethodsReturningAReferenceNeedsAContractToEnsureThatANonNullReferenceIsReturned</Id>
warnif count > 0
let ensureMethods = Application.Methods.WithFullName(
"System.Diagnostics.Contracts.__ContractsRuntime.Ensures(Boolean,String,String)")
from ensureMethod in ensureMethods
from m in ensureMethod.ParentAssembly.ChildMethods where
m.IsPubliclyVisible &&
!m.IsAbstract &&
m.ReturnType != null &&
// Identify that the return type is a reference type
(m.ReturnType.IsClass || m.ReturnType.IsInterface) &&
!m.IsUsing(ensureMethod) &&
// Don't match method not implemented yet!
!m.CreateA("System.NotImplementedException".AllowNoMatch())
select new {
m,
ReturnTypeReference = m.ReturnType,
Debt = (5+3*m.MethodsCallingMe.Count()).ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *public {0}* returns a reference and should be tagged
// with a contract to ensure that a non-null reference is returned
//</Expl>
//<Desc>
// **Code Contracts** decrease caller/callee ambiguity; not ensuring a
// returned reference is *non-null* leaves ambiguity. Matches methods
// returning a reference type without using **Contract.Ensure()** (from
// **Microsoft Code Contracts for .NET**), typically:
// *Contract.Ensures(Contract.Result<ReturnType>() != null,
// "returned reference is not null");*
// https://visualstudiogallery.msdn.microsoft.com/1ec7db13-3363-46c9-851f-1ce455f66970
//</Desc>
//<HowToFix>
// Use *Microsoft Code Contracts for .NET* on your API's public surface to
// remove *null / not null* ambiguities. For methods that might not return a
// result, prefer the **TryXXX()** pattern (like *System.Int32.TryParse()*)
// over returning *null*.
//
// Estimated Debt: 5 minutes per method possibly returning null plus 3
// minutes per caller.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Classes tagged with InitializeOnLoad should have a static constructor - Unity
InitializeOnLoadAttribute initializes an Editor class when Unity loads and when scripts are recompiled; the tagged class's static constructor is responsible for the initialization.
How to Fix:
Create a static constructor for the matched classes.
// <Name>Classes tagged with InitializeOnLoad should have a static constructor</Name>
// <Id>ND3200:ClassesTaggedWithInitializeOnLoadShouldHaveAStaticConstructor</Id>
warnif count > 0
from t in Application.Types where t.HasAttribute("UnityEditor.InitializeOnLoadAttribute".AllowNoMatch())
where !t.Name.Contains("Readme") && // Discard false positive on generated code
t.Methods.SingleOrDefault(m => m.IsClassConstructor) == null
select new {
t,
Debt = 8.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* is tagged with *InitializeOnLoadAttribute* and thus,
// should have a static constructor to initialize the *Editor* class.
//</Expl>
//<Desc>
// *InitializeOnLoadAttribute* initializes an Editor class when Unity loads
// and when scripts are recompiled; the tagged class's static constructor is
// responsible for the initialization.
//</Desc>
//<HowToFix>
// Create a static constructor for the matched classes.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid using non-generic GetComponent - Unity
Prefer using the generic version of Component.GetComponent() for type safety.
How to Fix:
Refactor GetComponent(typeof(MyType)) calls in GetComponent<MyType>().
// <Name>Avoid using non-generic GetComponent</Name>
// <Id>ND3201:AvoidUsingNonGenericGetComponent</Id>
warnif count > 0
from m in Application.Methods where m.IsUsing("UnityEngine.Component.GetComponent(Type)".AllowNoMatch())
select new {
m,
Debt = 8.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* is using *UnityEngine.Component.GetComponent(Type)*.
// It should use instead the generic version of *Component.GetComponent()*
// for type safety.
//</Expl>
//<Desc>
// Prefer using the generic version of *Component.GetComponent()*
// for type safety.
//</Desc>
//<HowToFix>
// Refactor *GetComponent(typeof(MyType))* calls in *GetComponent<MyType>()*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid empty Unity message - Unity
Unity messages are called by the runtime even if they don't contain any code.
How to Fix:
Remove matched methods to avoid unnecessary processing.
// <Name>Avoid empty Unity message</Name>
// <Id>ND3202:AvoidEmptyUnityMessage</Id>
warnif count > 0
from t in Application.Types
where t.DeriveFrom("UnityEngine.MonoBehaviour".AllowNoMatch())
from m in t.InstanceMethods where
!m.IsConstructor &&
!m.IsPropertyGetter &&
!m.IsPropertySetter &&
m.NbLinesOfCode == 0 &&
m.SimpleNameEqualsAny("Start","Update","FixedUpdate","LateUpdate",
"OnGUI","OnDisable","OnEnable")
select new {
m,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.Low
}
//<Expl>
// The *{0}* is an empty Unity message and is still called by the runtime.
// Comment it or remove it.
//</Expl>
//<Desc>
// Unity messages are called by the runtime even if they don't contain any code.
//</Desc>
//<HowToFix>
// Remove matched methods to avoid unnecessary processing.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Avoid using Time.fixedDeltaTime with Update - Unity
Update() and FixedUpdate() depend on the frame rate. Use Time.deltaTime instead of Time.fixedDeltaTime: it automatically returns the right delta time inside these functions.
How to Fix:
Replace Time.fixedDeltaTime with Time.deltaTime within Update() and FixedUpdate().
// <Name>Avoid using Time.fixedDeltaTime with Update</Name>
// <Id>ND3203:AvoidUsingTimeFixedDeltaTimeWithUpdate</Id>
warnif count > 0
from m in Application.Methods where
m.SimpleNameEqualsAny("Update","FixedUpdate") &&
m.NbParameters == 0 &&
m.IsUsing("UnityEngine.Time.get_fixedDeltaTime()".AllowNoMatch()) &&
m.ParentType.DeriveFrom("UnityEngine.MonoBehaviour".AllowNoMatch())
select new {
m,
Debt = 8.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* is dependent on the frame rate.
// It should use *Time.deltaTime* instead of *Time.fixedDeltaTime*.
//</Expl>
//<Desc>
// *Update()* and *FixedUpdate()* depend on the frame rate. Use
// *Time.deltaTime* instead of *Time.fixedDeltaTime*: it automatically
// returns the right delta time inside these functions.
//</Desc>
//<HowToFix>
// Replace *Time.fixedDeltaTime* with *Time.deltaTime* within *Update()*
// and *FixedUpdate()*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Use CreateInstance to create a scriptable object - Unity
Instances of types deriving from UnityEngine.ScriptableObject must be created via ScriptableObject.CreateInstance<T>(): the Unity engine needs to create the object itself to handle Unity message methods.
How to Fix:
Replace new Foo() expressions with ScriptableObject.CreateInstance<Foo>() expressions.
// <Name>Use CreateInstance to create a scriptable object</Name>
// <Id>ND3204:UseCreateInstanceToCreateAScriptableObject</Id>
warnif count > 0
let scriptableObjects = Types.Where(t => t.DeriveFrom("UnityEngine.ScriptableObject".AllowNoMatch()))
let ctors = scriptableObjects.SelectMany(t => t.Constructors).ToHashSetEx()
from m in Application.Methods.ThatCreateAny(scriptableObjects)
let ctorsCalled = m.MethodsCalled.Intersect(ctors).ToArray()
let typesInstantiated = ctorsCalled.ParentTypes()
where typesInstantiated.Any()
select new {
m,
shouldntInstantiate = typesInstantiated,
Debt = (5 + 5*typesInstantiated.Count()).ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* shouldn't instantiate directly the *{1}*
// and should use the method *ScriptableObject.CreateInstance<T>()* instead.
//</Expl>
//<Desc>
// Instances of types deriving from *UnityEngine.ScriptableObject* must be
// created via *ScriptableObject.CreateInstance<T>()*: the Unity engine
// needs to create the object itself to handle Unity message methods.
//</Desc>
//<HowToFix>
// Replace *new Foo()* expressions with
// *ScriptableObject.CreateInstance<Foo>()* expressions.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
The SerializeField attribute is redundant on public fields - Unity
SerializeField makes a field display in the Inspector and be saved — which declaring the field public already does. Hence the attribute is redundant on public fields.
How to Fix:
Remove the SerializeField attribute on matched public fields.
// <Name>The SerializeField attribute is redundant on public fields</Name>
// <Id>ND3205:TheSerializeFieldAttributeIsRedundantOnPublicFields</Id>
warnif count > 0
from f in Application.Fields where
f.IsPubliclyVisible &&
f.HasAttribute("UnityEngine.SerializeFieldAttribute".AllowNoMatch())
select new {
f,
Debt = 3.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *public {0}* shouldn't be tagged with *SerializeFieldAttribute*,
// this causes redundancy.
//</Expl>
//<Desc>
// *SerializeField* makes a field display in the Inspector and be saved —
// which declaring the field public already does. Hence the attribute is
// redundant on public fields.
//</Desc>
//<HowToFix>
// Remove the *SerializeField* attribute on matched public fields.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
InitializeOnLoadMethod should tag only static and parameterless methods - Unity
InitializeOnLoadMethodAttribute initializes an editor static method when Unity loads; RuntimeInitializeOnLoadMethodAttribute does the same after the game has loaded. Both can only tag static parameterless methods.
How to Fix:
Make the matched methods static and parameterless.
// <Name>InitializeOnLoadMethod should tag only static and parameterless methods</Name>
// <Id>ND3206:InitializeOnLoadMethodShouldTagOnlyStaticAndParameterlessMethods</Id>
warnif count > 0
from m in Application.Methods where
(m.HasAttribute ("UnityEditor.InitializeOnLoadMethodAttribute".AllowNoMatch()) ||
m.HasAttribute ("UnityEditor.RuntimeInitializeOnLoadMethodAttribute".AllowNoMatch())) &&
(m.NbParameters > 0 || !m.IsStatic)
select new {
m,
Debt = 3.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* is tagged with *InitializeOnLoadMethodAttribute*
// that should only tag static and parameterless methods.
//</Expl>
//<Desc>
// *InitializeOnLoadMethodAttribute* initializes an editor static method
// when Unity loads; *RuntimeInitializeOnLoadMethodAttribute* does the same
// after the game has loaded. Both can only tag static parameterless
// methods.
//</Desc>
//<HowToFix>
// Make the matched methods static and parameterless.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Prefer using SetPixels32() over SetPixels() - Unity
Color uses floating point [0.0,1.0] per component; Color32 uses a byte [0,255]: 4 times less memory and much faster to work with.
How to Fix:
If 32bit-RGBA is compatible with your scenario, use SetPixels32() instead.
// <Name>Prefer using SetPixels32() over SetPixels()</Name>
// <Id>ND3207:PreferUsingSetPixels32OverSetPixels</Id>
warnif count > 0
let setPixelMethods = Types.WithFullName("UnityEngine.Texture2D")
.ChildMethods().WithSimpleNameIn("SetPixel","SetPixels").ToHashSetEx()
from m in Application.Methods.UsingAny(setPixelMethods)
let shouldntCall = m.MethodsCalled.Intersect(setPixelMethods)
select new {
m,
shouldntCall,
Debt = 15.ToMinutes().ToDebt(),
Severity = Severity.Major
}
//<Expl>
// The *{0}* shouldn't call *{1}* but instead
// should call *SetPixels32()* for performance reasons.
//</Expl>
//<Desc>
// *Color* uses floating point [0.0,1.0] per component; *Color32* uses a
// byte [0,255]: 4 times less memory and much faster to work with.
//</Desc>
//<HowToFix>
// If 32bit-RGBA is compatible with your scenario, use
// *SetPixels32()* instead.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Don't use System.Reflection in performance critical messages - Unity
System.Reflection is slow: don't use it in performance critical messages like Update, FixedUpdate, LateUpdate, or OnGUI.
How to Fix:
Don't use reflection in matched methods.
// <Name>Don't use System.Reflection in performance critical messages</Name>
// <Id>ND3208:DontUseSystemReflectionInPerformanceCriticalMessages</Id>
warnif count > 0
let reflectionMembers = Namespaces.WithName("System.Reflection").ChildMembers()
from m in Application.Methods.UsingAny(reflectionMembers) where
m.ParentType.DeriveFrom("UnityEngine.MonoBehaviour".AllowNoMatch()) &&
m.SimpleNameEqualsAny("Update", "FixedUpdate", "LateUpdate", "OnGUI")
let shouldntUse = m.MembersUsed.Intersect(reflectionMembers)
select new {
m,
shouldntUse,
Debt = (5 + 5*shouldntUse.Count()).ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* shouldn't use *{1}* because
// *Reflection* shouldn't be used in performance critical messages.
//</Expl>
//<Desc>
// *System.Reflection* is slow: don't use it in performance critical
// messages like *Update*, *FixedUpdate*, *LateUpdate*, or *OnGUI*.
//</Desc>
//<HowToFix>
// Don't use reflection in matched methods.
//</HowToFix>
./rules/assets/img/JustMyCode32x32.png
Discard generated Assemblies from JustMyCode - Defining JustMyCode
A
notmycode query: matched assemblies (and their namespaces, types, methods, fields) are removed from the
JustMyCode code base view used by most default queries and rules.
It matches assemblies tagged
System.CodeDom.Compiler.GeneratedCodeAttribute, ASP.NET Core Razor
.Views.dll assemblies, and assemblies with no C#, VB or F# code. Enrich it to discard your own generated assemblies.
notmycode queries run before other queries and rules; modifying one re-runs the queries relying on
JustMyCode. Several
notmycode assemblies queries cumulate. Documentation:
https://www.ndepend.com/docs/cqlinq-syntax#NotMyCode
// <Name>Discard generated Assemblies from JustMyCode</Name>
// <Id>ND3000:DiscardGeneratedAssembliesFromJustMyCode</Id>
notmycode
from a in Application.Assemblies where
// Assemblies generated for Xsl IL compilation for example are tagged with this attribute
a.HasAttribute ("System.CodeDom.Compiler.GeneratedCodeAttribute".AllowNoMatch())
// Consider '.Views.dll' ASP.NET Core Razor assemblies as notmycode
// because they are generated by the compiler
|| a.SimpleName.EndsWith(".views", StringComparison.OrdinalIgnoreCase) &&
a.AssembliesUsed.Any(aUsed => aUsed.SimpleName.EqualsAny(
"microsoft.aspnetcore.razor", "microsoft.aspnetcore.mvc.razor"
, StringComparison.OrdinalIgnoreCase))
// Don't consider assemblies with no types
|| a.ChildTypes.FirstOrDefault() == null
select a
//<Desc>
// A **notmycode** query: matched assemblies (and their namespaces, types,
// methods, fields) are removed from the *JustMyCode* code base view used by
// most default queries and rules.
//
// It matches assemblies tagged
// *System.CodeDom.Compiler.GeneratedCodeAttribute*, ASP.NET Core Razor
// *.Views.dll* assemblies, and assemblies with no C#, VB or F# code. Enrich
// it to discard your own generated assemblies.
//
// *notmycode* queries run before other queries and rules; modifying one
// re-runs the queries relying on *JustMyCode*. Several *notmycode*
// assemblies queries cumulate. Documentation:
// https://www.ndepend.com/docs/cqlinq-syntax#NotMyCode
//</Desc>
./rules/assets/img/JustMyCode32x32.png
Discard generated Namespaces from JustMyCode - Defining JustMyCode
A
notmycode query: matched namespaces (and their types, methods, fields) are removed from the
JustMyCode code base view used by most default queries and rules.
It matches the VB.NET-generated
My namespaces, and tool-generated namespaces like
mshtml or
Microsoft.Office.*.
notmycode queries run before other queries and rules; modifying one re-runs the queries relying on
JustMyCode. Several
notmycode namespaces queries cumulate. Documentation:
https://www.ndepend.com/docs/cqlinq-syntax#NotMyCode
// <Name>Discard generated Namespaces from JustMyCode</Name>
// <Id>ND3001:DiscardGeneratedNamespacesFromJustMyCode</Id>
notmycode
// First gather assemblies written with VB.NET
let vbnetAssemblies = Application.Assemblies.Where(
a => a.SourceDecls.Any(decl =>
string.Equals(decl.SourceFile.FileNameExtension, ".vb",
StringComparison.OrdinalIgnoreCase)))
// Then find the My namespace and its child namespaces.
let vbnetMyNamespaces = vbnetAssemblies.ChildNamespaces().Where(
n => n.SimpleNameEquals("My") ||
n.ParentNamespaces.Any(nParent => nParent.SimpleNameEquals("My")))
let generatedNamespace = Application.Namespaces.WithFullNameIn(
// COM/Import mshtml namespace and all its types are not-my-code
"mshtml",
// Some VB.NET generated code goes into this namespace
"Microsoft.VisualBasic",
// Roslyn can generate types in these namespaces, consider them as not-my-code
"Microsoft.CodeAnalysis",
"System.Runtime.CompilerServices",
"AspNetCoreGeneratedDocument")
.Concat(Application.Namespaces.WithSimpleNameIn(
// Don't match types generated by the Refit infrastructure like the class PreserveAttribute
"RefitInternalGenerated"))
.Concat(Application.Namespaces.Where(
// Don't match classes in namespaces generated by the Blazor and Maui infrastructure
n => n.Name.StartsWithAny("__Blazor", "Microsoft.Maui.Controls")))
.Concat(Application.Namespaces.Where(
// Anonymous namespaces with only types generated by the compiler are not-my-code
n => n.Name == "" && n.ChildTypes.All(t => t.IsGeneratedByCompiler)))
.Concat(Application.Namespaces.Where(
// Don't match namespaces generated by PostSharp
n => n.Name.StartsWith("PostSharp.ImplementationDetails")))
.Concat(Application.Namespaces.Where(
// Namespaces of a Roslyn source generator whose generated files are not on disk: all their types
// are tagged with GeneratedCodeAttribute, generated by the compiler, or declared in no source file
// of an assembly that has sources (an assembly analyzed without its PDB files is not concerned).
n => n.ParentAssembly.SourceFileDeclAvailable &&
n.ChildTypes.Any(t => t.HasAttribute("System.CodeDom.Compiler.GeneratedCodeAttribute".AllowNoMatch())) &&
n.ChildTypes.All(t => t.HasAttribute("System.CodeDom.Compiler.GeneratedCodeAttribute".AllowNoMatch()) ||
t.IsGeneratedByCompiler ||
!t.SourceFileDeclAvailable)))
// Discard types generated in Microsoft.Office.* namespaces.
// Issues won't be reported on this code. Especially issues related
// to the rule "Interface name should begin with a 'I'" broken
// by many interfaces in these namespaces.
let microsoftOffices = Application.Namespaces.Where(n => n.Name.StartsWith("Microsoft.Office"))
from n in vbnetMyNamespaces.Concat(generatedNamespace).Concat(microsoftOffices)
select n
//<Desc>
// A **notmycode** query: matched namespaces (and their types, methods,
// fields) are removed from the *JustMyCode* code base view used by most
// default queries and rules.
//
// It matches the VB.NET-generated **My** namespaces, and tool-generated
// namespaces like **mshtml** or **Microsoft.Office.***.
//
// *notmycode* queries run before other queries and rules; modifying one
// re-runs the queries relying on *JustMyCode*. Several *notmycode*
// namespaces queries cumulate. Documentation:
// https://www.ndepend.com/docs/cqlinq-syntax#NotMyCode
//</Desc>
./rules/assets/img/JustMyCode32x32.png
Discard generated Types from JustMyCode - Defining JustMyCode
A
notmycode query: matched types (and their methods and fields) are removed from the
JustMyCode code base view used by most default queries and rules.
It matches several well-identified generated types, like the ones tagged
System.CodeDom.Compiler.GeneratedCodeAttribute. Enrich it to discard your own generated types.
notmycode queries run before other queries and rules; modifying one re-runs the queries relying on
JustMyCode. Several
notmycode types queries cumulate. Documentation:
https://www.ndepend.com/docs/cqlinq-syntax#NotMyCode
// <Name>Discard generated Types from JustMyCode</Name>
// <Id>ND3002:DiscardGeneratedTypesFromJustMyCode</Id>
notmycode
// Define some sets to quickly test EntityFramework generated types
let efContexts = Application.Types.Where(t =>
t.DeriveFrom("System.Data.Entity.DbContext".AllowNoMatch()) &&
t.SourceDecls.Count(
sd => sd.SourceFile.FileName.EndsWithAny(
".context.cs", ".context.vb", StringComparison.OrdinalIgnoreCase))
== 1).ToHashSetEx()
let efEntities = Application.Types.UsedByAny(efContexts).ToHashSetEx()
let efMigrations = Application.Types.Where(t =>
t.DeriveFrom("System.Data.Entity.Migrations.DbMigration".AllowNoMatch()) ||
t.DeriveFrom("Microsoft.EntityFrameworkCore.Migrations.Migration".AllowNoMatch())).ToHashSetEx()
let efModelSnapshots = Application.Types.Where(t => t.DeriveFrom(
"Microsoft.EntityFrameworkCore.Infrastructure.ModelSnapshot".AllowNoMatch())).ToHashSetEx()
let unityContext = ThirdParty.Assemblies.Any(a => a.Name.StartsWith("UnityEngine"))
from t in Application.Types where
// Don't consider anonymous types as JustMyCode
// C# and VB.NET anonymous types generated by the compiler satisfies these conditions
(t.IsGeneratedByCompiler &&
t.ParentNamespace.Name.Length == 0 &&
t.SimpleNameLike("AnonymousType")) ||
// Types generated by the compiler for collections
(t.IsGeneratedByCompiler &&
t.Name.EndsWith("z__ReadOnlyArray<T>")) ||
// Don't consider Attribute classes as my code, to avoid reporting issues
// that comes from the fact that attribute are not explicitly instantiated in code,
// this provokes coverage or dead code issues.
t.IsAttributeClass ||
// Resources, Settings, or typed DataSet generated types for example, are tagged with this attribute
t.HasAttribute ("System.CodeDom.Compiler.GeneratedCodeAttribute".AllowNoMatch()) ||
// This attribute identifies classes compiled by the Razor infrastructure
t.HasAttribute ("Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemMetadataAttribute".AllowNoMatch()) ||
// This attribute identifies a type or member that is not part of the user code for an application.
t.HasAttribute ("System.Diagnostics.DebuggerNonUserCodeAttribute".AllowNoMatch()) ||
// These attributes identify a type that is defined with COM, elsewhere, so consider it not-my-code.
t.HasAttribute ("System.Runtime.InteropServices.GuidAttribute".AllowNoMatch()) ||
t.HasAttribute ("System.Runtime.InteropServices.TypeIdentifierAttribute".AllowNoMatch()) ||
// Discard ASP.NET page types generated by aspnet_compiler.exe
// See: https://www.ndepend.com/FAQ.aspx#ASPNET
t.ParentNamespace.Name.EqualsAny("ASP", "__ASP") ||
// Discard ASP.NET special types
(t.SimpleNameEqualsAny("Startup","BundleConfig","RouteConfig") &&
ThirdParty.Assemblies.WithNameWildcardMatchIn("System.Web*", "Microsoft.AspNetCore*").Any()) ||
// Discard Blazor and Razor special types
(t.SimpleNameEqualsAny("_Imports", "TypeInference")) ||
// Discard xUnit generated type
(t.SimpleNameEqualsAny("SelfRegisteredExtensions", "XunitAutoGeneratedEntryPoint")) ||
// Discard DataSet classes and their nested types
(t.DeriveFrom("System.Data.DataSet".AllowNoMatch()) &&
t.HasAttribute("System.ComponentModel.DesignerCategoryAttribute".AllowNoMatch())) ||
(t.IsNested && t.ParentType != null &&
t.ParentType.DeriveFrom("System.Data.DataSet".AllowNoMatch()) &&
t.ParentType.HasAttribute("System.ComponentModel.DesignerCategoryAttribute".AllowNoMatch())) ||
// Discard DataSet TableAdapterManager classes their nested types
(t.SimpleNameEquals("TableAdapterManager") &&
t.DeriveFrom("System.ComponentModel.Component".AllowNoMatch())) ||
(t.IsNested && t.ParentType != null &&
t.ParentType.SimpleNameEquals("TableAdapterManager") &&
t.ParentType.DeriveFrom("System.ComponentModel.Component".AllowNoMatch())) ||
// Discard Xamarin form generated types that contain the method LoadDataTemplate()
(t.IsNested &&
t.SimpleName.StartsWith("<InitializeComponent>") &&
t.Methods.Count(m => m.SimpleNameEquals("LoadDataTemplate")) == 1 &&
t.ParentAssembly.AssembliesUsed.Count(a => a.Name.StartsWithAny("Xamarin", "Mono.Android")) > 0) ||
// Discard Xamarin Resource types
(t.IsNested &&
t.ParentType != null &&
t.ParentType.Name == "Resource" &&
t.ParentAssembly.AssembliesUsed.Count(a => a.Name.StartsWithAny("Xamarin", "Mono.Android")) > 0) ||
// Discard Entity Framework generated DB context types and entities types used by DB context types!
efContexts.Contains(t) ||
efEntities.Contains(t) ||
efMigrations.Contains(t) ||
efModelSnapshots.Contains(t) ||
// Discard types generated for code contract
t.FullName.StartsWith("System.Diagnostics.Contracts.__ContractsRuntime") ||
t.FullName == "System.Diagnostics.Contracts.RuntimeContractsAttribute" ||
// Discard all types declared in a folder path containing the word "generated"
(t.SourceFileDeclAvailable &&
(t.SourceDecls.All(s =>
s.SourceFile.FilePath.ParentDirectoryPath.ToString()
.IndexOf("generated", StringComparison.OrdinalIgnoreCase) >= 0) ||
t.SourceDecls.FirstOrDefault(s =>
s.SourceFile.FilePathString.EndsWith(
".cshtml", StringComparison.OrdinalIgnoreCase)) != null)) ||
// Discard types declared only in generated source files.
// Typically the default output of Roslyn source generators.
(t.SourceFileDeclAvailable &&
t.SourceDecls.All(s =>
s.SourceFile.FileName.EndsWithAny(
".g.cs", ".g.vb", ".generated.cs", ".generated.vb",
StringComparison.OrdinalIgnoreCase))) ||
// Discard a type with no source declaration, in an assembly that has sources, when its namespace
// holds types tagged with GeneratedCodeAttribute: the output of a Roslyn source generator that does
// not tag all its types and whose generated files are not on disk. An assembly analyzed without
// its PDB files has no source at all: its types are not concerned.
(!t.SourceFileDeclAvailable &&
t.ParentAssembly.SourceFileDeclAvailable &&
t.ParentNamespace.ChildTypes.Any(nt =>
nt.HasAttribute("System.CodeDom.Compiler.GeneratedCodeAttribute".AllowNoMatch()))) ||
// Types created by the test infrastructure
t.FullName == "AutoGeneratedProgram" ||
// Discard special types generated by the Roslyn compiler that are typically used by several methods
// and as a consequence, cannot be merged in application code through the option
// NDepend Project Properties > Analysis > Merge Code Generated by Compiler into Application Code
(t.IsGeneratedByCompiler && t.IsNested && t.SimpleName.StartsWith("<")) ||
// Discard unity generated type
(unityContext && (t.FullNameContainsAny("UnitySourceGenerated","Readme")))
select t
//<Desc>
// A **notmycode** query: matched types (and their methods and fields) are
// removed from the *JustMyCode* code base view used by most default queries
// and rules.
//
// It matches several well-identified generated types, like the ones tagged
// *System.CodeDom.Compiler.GeneratedCodeAttribute*. Enrich it to discard
// your own generated types.
//
// *notmycode* queries run before other queries and rules; modifying one
// re-runs the queries relying on *JustMyCode*. Several *notmycode* types
// queries cumulate. Documentation:
// https://www.ndepend.com/docs/cqlinq-syntax#NotMyCode
//</Desc>
./rules/assets/img/JustMyCode32x32.png
Discard generated and designer Methods from JustMyCode - Defining JustMyCode
A
notmycode query: matched methods are removed from the
JustMyCode code base view used by most default queries and rules.
It matches several well-identified generated methods, like the ones tagged
System.CodeDom.Compiler.GeneratedCodeAttribute or declared in a
.designer.cs source file. Enrich it to discard your own generated methods.
notmycode queries run before other queries and rules; modifying one re-runs the queries relying on
JustMyCode. Several
notmycode methods queries cumulate. Documentation:
https://www.ndepend.com/docs/cqlinq-syntax#NotMyCode
// <Name>Discard generated and designer Methods from JustMyCode</Name>
// <Id>ND3003:DiscardGeneratedAndDesignerMethodsFromJustMyCode</Id>
notmycode
//
// First define source files paths to discard
//
from a in Application.Assemblies
where a.SourceFileDeclAvailable
let asmSourceFilesPaths = a.SourceDecls.Select(s => s.SourceFile.FilePath)
let sourceFilesPathsToDiscard = (
from filePath in asmSourceFilesPaths
let filePathStr = filePath.ToString() where
filePathStr.EndsWithAny(
// Popular pattern to name generated files.
".g.cs",
".g.vb",
".generated.cs",
".generated.vb",
".xaml", // notmycode WPF xaml code
".cshtml", // notmycode Razor markup
".designer.cs", // notmycode C# Windows Forms designer code
".designer.vb", // notmycode VB.NET Windows Forms designer code
StringComparison.OrdinalIgnoreCase)
||
// notmycode methods in source files in a directory containing generated
filePathStr.IndexOf("generated", StringComparison.OrdinalIgnoreCase) >= 0
select filePath
).ToHashSetEx()
//
// Second: discard methods in sourceFilesPathsToDiscard
//
from m in a.ChildMethods
where (m.SourceFileDeclAvailable &&
sourceFilesPathsToDiscard.Contains(m.SourceDecls.First().SourceFile.FilePath)) ||
// Generated methods might be tagged with this attribute
m.HasAttribute ("System.CodeDom.Compiler.GeneratedCodeAttribute".AllowNoMatch()) ||
// This attributes identifies a type or member that is not part of the user code for an application.
m.HasAttribute ("System.Diagnostics.DebuggerNonUserCodeAttribute".AllowNoMatch()) ||
// Event adder/remover methods generated by the compiler.
((m.IsEventAdder || m.IsEventRemover) && !m.SourceFileDeclAvailable) ||
// Delegate type implementation is always generated
m.ParentType.IsDelegate ||
// Default/implicit constructor generated by the compiler on classes that don't have constructor
m.IsDefaultConstructor ||
// Methods of C#9 record types are considered as generated methods
// when they don't have source file decl.
// Doing so avoids some false positive issues on these methods.
(m.ParentType.IsRecord && (!m.SourceFileDeclAvailable || m.IsGeneratedByCompiler ||
m.IsConstructor || m.IsClassConstructor)) ||
// Razor BuildRenderTree() generated method
(m.IsProtected && m.IsVirtual && m.Name == "BuildRenderTree(RenderTreeBuilder)")
select new { m, m.NbLinesOfCode, m.SourceFileDeclAvailable, m.IsGeneratedByCompiler }
//<Desc>
// A **notmycode** query: matched methods are removed from the *JustMyCode*
// code base view used by most default queries and rules.
//
// It matches several well-identified generated methods, like the ones
// tagged *System.CodeDom.Compiler.GeneratedCodeAttribute* or declared in
// a *.designer.cs* source file. Enrich it to discard your own generated
// methods.
//
// *notmycode* queries run before other queries and rules; modifying one
// re-runs the queries relying on *JustMyCode*. Several *notmycode* methods
// queries cumulate. Documentation:
// https://www.ndepend.com/docs/cqlinq-syntax#NotMyCode
//</Desc>
./rules/assets/img/JustMyCode32x32.png
Discard generated Fields from JustMyCode - Defining JustMyCode
A
notmycode query: matched fields are removed from the
JustMyCode code base view used by most default queries and rules.
It matches fields tagged
System.CodeDom.Compiler.GeneratedCodeAttribute and
Windows Form,
WPF and
UWP designer-generated fields. Enrich it to discard your own generated fields.
notmycode queries run before other queries and rules; modifying one re-runs the queries relying on
JustMyCode. Several
notmycode fields queries cumulate. Documentation:
https://www.ndepend.com/docs/cqlinq-syntax#NotMyCode
// <Name>Discard generated Fields from JustMyCode</Name>
// <Id>ND3004:DiscardGeneratedFieldsFromJustMyCode</Id>
notmycode
// Define WindowsForm fields defined as fields assigned by the InitializeComponent() WindowsForm methods.
let winFormInitializeComponentsMethods =
Application.Methods.WithName("InitializeComponent()")
.Where(m => m.ParentType.BaseClass != null && m.ParentType.BaseClass.ParentNamespace.Name == "System.Windows.Forms")
let winFormFields = winFormInitializeComponentsMethods.SelectMany(m => m.FieldsAssigned).ToHashSetEx()
// Types deriving from a System.Web.UI class, computed once instead of once per field
let webUITypes = Application.Types.Where(t => t.BaseClasses.Any(bc => bc.ParentNamespace.Name.StartsWith("System.Web.UI"))).ToHashSetEx()
from a in Application.Assemblies
let asmSourceFilesPaths = a.SourceFileDeclAvailable ?
a.SourceDecls.Select(s => s.SourceFile.FilePath) :
new IAbsoluteFilePath[0]
let sourceFilesPathsToDiscard = (
from filePath in asmSourceFilesPaths
let filePathStr = filePath.ToString() where
filePathStr.EndsWithAny(
// Popular pattern to name generated files.
".g.cs",
".g.vb",
".generated.cs",
".generated.vb",
".xaml", // notmycode WPF xaml code
".cshtml", // notmycode Razor markup
".designer.cs", // notmycode C# Windows Forms designer code
".designer.vb", // notmycode VB.NET Windows Forms designer code
StringComparison.OrdinalIgnoreCase)
||
// notmycode methods in source files in a directory containing generated
filePathStr.IndexOf("generated", StringComparison.OrdinalIgnoreCase) >= 0
select filePath
).ToHashSetEx()
from f in a.ChildFields where
(f.SourceFileDeclAvailable &&
sourceFilesPathsToDiscard.Contains(f.SourceDecls.First().SourceFile.FilePath)) ||
// Discard WindowsForm fields
winFormFields.Contains(f) ||
// Eliminate "components" generated in Windows Form Control context
(f.Name == "components" && f.ParentType.DeriveFrom("System.Windows.Forms.Control".AllowNoMatch())) ||
// Eliminate tagHelper generated fields
f.Name == "_tagHelperStringValueBuffer" ||
// Eliminate XAML generated fields
// IComponentConnector is XAML specific and is automatically implemented for every Window, Page and UserControl.
f.ParentType.Implement( "System.Windows.Markup.IComponentConnector".AllowNoMatch()) || // WPF IComponentConnector
f.ParentType.Implement("Windows.UI.Xaml.Markup.IComponentConnector".AllowNoMatch()) || // UWP IComponentConnector
f.HasAttribute ("System.CodeDom.Compiler.GeneratedCodeAttribute".AllowNoMatch()) ||
// Property backing fields generated by the compiler
(f.IsGeneratedByCompiler && !f.IsEventDelegateObject) ||
// Match fields generated by the ASP.NET infrastructure
// in System.Web.UI classes like Page, Control, MasterPage...
(f.FieldType != null &&
webUITypes.Contains(f.ParentType) &&
(f.FieldType.ParentNamespace.Name.StartsWith("System.Web.UI") ||
f.FieldType.BaseClasses.Any(bc => bc.ParentNamespace.Name.StartsWith("System.Web.UI"))
)) ||
// Delegate type implementation is always generated
f.ParentType.IsDelegate ||
// Match fields named 'mappingSource' for DataContext classes
(f.Name == "mappingSource" && f.ParentType.DeriveFrom("System.Data.Linq.DataContext".AllowNoMatch()))
select f
//<Desc>
// A **notmycode** query: matched fields are removed from the *JustMyCode*
// code base view used by most default queries and rules.
//
// It matches fields tagged *System.CodeDom.Compiler.GeneratedCodeAttribute*
// and *Windows Form*, *WPF* and *UWP* designer-generated fields. Enrich it
// to discard your own generated fields.
//
// *notmycode* queries run before other queries and rules; modifying one
// re-runs the queries relying on *JustMyCode*. Several *notmycode* fields
// queries cumulate. Documentation:
// https://www.ndepend.com/docs/cqlinq-syntax#NotMyCode
//</Desc>
./rules/assets/img/JustMyCode32x32.png
JustMyCode code elements - Defining JustMyCode
Enumerates all assemblies, namespaces, types, methods and fields considered as your code: those not matched by any notmycode query. The ICodeBaseView JustMyCode shows only these elements, so default rules avoid warning on tool-generated code.
// <Name>JustMyCode code elements</Name>
from elem in JustMyCode.CodeElements
select new {
elem,
loc = elem.IsCodeContainer ? elem.AsCodeContainer.NbLinesOfCode : null
}
//<Desc>
// Enumerates all *assemblies*, *namespaces*, *types*, *methods* and
// *fields* considered as your code: those not matched by any **notmycode**
// query. The *ICodeBaseView* **JustMyCode** shows only these elements, so
// default rules avoid warning on tool-generated code.
//</Desc>
./rules/assets/img/JustMyCode32x32.png
NotMyCode code elements - Defining JustMyCode
Enumerates all assemblies, namespaces, types, methods and fields considered as not being your code: those matched by notmycode queries. The ICodeBaseView JustMyCode hides these elements, so default rules avoid warning on tool-generated code.
// <Name>NotMyCode code elements</Name>
from elem in Application.CodeElements.Where(element => !JustMyCode.Contains(element))
select new {
elem,
loc = elem.IsCodeContainer ? elem.AsCodeContainer.NbLinesOfCode : null
}
//<Desc>
// Enumerates all *assemblies*, *namespaces*, *types*, *methods* and
// *fields* considered as not being your code: those matched by
// **notmycode** queries. The *ICodeBaseView* **JustMyCode** hides these
// elements, so default rules avoid warning on tool-generated code.
//</Desc>
./rules/assets/img/Info32x32.png
# New Issues since Baseline - Issues
// <TrendMetric Name="# New Issues since Baseline" Unit="issues"/>
from issue in Issues
where issue.WasAdded()
select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity }
./rules/assets/img/Info32x32.png
# Issues Fixed since Baseline - Issues
// <TrendMetric Name="# Issues Fixed since Baseline" Unit="issues"/>
from issue in Issues
where issue.WasFixed()
select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity }
./rules/assets/img/Info32x32.png
# Issues Worsened since Baseline - Issues
An issue is considered worsened if its
debt increased since the baseline.
Debt documentation:
https://www.ndepend.com/docs/technical-debt#Debt
// <TrendMetric Name="# Issues Worsened since Baseline" Unit="issues"/>
from issue in Issues
where !issue.WasAdded() &&
(issue.DebtDiff() > Debt.Zero || issue.AnnualInterestDiff() > AnnualInterest.Zero)
select new {
issue,
issue.Debt, debtDiff = issue.DebtDiff(),
issue.AnnualInterest, annualInterestDiff = issue.AnnualInterestDiff(),
issue.Severity
}
//<Desc>
// An issue is considered worsened if its *debt* increased since the baseline.
//
// Debt documentation: https://www.ndepend.com/docs/technical-debt#Debt
//</Desc>
./rules/assets/img/Info32x32.png
# Issues with severity Blocker - Issues
A Blocker issue cannot move to production, it must be fixed. Severity is inferred from the issue annual interest and thresholds in Project Properties > Issue and Debt.
// <TrendMetric Name="# Issues with severity Blocker" Unit="issues"/>
from issue in Issues
where issue.Severity == Severity.Blocker
select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity }
//<Desc>
// A **Blocker** issue cannot move to production, it must be fixed.
// Severity is inferred from the issue *annual interest* and thresholds in
// Project Properties > Issue and Debt.
//</Desc>
./rules/assets/img/Info32x32.png
# Issues with severity Critical - Issues
A Critical issue shouldn't move to production (it can for business imperatives) but must be fixed during the next iterations. Severity is inferred from the issue annual interest and thresholds in Project Properties > Issue and Debt.
// <TrendMetric Name="# Issues with severity Critical" Unit="issues"/>
from issue in Issues
where issue.Severity == Severity.Critical
select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity }
//<Desc>
// A **Critical** issue shouldn't move to production (it can for business
// imperatives) but must be fixed during the next iterations.
// Severity is inferred from the issue *annual interest* and thresholds in
// Project Properties > Issue and Debt.
//</Desc>
./rules/assets/img/Info32x32.png
# Issues with severity High - Issues
A High issue should be fixed quickly, but can wait until the next scheduled interval. Severity is inferred from the issue annual interest and thresholds in Project Properties > Issue and Debt.
// <TrendMetric Name="# Issues with severity High" Unit="issues"/>
from issue in Issues
where issue.Severity == Severity.High
select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity }
//<Desc>
// A **High** issue should be fixed quickly, but can wait until the next
// scheduled interval. Severity is inferred from the issue *annual interest*
// and thresholds in Project Properties > Issue and Debt.
//</Desc>
./rules/assets/img/Info32x32.png
# Issues with severity Medium - Issues
A Medium issue is a warning that if not fixed won't significantly impact development. Severity is inferred from the issue annual interest and thresholds in Project Properties > Issue and Debt.
// <TrendMetric Name="# Issues with severity Medium" Unit="issues"/>
from issue in Issues
where issue.Severity == Severity.Medium
select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity }
//<Desc>
// A **Medium** issue is a warning that if not fixed won't significantly
// impact development. Severity is inferred from the issue *annual interest*
// and thresholds in Project Properties > Issue and Debt.
//</Desc>
./rules/assets/img/Info32x32.png
# Issues with severity Low - Issues
Low severity is for issues with zero or near-zero
Annual Interest.
Low and
Medium issues are small elegance improvements — the
broken windows of a code base, per the
Broken Window Theory https://en.wikipedia.org/wiki/Broken_windows_theory: left unfixed, developers stop caring for elegant code, costing extra maintenance long term. Severity is inferred from the issue
annual interest and thresholds in Project Properties > Issue and Debt.
// <TrendMetric Name="# Issues with severity Low" Unit="issues"/>
from issue in Issues
where issue.Severity == Severity.Low
select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity }
//<Desc>
// **Low** severity is for issues with zero or near-zero **Annual
// Interest**. *Low* and *Medium* issues are small elegance improvements —
// the *broken windows* of a code base, per the **Broken Window Theory**
// https://en.wikipedia.org/wiki/Broken_windows_theory: left unfixed,
// developers stop caring for elegant code, costing extra maintenance long
// term. Severity is inferred from the issue *annual interest* and
// thresholds in Project Properties > Issue and Debt.
//</Desc>
./rules/assets/img/Info32x32.png
# Blocker/Critical/High Issues - Issues
Number of Blocker, Critical or High issues. A Blocker issue cannot move to production. A Critical issue shouldn't (it can for business imperatives) but must be fixed during the next iterations. A High issue should be fixed quickly, but can wait until the next scheduled interval.
// <TrendMetric Name="# Blocker/Critical/High Issues" Unit="issues"/>
from issue in Issues
where issue.Severity.EqualsAny(Severity.Blocker, Severity.Critical, Severity.High)
select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity }
//<Desc>
// Number of Blocker, Critical or High issues. A **Blocker** issue cannot
// move to production. A **Critical** issue shouldn't (it can for business
// imperatives) but must be fixed during the next iterations. A **High**
// issue should be fixed quickly, but can wait until the next scheduled
// interval.
//</Desc>
./rules/assets/img/Info32x32.png
# Issues - Issues
The number of issues no matter the issue severity.
// <TrendMetric Name="# Issues" Unit="issues"/>
from issue in Issues
select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity }
//<Desc>
// The number of issues no matter the issue severity.
//</Desc>
./rules/assets/img/Info32x32.png
# Suppressed Issues - Issues
Number of issues suppressed via SuppressMessage. Documentation:
https://www.ndepend.com/docs/suppress-issues
// <TrendMetric Name="# Suppressed Issues" Unit="issues"/>
from issue in context.IssuesSet.AllSuppressedIssues
select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity }
//<Desc>
// Number of issues suppressed via SuppressMessage. Documentation:
// https://www.ndepend.com/docs/suppress-issues
//</Desc>
./rules/assets/img/Info32x32.png
# Rules - Rules
Counts active rules: violated or not, critical or not. Without a baseline, rules relying on diff are not counted, which can explain an unexpected slight decrease of this count.
// <TrendMetric Name="# Rules" Unit="rules"/>
from rule in Rules
select new {
rule,
issues = rule.Issues(),
debt = rule.Debt(),
annualInterest = rule.AnnualInterest(),
maxSeverity = rule.IsViolated() && rule.Issues().Any() ?
(Severity?)rule.Issues().Max(i => i.Severity) : null
}
//<Desc>
// Counts active rules: violated or not, critical or not. Without a
// baseline, rules relying on diff are not counted, which can explain an
// unexpected slight decrease of this count.
//</Desc>
./rules/assets/img/Info32x32.png
# Rules Violated - Rules
Counts violated active rules, critical or not. Without a baseline, rules relying on diff are not counted, which can explain an unexpected slight decrease of this count.
// <TrendMetric Name="# Rules Violated" Unit="rules"/>
from rule in Rules
where rule.IsViolated()
select new {
rule,
issues = rule.Issues(),
debt = rule.Debt(),
annualInterest = rule.AnnualInterest(),
maxSeverity = rule.IsViolated() && rule.Issues().Any() ?
(Severity?)rule.Issues().Max(i => i.Severity) : null
}
//<Desc>
// Counts violated active rules, critical or not. Without a baseline, rules
// relying on diff are not counted, which can explain an unexpected slight
// decrease of this count.
//</Desc>
./rules/assets/img/Info32x32.png
# Critical Rules Violated - Rules
Counts violated critical active rules. Critical rules pinpoint rules that should not be violated. Without a baseline, rules relying on diff are not counted, which can explain an unexpected slight decrease of this count.
// <TrendMetric Name="# Critical Rules Violated" Unit="rules"/>
from rule in Rules
where rule.IsViolated() && rule.IsCritical
select new {
rule,
issues = rule.Issues(),
debt = rule.Debt(),
annualInterest = rule.AnnualInterest(),
maxSeverity = rule.IsViolated() && rule.Issues().Any() ?
(Severity?)rule.Issues().Max(i => i.Severity) : null
}
//<Desc>
// Counts violated critical active rules. Critical rules pinpoint rules that
// should not be violated. Without a baseline, rules relying on diff are not
// counted, which can explain an unexpected slight decrease of this count.
//</Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
# Quality Gates - Quality Gates
Counts active quality gates, whatever their status (Pass, Warn, Fail). Without a baseline, gates relying on diff are not counted, which can explain an unexpected slight decrease of this count.
// <TrendMetric Name="# Quality Gates" Unit="quality gates"/>
from qualityGate in QualityGates
select new {
qualityGate ,
qualityGate.ValueString,
qualityGate.Status,
}
//<Desc>
// Counts active quality gates, whatever their status (Pass, Warn, Fail).
// Without a baseline, gates relying on diff are not counted, which can
// explain an unexpected slight decrease of this count.
//</Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
# Quality Gates Warn - Quality Gates
Counts active quality gates that warn. Without a baseline, gates relying on diff are not counted, which can explain an unexpected slight decrease of this count.
// <TrendMetric Name="# Quality Gates Warn" Unit="quality gates"/>
from qualityGate in QualityGates
where qualityGate.Warn
select new {
qualityGate ,
qualityGate.ValueString,
}
//<Desc>
// Counts active quality gates that warn. Without a baseline, gates relying
// on diff are not counted, which can explain an unexpected slight decrease
// of this count.
//</Desc>
./rules/assets/img/QualityGateStatusPass32x32.png
# Quality Gates Fail - Quality Gates
Counts active quality gates that fail. Without a baseline, gates relying on diff are not counted, which can explain an unexpected slight decrease of this count.
// <TrendMetric Name="# Quality Gates Fail" Unit="quality gates"/>
from qualityGate in QualityGates
where qualityGate.Fail
select new {
qualityGate ,
qualityGate.ValueString,
}
//<Desc>
// Counts active quality gates that fail. Without a baseline, gates relying
// on diff are not counted, which can explain an unexpected slight decrease
// of this count.
//</Desc>
./rules/assets/img/Info32x32.png
Percentage Debt (Metric) - Debt
Name suffixed with (Metric) to avoid collision with the same-named Quality Gate.
Ratio of estimated total time to fix all issues (the Debt) over estimated total time to develop the code base (inferred from # lines of code and the
man-day to develop 1000 logical lines of code setting in Project Properties > Issue and Debt). Debt documentation:
https://www.ndepend.com/docs/technical-debt#Debt
// <TrendMetric Name="Percentage Debt (Metric)" Unit="%" />
// Effort to develop the application code only: test code (the TestCode view) is not counted
let timeToDev = TimeSpan.FromMinutes(Application.Assemblies.Sum(a => (a.EffortToDevelop() ?? TimeSpan.Zero).TotalMinutes))
let debt = Issues.Sum(i => i.Debt)
select 100d * debt.ToManDay() / timeToDev.ToManDay()
// <Desc>
// Name suffixed with (Metric) to avoid collision with the same-named
// Quality Gate.
//
// Ratio of estimated total time to fix all issues (the Debt) over estimated
// total time to develop the code base (inferred from # lines of code and
// the *man-day to develop 1000 logical lines of code* setting in Project
// Properties > Issue and Debt).
// Debt documentation: https://www.ndepend.com/docs/technical-debt#Debt
// </Desc>
./rules/assets/img/Info32x32.png
Debt (Metric) - Debt
Name suffixed with (Metric) to avoid collision with the same-named Quality Gate. Debt documentation:
https://www.ndepend.com/docs/technical-debt#Debt
// <TrendMetric Name="Debt (Metric)" Unit="man-days" />
Issues.Sum(i => i.Debt).ToManDay()
//<Desc>
// Name suffixed with (Metric) to avoid collision with the same-named
// Quality Gate.
// Debt documentation: https://www.ndepend.com/docs/technical-debt#Debt
//</Desc>
./rules/assets/img/Info32x32.png
New Debt since Baseline (Metric) - Debt
Name suffixed with (Metric) to avoid collision with the same-named Quality Gate. Debt added (or fixed if negative) since baseline. Debt documentation:
https://www.ndepend.com/docs/technical-debt#Debt
// <TrendMetric Name="New Debt since Baseline (Metric)" Unit="man-days" />
let debt = Issues.Sum(i => i.Debt)
let debtInBaseline = IssuesInBaseline.Sum(i => i.Debt)
select (debt - debtInBaseline).ToManDay()
//<Desc>
// Name suffixed with (Metric) to avoid collision with the same-named
// Quality Gate. Debt added (or fixed if negative) since baseline.
// Debt documentation: https://www.ndepend.com/docs/technical-debt#Debt
//</Desc>
./rules/assets/img/Info32x32.png
Annual Interest (Metric) - Debt
Name suffixed with (Metric) to avoid collision with the same-named Quality Gate. Annual Interest documentation:
https://www.ndepend.com/docs/technical-debt#AnnualInterest
// <TrendMetric Name="Annual Interest (Metric)" Unit="man-days" />
Issues.Sum(i => i.AnnualInterest).ToManDay()
//<Desc>
// Name suffixed with (Metric) to avoid collision with the same-named
// Quality Gate.
// Annual Interest documentation: https://www.ndepend.com/docs/technical-debt#AnnualInterest
//</Desc>
./rules/assets/img/Info32x32.png
New Annual Interest since Baseline (Metric) - Debt
Name suffixed with (Metric) to avoid collision with the same-named Quality Gate. Annual Interest added (or fixed if negative) since baseline. Annual Interest documentation:
https://www.ndepend.com/docs/technical-debt#AnnualInterest
// <TrendMetric Name="New Annual Interest since Baseline (Metric)" Unit="man-days" />
let ai = Issues.Sum(i => i.AnnualInterest)
let aiInBaseline = IssuesInBaseline.Sum(i => i.AnnualInterest)
select (ai - aiInBaseline).ToManDay()
//<Desc>
// Name suffixed with (Metric) to avoid collision with the same-named
// Quality Gate. Annual Interest added (or fixed if negative) since baseline.
// Annual Interest documentation: https://www.ndepend.com/docs/technical-debt#AnnualInterest
//</Desc>
./rules/assets/img/Info32x32.png
Breaking Point - Debt
The
breaking point of a set of issues is the
debt (estimated cost-to-fix) divided by the
annual interest (estimated yearly cost-to-
not-fix): the point in time when not fixing costs as much as fixing. Documentation:
https://www.ndepend.com/docs/technical-debt#BreakingPoint
// <TrendMetric Name="Breaking Point" Unit="years"/>
(Issues.Sum(i =>i.Debt).BreakingPoint(Issues.Sum(i =>i.AnnualInterest))).TotalYears()
// <Desc>
// The **breaking point** of a set of issues is the **debt** (estimated
// cost-to-fix) divided by the **annual interest** (estimated yearly
// cost-to-**not**-fix): the point in time when not fixing costs as much as
// fixing. Documentation:
// https://www.ndepend.com/docs/technical-debt#BreakingPoint
// </Desc>
./rules/assets/img/Info32x32.png
Breaking Point of Blocker / Critical / High Issues - Debt
The
breaking point of a set of issues is the
debt (estimated cost-to-fix) divided by the
annual interest (estimated yearly cost-to-
not-fix): the point in time when not fixing costs as much as fixing. Documentation:
https://www.ndepend.com/docs/technical-debt#BreakingPoint
// <TrendMetric Name="Breaking Point of Blocker / Critical / High Issues" Unit="years"/>
let issues = Issues.Where(i => i.Severity.EqualsAny(Severity.Blocker, Severity.Critical, Severity.High))
select (issues.Sum(i =>i.Debt).BreakingPoint(issues.Sum(i =>i.AnnualInterest))).TotalYears()
// <Desc>
// The **breaking point** of a set of issues is the **debt** (estimated
// cost-to-fix) divided by the **annual interest** (estimated yearly
// cost-to-**not**-fix): the point in time when not fixing costs as much as
// fixing. Documentation:
// https://www.ndepend.com/docs/technical-debt#BreakingPoint
// </Desc>
./rules/assets/img/Info32x32.png
# Lines of Code - Code Size
// <TrendMetric Name="# Lines of Code" Unit="LoC" />
// Application code only: test code (the TestCode view) is not counted
Application.Assemblies.Sum(a => a.NbLinesOfCode)
./rules/assets/img/Info32x32.png
# Lines of Code (JustMyCode) - Code Size
// <TrendMetric Name="# Lines of Code (JustMyCode)" Unit="LoC" />
JustMyCode.Methods.Sum(m => m.NbLinesOfCode)
// JustMyCode is defined by code queries prefixed with 'notmycode'
// in the group 'Defining JustMyCode'.
./rules/assets/img/Info32x32.png
# Lines of Code (NotMyCode) - Code Size
// <TrendMetric Name="# Lines of Code (NotMyCode)" Unit="LoC" />
Application.Methods.Except(JustMyCode.Methods).Sum(m => m.NbLinesOfCode)
// JustMyCode is defined by code queries prefixed with 'notmycode'
// in the group 'Defining JustMyCode'.
./rules/assets/img/Info32x32.png
# Lines of Code Added since the Baseline - Code Size
// <TrendMetric Name="# Lines of Code Added since the Baseline" Unit="LoC" />
from a in Application.Assemblies
let nbLocAdded = !a.IsPresentInBothBuilds()
? a.NbLinesOfCode
: (a.NbLinesOfCode != null && a.OlderVersion().NbLinesOfCode != null)
? a.NbLinesOfCode - (int)a.OlderVersion().NbLinesOfCode
: 0
select (double?)nbLocAdded
// A value is computed by this Trend Metric query
// only if a Baseline for Comparison is provided.
// See Project Properties > Baseline > Baseline for Comparison
./rules/assets/img/Info32x32.png
# Source Files - Code Size
Counts source files. A value of 0 means PDB files were unavailable at analysis time:
https://www.ndepend.com/docs/ndepend-analysis-inputs-explanation
Source files can't be matched by a query, but editing "Application Types" with
Group by source file declarations lists them with types declarations.
// <TrendMetric Name="# Source Files" Unit="Source Files" />
Application.Assemblies.SelectMany(
a => a.SourceDecls.Select(sd => sd.SourceFile.FilePathString))
.Distinct(StringComparer.OrdinalIgnoreCase)
.Count()
//<Desc>
// Counts source files. A value of 0 means PDB files were unavailable at
// analysis time:
// https://www.ndepend.com/docs/ndepend-analysis-inputs-explanation
//
// Source files can't be matched by a query, but editing "Application Types"
// with *Group by source file declarations* lists them with types
// declarations.
//</Desc>
./rules/assets/img/Info32x32.png
# Line Feed - Code Size
Counts line feeds in the application source files: the
physical lines of code, while
# Lines of Code counts
logical lines of code (see
https://www.ndepend.com/docs/code-metrics#NbLinesOfCode). Source files declaring test code are not counted.
// <TrendMetric Name="# Line Feed" Unit="LF" />
// Application source files only: files declaring test code (the TestCode view) are not counted
codeBase.SourceFiles.Where(f => !f.CodeElements.Any(ce => ce.IsTestCode)).Sum(f => f.NbLines)
//<Desc>
// Counts line feeds in the application source files: the **physical lines
// of code**, while **# Lines of Code** counts **logical lines of code** (see
// https://www.ndepend.com/docs/code-metrics#NbLinesOfCode). Source files
// declaring test code are not counted.
//</Desc>
./rules/assets/img/Info32x32.png
# IL Instructions - Code Size
// <TrendMetric Name="# IL Instructions" Unit="IL Instructions" />
// Application code only: test code (the TestCode view) is not counted
Application.Assemblies.Sum(a => a.NbILInstructions)
./rules/assets/img/Info32x32.png
# IL Instructions (NotMyCode) - Code Size
// <TrendMetric Name="# IL Instructions (NotMyCode)" Unit="IL Instructions" />
Application.Methods.Except(JustMyCode.Methods).Sum(m => m.NbILInstructions)
// JustMyCode is defined by code queries prefixed with 'notmycode'
// in the group 'Defining JustMyCode'.
./rules/assets/img/Info32x32.png
# Assemblies - Code Size
Counts all application assemblies, showing per assembly the all technical-debt and issues (of the assembly and its child namespaces, types and members).
// <TrendMetric Name="# Assemblies" Unit="Assemblies" />
from a in Application.Assemblies
select new {
a,
Debt = a.AllDebt(),
Issues = a.AllIssues(),
a.NbLinesOfCode
}
//<Desc>
// Counts all application assemblies, showing per assembly the **all**
// technical-debt and issues (of the assembly and its child namespaces,
// types and members).
//</Desc>
./rules/assets/img/Info32x32.png
# Namespaces - Code Size
Counts all application namespaces, showing per namespace the all technical-debt and issues (of the namespace and its child types and members).
// <TrendMetric Name="# Namespaces" Unit="Namespaces" />
from n in Application.Namespaces
select new {
n,
Debt = n.AllDebt(),
Issues = n.AllIssues(),
n.NbLinesOfCode
}
//<Desc>
// Counts all application namespaces, showing per namespace the **all**
// technical-debt and issues (of the namespace and its child types and
// members).
//</Desc>
./rules/assets/img/Info32x32.png
# Types - Code Size
Counts all non-compiler-generated application types, showing per type the all technical-debt and issues (of the type and its child members).
// <TrendMetric Name="# Types" Unit="Types" />
from t in Application.Types.Where(t => !t.IsGeneratedByCompiler ||
// C# Top Level Statement type is generated by compiler but still part of Application types
t.FullNameEqualsAny("Program", "<Program>$"))
select new {
t,
Debt = t.AllDebt(),
Issues = t.AllIssues(),
t.NbLinesOfCode
}
//<Desc>
// Counts all non-compiler-generated application types, showing per type the
// **all** technical-debt and issues (of the type and its child members).
//</Desc>
./rules/assets/img/Info32x32.png
# Public Types - Code Size
// <TrendMetric Name="# Public Types" Unit="Types" />
Application.Types.Where(t => t.IsPubliclyVisible && !t.IsGeneratedByCompiler)
./rules/assets/img/Info32x32.png
# Classes - Code Size
// <TrendMetric Name="# Classes" Unit="Types" />
Application.Types.Where(t => t.IsClass && !t.IsGeneratedByCompiler)
./rules/assets/img/Info32x32.png
# Abstract Classes - Code Size
// <TrendMetric Name="# Abstract Classes" Unit="Types" />
Application.Types.Where(t => t.IsClass && t.IsAbstract && !t.IsGeneratedByCompiler)
./rules/assets/img/Info32x32.png
# Interfaces - Code Size
// <TrendMetric Name="# Interfaces" Unit="Types" />
Application.Types.Where(t => t.IsInterface)
./rules/assets/img/Info32x32.png
# Structures - Code Size
// <TrendMetric Name="# Structures" Unit="Types" />
Application.Types.Where(t => t.IsStructure && !t.IsGeneratedByCompiler)
./rules/assets/img/Info32x32.png
# Methods - Code Size
Counts all non-compiler-generated application methods, showing per method the technical-debt and issues.
// <TrendMetric Name="# Methods" Unit="Methods" />
from m in Application.Methods.Where(m => !m.IsGeneratedByCompiler ||
// C# Top Level Statement method is generated by compiler but still part of Application types
m.Name.StartsWith("<Main>$"))
select new {
m,
Debt = m.Debt(),
Issues = m.Issues(),
m.NbLinesOfCode
}
//<Desc>
// Counts all non-compiler-generated application methods, showing per method
// the technical-debt and issues.
//</Desc>
./rules/assets/img/Info32x32.png
# Abstract Methods - Code Size
// <TrendMetric Name="# Abstract Methods" Unit="Methods" />
Application.Methods.Where(m => m.IsAbstract)
./rules/assets/img/Info32x32.png
# Concrete Methods - Code Size
// <TrendMetric Name="# Concrete Methods" Unit="Methods" />
Application.Methods.Where(m => !m.IsAbstract && !m.IsGeneratedByCompiler)
./rules/assets/img/Info32x32.png
# Fields - Code Size
Counts all non-compiler-generated application fields that are neither enumeration values nor constants, showing per field the technical-debt and issues.
// <TrendMetric Name="# Fields" Unit="Fields" />
from f in Application.Fields.Where(f =>
!f.IsEnumValue &&
!f.ParentType.IsEnumeration &&
!f.IsGeneratedByCompiler &&
!f.IsLiteral)
select new {
f,
Debt = f.AllDebt(),
Issues = f.AllIssues()
}
//<Desc>
// Counts all non-compiler-generated application fields that are neither
// enumeration values nor constants, showing per field the technical-debt
// and issues.
//</Desc>
./rules/assets/img/Info32x32.png
Max # Lines of Code for Methods (JustMyCode) - Maximum and Average
// <TrendMetric Name="Max # Lines of Code for Methods (JustMyCode)" Unit="LoC" />
JustMyCode.Methods
.Max(m => m.NbLinesOfCode)
// Here is the code query to get the (JustMyCode) method with largest # Lines of Code
// JustMyCode.Methods.OrderByDescending(m => m.NbLinesOfCode).Take(1).Select(m => new {m, m.NbLinesOfCode})
./rules/assets/img/Info32x32.png
Average # Lines of Code for Methods - Maximum and Average
// <TrendMetric Name="Average # Lines of Code for Methods" Unit="LoC" />
Application.Methods.Where(m => m.NbLinesOfCode > 0)
.Average(m => m.NbLinesOfCode)
./rules/assets/img/Info32x32.png
Average # Lines of Code for Methods with at least 3 Lines of Code - Maximum and Average
// <TrendMetric Name="Average # Lines of Code for Methods with at least 3 Lines of Code" Unit="LoC" />
Application.Methods.Where(m => m.NbLinesOfCode >= 3)
.Average(m => m.NbLinesOfCode)
./rules/assets/img/Info32x32.png
Max # Lines of Code for Types (JustMyCode) - Maximum and Average
// <TrendMetric Name="Max # Lines of Code for Types (JustMyCode)" Unit="LoC" />
JustMyCode.Types
.Max(t => t.NbLinesOfCode)
// Here is the code query to get the (JustMyCode) type with largest # Lines of Code
// JustMyCode.Types.OrderByDescending(t => t.NbLinesOfCode).Take(1).Select(t => new {t, t.NbLinesOfCode})
./rules/assets/img/Info32x32.png
Average # Lines of Code for Types - Maximum and Average
// <TrendMetric Name="Average # Lines of Code for Types" Unit="LoC" />
Application.Types.Where(t => t.NbLinesOfCode > 0)
.Average(t => t.NbLinesOfCode)
./rules/assets/img/Info32x32.png
Max Cyclomatic Complexity for Methods - Maximum and Average
// <TrendMetric Name="Max Cyclomatic Complexity for Methods" Unit="Paths" />
Application.Methods
.Max(m => m.CyclomaticComplexity)
// Here is the code query to get the most complex method, according to Cyclomatic Complexity
// Application.Methods.OrderByDescending(m => m.CyclomaticComplexity).Take(1).Select(m => new {m, m.CyclomaticComplexity})
./rules/assets/img/Info32x32.png
Average Cyclomatic Complexity for Methods - Maximum and Average
// <TrendMetric Name="Average Cyclomatic Complexity for Methods" Unit="Paths" />
Application.Methods.Where(m => m.NbLinesOfCode> 0)
.Average(m => m.CyclomaticComplexity)
./rules/assets/img/Info32x32.png
Max IL Cyclomatic Complexity for Methods - Maximum and Average
// <TrendMetric Name="Max IL Cyclomatic Complexity for Methods" Unit="Paths" />
Application.Methods
.Max(m => m.ILCyclomaticComplexity)
// Here is the code query to get the most complex method, according to Cyclomatic Complexity computed from IL code.
// Application.Methods.OrderByDescending(m => m.ILCyclomaticComplexity).Take(1).Select(m => new {m, m.CyclomaticComplexity})
./rules/assets/img/Info32x32.png
Average IL Cyclomatic Complexity for Methods - Maximum and Average
// <TrendMetric Name="Average IL Cyclomatic Complexity for Methods" Unit="Paths" />
Application.Methods.Where(m => m.NbILInstructions> 0)
.Average(m => m.ILCyclomaticComplexity)
./rules/assets/img/Info32x32.png
Max IL Nesting Depth for Methods - Maximum and Average
// <TrendMetric Name="Max IL Nesting Depth for Methods" Unit="Scopes" />
Application.Methods
.Max(m => m.ILNestingDepth)
// Here is the code query to get the method with higher ILNestingDepth.
// Application.Methods.OrderByDescending(m => m.ILNestingDepth).Take(1).Select(m => new {m, m.ILNestingDepth})
./rules/assets/img/Info32x32.png
Average IL Nesting Depth for Methods - Maximum and Average
// <TrendMetric Name="Average IL Nesting Depth for Methods" Unit="Scopes" />
Application.Methods.Where(m => m.NbILInstructions> 0)
.Average(m => m.ILNestingDepth)
./rules/assets/img/Info32x32.png
Max # of Methods for Types - Maximum and Average
// <TrendMetric Name="Max # of Methods for Types" Unit="Methods" />
Application.Types
.Max(t => t.NbMethods)
// Here is the code query to get the (JustMyCode) type with largest # of Methods
// JustMyCode.Types.OrderByDescending(t => t.NbMethods).Take(1).Select(t => new {t, t.Methods})
./rules/assets/img/Info32x32.png
Average # Methods for Types - Maximum and Average
// <TrendMetric Name="Average # Methods for Types" Unit="Methods" />
Application.Types.Average(t => t.NbMethods)
./rules/assets/img/Info32x32.png
Max # of Methods for Interfaces - Maximum and Average
// <TrendMetric Name="Max # of Methods for Interfaces" Unit="Methods" />
Application.Types.Where(t => t.IsInterface)
.Max(t => t.NbMethods)
// Here is the code query to get the (JustMyCode) type with largest # of Methods
// JustMyCode.Types.OrderByDescending(t => t.NbMethods).Take(1).Select(t => new {t, t.Methods})
./rules/assets/img/Info32x32.png
Average # Methods for Interfaces - Maximum and Average
// <TrendMetric Name="Average # Methods for Interfaces" Unit="Methods" />
JustMyCode.Types.Where(t => t.IsInterface)
.Average(t => t.NbMethods)
./rules/assets/img/Info32x32.png
Percentage Code Coverage - Coverage
// <TrendMetric Name="Percentage Code Coverage" Unit="%" />
// Coverage of the application code only: test code (the TestCode view) is not counted
let covered = (double)Application.Assemblies.Sum(a => a.NbLinesOfCodeCovered ?? 0)
let notCovered = (double)Application.Assemblies.Sum(a => a.NbLinesOfCodeNotCovered ?? 0)
select (covered + notCovered) == 0d ? (double?)null : 100d * covered / (covered + notCovered)
./rules/assets/img/Info32x32.png
# Lines of Code Covered - Coverage
// <TrendMetric Name="# Lines of Code Covered" Unit="LoC" />
// Application code only: test code (the TestCode view) is not counted
Application.Assemblies.Sum(a => a.NbLinesOfCodeCovered)
./rules/assets/img/Info32x32.png
# Lines of Code Not Covered - Coverage
// <TrendMetric Name="# Lines of Code Not Covered" Unit="LoC" />
// Application code only: test code (the TestCode view) is not counted
Application.Assemblies.Sum(a => a.NbLinesOfCodeNotCovered)
./rules/assets/img/Info32x32.png
# Tests - Coverage
Number of test methods: methods of test assemblies (the TestCode view) tagged with a test attribute of a supported test framework (xUnit, NUnit, MSTest, TUnit…).
// <TrendMetric Name="# Tests" Unit="Tests" />
from m in TestCode.Methods
where m.IsTestMethod
select new { m, m.ParentType, m.NbLinesOfCode }
//<Desc>
// Number of test methods: methods of test assemblies (the **TestCode** view)
// tagged with a test attribute of a supported test framework (xUnit, NUnit,
// MSTest, TUnit…).
//</Desc>
./rules/assets/img/Info32x32.png
# Lines of Code Uncoverable - Coverage
Number of lines of code Uncoverable: lines in methods without coverage data in the imported file plus methods explicitly excluded from coverage — via a .runsettings file (Project Properties > Code Coverage > Settings > Exclusion Coverage File) or an Uncoverable attribute (typically ExcludeFromCodeCoverageAttribute; custom ones definable in Settings > Un-Coverable attributes; with VS coverage, DebuggerNonUserCodeAttribute and DebuggerHiddenAttribute count too).
If coverage data is not in-sync with the code base, methods missing from the data are also listed.
// <TrendMetric Name="# Lines of Code Uncoverable" Unit="LoC" />
Application.Methods.Where(m => !m.CoverageDataAvailable).Sum(m => m.NbLinesOfCode).ToNullableDouble()
//<Desc>
// Number of **lines of code Uncoverable**: lines in methods without
// coverage data in the imported file plus methods explicitly excluded from
// coverage — via a *.runsettings* file (*Project Properties > Code Coverage >
// Settings > Exclusion Coverage File*) or an *Uncoverable
// attribute* (typically *ExcludeFromCodeCoverageAttribute*; custom ones
// definable in *Settings > Un-Coverable attributes*; with VS coverage,
// *DebuggerNonUserCodeAttribute* and *DebuggerHiddenAttribute* count too).
//
// If coverage data is not *in-sync* with the code base, methods missing
// from the data are also listed.
//</Desc>
./rules/assets/img/Info32x32.png
# Lines of Code in Types 100% Covered - Coverage
A covered line is even more valuable in a type 100% covered. Covering 90% of a class is not enough: the 10% uncovered is hard-to-test, hence not well-designed, hence error-prone. Better test error-prone code, isn't it?
// <TrendMetric Name="# Lines of Code in Types 100% Covered" Unit="LoC" />
Application.Types.Where(t => t.PercentageCoverage == 100)
.Sum(t => t.NbLinesOfCodeCovered)
//<Desc>
// A covered line is *even more valuable* in a type 100% covered. Covering
// 90% of a class is not enough: the 10% uncovered is hard-to-test, hence
// not well-designed, hence error-prone. Better test error-prone code,
// isn't it?
//</Desc>
./rules/assets/img/Info32x32.png
# Lines of Code in Methods 100% Covered - Coverage
Same remark as # Lines of Code in Types 100% Covered: a covered line is even more valuable in a method 100% covered.
// <TrendMetric Name="# Lines of Code in Methods 100% Covered" Unit="LoC" />
Application.Methods.Where(m => m.PercentageCoverage == 100)
.Sum(m => m.NbLinesOfCodeCovered)
//<Desc>
// Same remark as **# Lines of Code in Types 100% Covered**: a covered line
// is *even more valuable* in a method 100% covered.
//</Desc>
./rules/assets/img/Info32x32.png
Max C.R.A.P Score - Coverage
Change Risk Analyzer and Predictor (CRAP) pinpoints overly complex and untested code (
http://www.artima.com/weblogs/viewpost.jsp?thread=215899). Formula:
CRAP(m) = CC(m)^2 * (1 – cov(m)/100)^3 + CC(m), with
CC(m) the
cyclomatic complexity and
cov(m) the
percentage coverage. The higher the score, the more painful and error prone the method. Threshold 30 as suggested by the inventors; methods under 10 lines of code are skipped. To list methods with high scores see the rule
Test and Code Coverage >
C.R.A.P method code metric.
// <TrendMetric Name="Max C.R.A.P Score" />
(from m in JustMyCode.Methods
// Don't match too short methods
where m.NbLinesOfCode > 10
let CC = m.CyclomaticComplexity
let uncov = (100 - m.PercentageCoverage) / 100f
let CRAP = (CC * CC * uncov * uncov * uncov) + CC
where CRAP != null && CRAP > 30 select CRAP)
.Max(CRAP => CRAP)
//<Desc>
// **Change Risk Analyzer and Predictor** (CRAP) pinpoints overly complex
// and untested code (http://www.artima.com/weblogs/viewpost.jsp?thread=215899).
// Formula: **CRAP(m) = CC(m)^2 * (1 – cov(m)/100)^3 + CC(m)**, with *CC(m)*
// the *cyclomatic complexity* and *cov(m)* the *percentage coverage*.
// The higher the score, the more painful and error prone the method.
// Threshold 30 as suggested by the inventors; methods under 10 lines of
// code are skipped. To list methods with high scores see the rule
// *Test and Code Coverage* > *C.R.A.P method code metric*.
//</Desc>
./rules/assets/img/Info32x32.png
Average C.R.A.P Score - Coverage
Change Risk Analyzer and Predictor (CRAP) pinpoints overly complex and untested code (
http://www.artima.com/weblogs/viewpost.jsp?thread=215899). Formula:
CRAP(m) = CC(m)^2 * (1 – cov(m)/100)^3 + CC(m), with
CC(m) the
cyclomatic complexity and
cov(m) the
percentage coverage. The higher the score, the more painful and error prone the method. Threshold 30 as suggested by the inventors; methods under 10 lines of code are skipped. To list methods with high scores see the rule
Test and Code Coverage >
C.R.A.P method code metric.
// <TrendMetric Name="Average C.R.A.P Score" />
(from m in JustMyCode.Methods
// Don't match too short methods
where m.NbLinesOfCode > 10
let CC = m.CyclomaticComplexity
let uncov = (100 - m.PercentageCoverage) / 100f
let CRAP = (CC * CC * uncov * uncov * uncov) + CC
where CRAP != null && CRAP > 30 select CRAP)
.Average(CRAP => CRAP)
//<Desc>
// **Change Risk Analyzer and Predictor** (CRAP) pinpoints overly complex
// and untested code (http://www.artima.com/weblogs/viewpost.jsp?thread=215899).
// Formula: **CRAP(m) = CC(m)^2 * (1 – cov(m)/100)^3 + CC(m)**, with *CC(m)*
// the *cyclomatic complexity* and *cov(m)* the *percentage coverage*.
// The higher the score, the more painful and error prone the method.
// Threshold 30 as suggested by the inventors; methods under 10 lines of
// code are skipped. To list methods with high scores see the rule
// *Test and Code Coverage* > *C.R.A.P method code metric*.
//</Desc>
./rules/assets/img/Info32x32.png
Percentage JustMyCode Coverage - Coverage
// <TrendMetric Name="Percentage JustMyCode Coverage" Unit="%" />
let justMyCodeLinesCovered = (float)JustMyCode.Methods.Sum(m => m.NbLinesOfCodeCovered)
let justMyCodeLines = (float)JustMyCode.Methods.Sum(m => m.NbLinesOfCode)
select 100f* justMyCodeLinesCovered / justMyCodeLines
// JustMyCode explanations: https://www.ndepend.com/docs/cqlinq-syntax#NotMyCode
./rules/assets/img/Info32x32.png
# Lines of JustMyCode Covered - Coverage
// <TrendMetric Name="# Lines of JustMyCode Covered" Unit="LoC" />
JustMyCode.Methods.Sum(m => m.NbLinesOfCodeCovered)
// JustMyCode explanations: https://www.ndepend.com/docs/cqlinq-syntax#NotMyCode
./rules/assets/img/Info32x32.png
# Lines of JustMyCode Not Covered - Coverage
// <TrendMetric Name="# Lines of JustMyCode Not Covered" Unit="LoC" />
JustMyCode.Methods.Sum(m => m.NbLinesOfCodeNotCovered)
// JustMyCode explanations: https://www.ndepend.com/docs/cqlinq-syntax#NotMyCode
./rules/assets/img/Info32x32.png
# Third-Party Assemblies Used - Third-Party Usage
// <TrendMetric Name="# Third-Party Assemblies Used" Unit="Assemblies" />
from a in ThirdParty.Assemblies
select new { a, a.AssembliesUsingMe }
./rules/assets/img/Info32x32.png
# Third-Party Namespaces Used - Third-Party Usage
// <TrendMetric Name="# Third-Party Namespaces Used" Unit="Namespaces" />
from n in ThirdParty.Namespaces
select new { n, n.NamespacesUsingMe }
./rules/assets/img/Info32x32.png
# Third-Party Types Used - Third-Party Usage
// <TrendMetric Name="# Third-Party Types Used" Unit="Types" />
from t in ThirdParty.Types
select new { t, t.TypesUsingMe }
./rules/assets/img/Info32x32.png
# Third-Party Methods Used - Third-Party Usage
// <TrendMetric Name="# Third-Party Methods Used" Unit="Methods" />
from m in ThirdParty.Methods
select new { m, m.MethodsCallingMe }
./rules/assets/img/Info32x32.png
# Third-Party Fields Used - Third-Party Usage
// <TrendMetric Name="# Third-Party Fields Used" Unit="Fields" />
from f in ThirdParty.Fields
where !f.ParentType.IsEnumeration
select new { f, f.MethodsUsingMe }
./rules/assets/img/Info32x32.png
# Third-Party Code Elements Used - Third-Party Usage
// <TrendMetric Name="# Third-Party Code Elements Used" Unit="Code Elements" />
from elem in ThirdParty.CodeElements
where !(elem.IsField && elem.AsField.ParentType.IsEnumeration)
let users = elem.IsMethod ? elem.AsMethod.MethodsCallingMe.Cast<ICodeElement>() :
elem.IsField ? elem.AsField.MethodsUsingMe.Cast<ICodeElement>() :
elem.IsType ? elem.AsType.TypesUsingMe.Cast<ICodeElement>() :
elem.IsNamespace ? elem.AsNamespace.NamespacesUsingMe.Cast<ICodeElement>() :
elem.AsAssembly.AssembliesUsingMe.Cast<ICodeElement>()
select new { elem, users }
./rules/assets/img/Info32x32.png
New assemblies - Code Diff Summary
Runs only in diff mode (baseline defined). Lists assemblies added since the baseline.
// <Name>New assemblies</Name>
from a in Application.Assemblies where a.WasAdded()
select new { a, a.NbLinesOfCode }
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists *assemblies* added
// since the *baseline*.
//</Desc>
./rules/assets/img/Info32x32.png
Assemblies removed - Code Diff Summary
Runs only in diff mode (baseline defined). Lists assemblies removed since the baseline.
// <Name>Assemblies removed</Name>
from a in codeBase.OlderVersion().Application.Assemblies where a.WasRemoved()
select new { a, a.NbLinesOfCode }
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists *assemblies* removed
// since the *baseline*.
//</Desc>
./rules/assets/img/Info32x32.png
Assemblies where code was changed - Code Diff Summary
Runs only in diff mode (baseline defined). Lists assemblies whose code changed since the baseline.
// <Name>Assemblies where code was changed</Name>
from a in Application.Assemblies where a.CodeWasChanged()
select new {
a,
a.NbLinesOfCode,
oldNbLinesOfCode = a.OlderVersion().NbLinesOfCode.GetValueOrDefault() ,
delta = (int) a.NbLinesOfCode.GetValueOrDefault() - a.OlderVersion().NbLinesOfCode.GetValueOrDefault()
}
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists *assemblies* whose
// code changed since the *baseline*.
//</Desc>
./rules/assets/img/Info32x32.png
New namespaces - Code Diff Summary
Runs only in diff mode (baseline defined). Lists namespaces added since the baseline.
// <Name>New namespaces</Name>
from n in Application.Namespaces where
!n.ParentAssembly.WasAdded() &&
n.WasAdded()
select new { n, n.NbLinesOfCode }
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists *namespaces* added
// since the *baseline*.
//</Desc>
./rules/assets/img/Info32x32.png
Namespaces removed - Code Diff Summary
Runs only in diff mode (baseline defined). Lists namespaces removed since the baseline.
// <Name>Namespaces removed</Name>
from n in codeBase.OlderVersion().Application.Namespaces where
!n.ParentAssembly.WasRemoved() &&
n.WasRemoved()
select new { n, n.NbLinesOfCode }
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists *namespaces* removed
// since the *baseline*.
//</Desc>
./rules/assets/img/Info32x32.png
Namespaces where code was changed - Code Diff Summary
Runs only in diff mode (baseline defined). Lists namespaces whose code changed since the baseline.
// <Name>Namespaces where code was changed</Name>
from n in Application.Namespaces where n.CodeWasChanged()
select new {
n,
n.NbLinesOfCode,
oldNbLinesOfCode = n.OlderVersion().NbLinesOfCode.GetValueOrDefault() ,
delta = (int) n.NbLinesOfCode.GetValueOrDefault() - n.OlderVersion().NbLinesOfCode.GetValueOrDefault()
}
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists *namespaces* whose
// code changed since the *baseline*.
//</Desc>
./rules/assets/img/Info32x32.png
New types - Code Diff Summary
Runs only in diff mode (baseline defined). Lists types added since the baseline.
// <Name>New types</Name>
from t in Application.Types where
!t.ParentNamespace.WasAdded() &&
t.WasAdded() &&
!t.IsGeneratedByCompiler
select new { t, t.NbLinesOfCode }
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists *types* added since
// the *baseline*.
//</Desc>
./rules/assets/img/Info32x32.png
Types removed - Code Diff Summary
Runs only in diff mode (baseline defined). Lists types removed since the baseline.
// <Name>Types removed</Name>
from t in codeBase.OlderVersion().Application.Types where
!t.ParentNamespace.WasRemoved() &&
t.WasRemoved() &&
!t.IsGeneratedByCompiler
select new { t, t.NbLinesOfCode }
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists *types* removed since
// the *baseline*.
//</Desc>
./rules/assets/img/Info32x32.png
Types where code was changed - Code Diff Summary
Runs only in diff mode (baseline defined). Lists types whose code changed since the baseline.
To visualize changes, right-click a matched type and select:
- Compare older and newer versions of source file
- Compare older and newer versions decompiled with Reflector
// <Name>Types where code was changed</Name>
from t in Application.Types where t.CodeWasChanged()
//select new { t, t.NbLinesOfCode }
select new {
t,
t.NbLinesOfCode,
oldNbLinesOfCode = t.OlderVersion().NbLinesOfCode ,
delta = (int?) t.NbLinesOfCode - t.OlderVersion().NbLinesOfCode
}
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists *types* whose code
// changed since the *baseline*.
//
// To visualize changes, right-click a matched type and select:
//
// - Compare older and newer versions of source file
//
// - Compare older and newer versions decompiled with Reflector
//</Desc>
./rules/assets/img/Info32x32.png
Heuristic to find types moved from one namespace or assembly to another - Code Diff Summary
Runs only in diff mode (baseline defined). Lists types moved between namespaces or assemblies, via a join LINQ query on type name (without namespace prefix) over the types added and removed sets.
// <Name>Heuristic to find types moved from one namespace or assembly to another</Name>
let typesRemoved = codeBase.OlderVersion().Application.Types.Where(t => t.WasRemoved())
let typesAdded = Application.Types.Where(t => t.WasAdded())
from tMoved in typesAdded.Join(
typesRemoved,
t => t.Name,
t => t.Name,
(tNewer, tOlder) => new { tNewer,
OlderParentNamespace = tOlder.ParentNamespace,
OlderParentAssembly = tOlder.ParentAssembly } )
select tMoved
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists *types* moved between
// namespaces or assemblies, via a **join LINQ query** on type name (without
// namespace prefix) over the types *added* and *removed* sets.
//</Desc>
./rules/assets/img/Info32x32.png
Types directly using one or several types changed - Code Diff Summary
Runs only in diff mode (baseline defined). Lists unchanged types directly using types whose code changed since the baseline: their code didn't change but their behavior might have. The result includes the changed types used.
// <Name>Types directly using one or several types changed</Name>
let typesChanged = Application.Types.Where(t => t.CodeWasChanged()).ToHashSetEx()
from t in JustMyCode.Types.UsingAny(typesChanged) where
!t.CodeWasChanged() &&
!t.WasAdded()
let typesChangedUsed = t.TypesUsed.Intersect(typesChanged)
select new { t, typesChangedUsed }
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists *unchanged* types
// directly using *types* whose code changed since the *baseline*: their
// code didn't change but their behavior might have. The result includes the
// changed types used.
//</Desc>
./rules/assets/img/Info32x32.png
Types indirectly using one or several types changed - Code Diff Summary
Runs only in
diff mode (baseline defined). Lists
unchanged types
using directly or indirectly types whose code changed since the
baseline: their code didn't change but their behavior might have. The result includes the changed types directly used and the
depth of usage, as defined for the
DepthOfIsUsingAny() NDepend API method:
https://www.ndepend.com/api/webframe.html#NDepend.API~NDepend.CodeModel.ExtensionMethodsSequenceUsage~DepthOfIsUsingAny.html
// <Name>Types indirectly using one or several types changed</Name>
let typesChanged = Application.Types.Where(t => t.CodeWasChanged()).ToHashSetEx()
// 'depth' represents a code metric defined on types using
// directly or indirectly any type where code was changed.
let depth = JustMyCode.Types.DepthOfIsUsingAny(typesChanged)
from t in depth.DefinitionDomain where
!t.CodeWasChanged() &&
!t.WasAdded()
let typesChangedDirectlyUsed = t.TypesUsed.Intersect(typesChanged)
let depthOfUsingTypesChanged = depth[t]
orderby depthOfUsingTypesChanged
select new {
t,
depthOfUsingTypesChanged,
typesChangedDirectlyUsed
}
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists *unchanged* types
// **using directly or indirectly** *types* whose code changed since the
// *baseline*: their code didn't change but their behavior might have. The
// result includes the changed types directly used and the **depth of
// usage**, as defined for the *DepthOfIsUsingAny()* NDepend API method:
// https://www.ndepend.com/api/webframe.html#NDepend.API~NDepend.CodeModel.ExtensionMethodsSequenceUsage~DepthOfIsUsingAny.html
//</Desc>
./rules/assets/img/Info32x32.png
New methods - Code Diff Summary
Runs only in diff mode (baseline defined). Lists methods added since the baseline.
// <Name>New methods</Name>
from m in Application.Methods where
!m.ParentType.WasAdded() &&
m.WasAdded() &&
!m.IsGeneratedByCompiler
select new { m, m.NbLinesOfCode }
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists *methods* added since
// the *baseline*.
//</Desc>
./rules/assets/img/Info32x32.png
Methods removed - Code Diff Summary
Runs only in diff mode (baseline defined). Lists methods removed since the baseline.
// <Name>Methods removed</Name>
from m in codeBase.OlderVersion().Application.Methods where
!m.ParentType.WasRemoved() &&
m.WasRemoved() &&
!m.IsGeneratedByCompiler
select new { m, m.NbLinesOfCode }
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists *methods* removed
// since the *baseline*.
//</Desc>
./rules/assets/img/Info32x32.png
Methods where code was changed - Code Diff Summary
Runs only in diff mode (baseline defined). Lists methods whose code changed since the baseline.
To visualize changes, right-click a matched method and select:
- Compare older and newer versions of source file
- Compare older and newer versions decompiled with Reflector
// <Name>Methods where code was changed</Name>
from m in Application.Methods where m.CodeWasChanged()
select new {
m,
m.NbLinesOfCode,
oldNbLinesOfCode = m.OlderVersion().NbLinesOfCode ,
delta = (int?) m.NbLinesOfCode - m.OlderVersion().NbLinesOfCode
}
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists *methods* whose code
// changed since the *baseline*.
//
// To visualize changes, right-click a matched method and select:
//
// - Compare older and newer versions of source file
//
// - Compare older and newer versions decompiled with Reflector
//</Desc>
./rules/assets/img/Info32x32.png
Methods directly calling one or several methods changed - Code Diff Summary
Runs only in diff mode (baseline defined). Lists unchanged methods directly calling methods whose code changed since the baseline: their code didn't change but their behavior might have. The result includes the changed methods called.
// <Name>Methods directly calling one or several methods changed</Name>
let methodsChanged = Application.Methods.Where(m => m.CodeWasChanged()).ToHashSetEx()
from m in JustMyCode.Methods.UsingAny(methodsChanged ) where
!m.CodeWasChanged() &&
!m.WasAdded()
let methodsChangedCalled = m.MethodsCalled.Intersect(methodsChanged)
select new {
m,
methodsChangedCalled
}
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists *unchanged* methods
// directly calling *methods* whose code changed since the *baseline*: their
// code didn't change but their behavior might have. The result includes the
// changed methods called.
//</Desc>
./rules/assets/img/Info32x32.png
Methods indirectly calling one or several methods changed - Code Diff Summary
Runs only in
diff mode (baseline defined). Lists
unchanged methods
using directly or indirectly methods whose code changed since the
baseline: their code didn't change but their behavior might have. The result includes the changed methods directly used and the
depth of usage, as defined for the
DepthOfIsUsingAny() NDepend API method:
https://www.ndepend.com/api/webframe.html#NDepend.API~NDepend.CodeModel.ExtensionMethodsSequenceUsage~DepthOfIsUsingAny.html
// <Name>Methods indirectly calling one or several methods changed</Name>
let methodsChanged = Application.Methods.Where(m => m.CodeWasChanged()).ToHashSetEx()
// 'depth' represents a code metric defined on methods using
// directly or indirectly any method where code was changed.
let depth = JustMyCode.Methods.DepthOfIsUsingAny(methodsChanged)
from m in depth.DefinitionDomain where
!m.CodeWasChanged() &&
!m.WasAdded()
let methodsChangedDirectlyUsed = m.MethodsCalled.Intersect(methodsChanged)
let depthOfUsingMethodsChanged = depth[m]
orderby depthOfUsingMethodsChanged
select new {
m,
depthOfUsingMethodsChanged,
methodsChangedDirectlyUsed
}
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists *unchanged* methods
// **using directly or indirectly** *methods* whose code changed since the
// *baseline*: their code didn't change but their behavior might have. The
// result includes the changed methods directly used and the **depth of
// usage**, as defined for the *DepthOfIsUsingAny()* NDepend API method:
// https://www.ndepend.com/api/webframe.html#NDepend.API~NDepend.CodeModel.ExtensionMethodsSequenceUsage~DepthOfIsUsingAny.html
//</Desc>
./rules/assets/img/Info32x32.png
New fields - Code Diff Summary
Runs only in diff mode (baseline defined). Lists fields added since the baseline.
// <Name>New fields</Name>
from f in Application.Fields where
!f.ParentType.WasAdded() &&
f.WasAdded() &&
!f.IsGeneratedByCompiler
select f
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists *fields* added since
// the *baseline*.
//</Desc>
./rules/assets/img/Info32x32.png
Fields removed - Code Diff Summary
Runs only in diff mode (baseline defined). Lists fields removed since the baseline.
// <Name>Fields removed</Name>
from f in codeBase.OlderVersion().Application.Fields where
!f.ParentType.WasRemoved() &&
f.WasRemoved() &&
!f.IsGeneratedByCompiler
select f
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists *fields* removed since
// the *baseline*.
//</Desc>
./rules/assets/img/Info32x32.png
Third party types that were not used and that are now used - Code Diff Summary
Runs only in diff mode (baseline defined). Lists third-party types not used at baseline time and now used.
// <Name>Third party types that were not used and that are now used</Name>
from t in ThirdParty.Types where t.IsUsedRecently()
select new {
t,
t.Methods,
t.Fields,
t.TypesUsingMe
}
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists **third-party** *types*
// not used at *baseline* time and now used.
//</Desc>
./rules/assets/img/Info32x32.png
Third party types that were used and that are not used anymore - Code Diff Summary
Runs only in diff mode (baseline defined). Lists third-party types used at baseline time and no longer used.
// <Name>Third party types that were used and that are not used anymore</Name>
from t in codeBase.OlderVersion().ThirdParty.Types where t.IsNotUsedAnymore()
select new {
t,
t.Methods,
t.Fields,
TypesThatUsedMe = t.TypesUsingMe
}
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists **third-party** *types*
// used at *baseline* time and no longer used.
//</Desc>
./rules/assets/img/Info32x32.png
Third party methods that were not used and that are now used - Code Diff Summary
Runs only in diff mode (baseline defined). Lists third-party methods not used at baseline time and now used.
// <Name>Third party methods that were not used and that are now used</Name>
from m in ThirdParty.Methods where
m.IsUsedRecently() &&
!m.ParentType.IsUsedRecently()
select new {
m,
m.MethodsCallingMe
}
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists **third-party**
// *methods* not used at *baseline* time and now used.
//</Desc>
./rules/assets/img/Info32x32.png
Third party methods that were used and that are not used anymore - Code Diff Summary
Runs only in diff mode (baseline defined). Lists third-party methods used at baseline time and no longer used.
// <Name>Third party methods that were used and that are not used anymore</Name>
from m in codeBase.OlderVersion().ThirdParty.Methods where
m.IsNotUsedAnymore() &&
!m.ParentType.IsNotUsedAnymore()
select new {
m,
MethodsThatCalledMe = m.MethodsCallingMe
}
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists **third-party**
// *methods* used at *baseline* time and no longer used.
//</Desc>
./rules/assets/img/Info32x32.png
Third party fields that were not used and that are now used - Code Diff Summary
Runs only in diff mode (baseline defined). Lists third-party fields not used at baseline time and now used.
// <Name>Third party fields that were not used and that are now used</Name>
from f in ThirdParty.Fields where
f.IsUsedRecently() &&
!f.ParentType.IsUsedRecently()
select new {
f,
f.MethodsUsingMe
}
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists **third-party**
// *fields* not used at *baseline* time and now used.
//</Desc>
./rules/assets/img/Info32x32.png
Third party fields that were used and that are not used anymore - Code Diff Summary
Runs only in diff mode (baseline defined). Lists third-party fields used at baseline time and no longer used.
// <Name>Third party fields that were used and that are not used anymore</Name>
from f in codeBase.OlderVersion().ThirdParty.Fields where
f.IsNotUsedAnymore() &&
!f.ParentType.IsNotUsedAnymore()
select new {
f,
MethodsThatUsedMe = f.MethodsUsingMe
}
//<Desc>
// Runs only in *diff mode* (baseline defined). Lists **third-party**
// *fields* used at *baseline* time and no longer used.
//</Desc>
./rules/assets/img/Info32x32.png
Most used types (Rank) - Statistics
Lists the 100 application types with the highest
TypeRank, computed by the
Google PageRank algorithm on the types dependency graph: types used by many types themselves having many users. Metric:
https://www.ndepend.com/docs/code-metrics#TypeRank
Any change to a much-used type
impacts most clients: painful. So high-
TypeRank types should preferably be
interfaces, less subject to change, and shielding clients from implementation details —
in essence the
Liskov Substitution Principle:
http://en.wikipedia.org/wiki/Liskov_substitution_principle
// <Name>Most used types (Rank)</Name>
(from t in Application.Types
where !t.IsGeneratedByCompiler
orderby t.Rank descending
select new { t, t.Rank, t.TypesUsingMe }).Take(100)
//<Desc>
// Lists the 100 application types with the highest **TypeRank**, computed
// by the **Google PageRank** algorithm on the types dependency graph: types
// used by many types themselves having many users. Metric:
// https://www.ndepend.com/docs/code-metrics#TypeRank
//
// Any change to a much-used type **impacts** most clients: painful. So
// high-*TypeRank* types should preferably be **interfaces**, less subject
// to change, and shielding clients from implementation details — *in
// essence* the **Liskov Substitution Principle**:
// http://en.wikipedia.org/wiki/Liskov_substitution_principle
//</Desc>
./rules/assets/img/Info32x32.png
Most used methods (Rank) - Statistics
Lists the 100 application methods with the highest
MethodRank, computed by the
Google PageRank algorithm on the methods dependency graph: methods called by many methods themselves having many callers. Metric:
https://www.ndepend.com/docs/code-metrics#MethodRank
Any change to a much-called method
impacts most callers: painful. So high-
MethodRank methods should preferably be
abstract methods, less subject to signature changes, and shielding callers from implementation details —
in essence the
Liskov Substitution Principle:
http://en.wikipedia.org/wiki/Liskov_substitution_principle
// <Name>Most used methods (Rank)</Name>
(from m in Application.Methods
where !m.IsGeneratedByCompiler
orderby m.Rank descending
select new { m, m.Rank, m.MethodsCallingMe }).Take(100)
//<Desc>
// Lists the 100 application methods with the highest **MethodRank**,
// computed by the **Google PageRank** algorithm on the methods dependency
// graph: methods called by many methods themselves having many callers.
// Metric: https://www.ndepend.com/docs/code-metrics#MethodRank
//
// Any change to a much-called method **impacts** most callers: painful. So
// high-*MethodRank* methods should preferably be **abstract methods**, less
// subject to signature changes, and shielding callers from implementation
// details — *in essence* the **Liskov Substitution Principle**:
// http://en.wikipedia.org/wiki/Liskov_substitution_principle
//</Desc>
./rules/assets/img/Info32x32.png
Most used assemblies (#AssembliesUsingMe) - Statistics
This code query lists the 100 application and third-party assemblies, with the higher number of assemblies users.
// <Name>Most used assemblies (#AssembliesUsingMe)</Name>
(from a in Assemblies orderby a.AssembliesUsingMe.Count() descending
select new { a, a.AssembliesUsingMe }).Take(100)
//<Desc>
// This code query lists the 100 *application* and *third-party*
// assemblies, with the higher number of assemblies users.
//</Desc>
./rules/assets/img/Info32x32.png
Most used namespaces (#NamespacesUsingMe ) - Statistics
This code query lists the 100 application and third-party namespaces, with the higher number of namespaces users.
// <Name>Most used namespaces (#NamespacesUsingMe )</Name>
(from n in Namespaces orderby n.NbNamespacesUsingMe descending
select new { n, n.NamespacesUsingMe }).Take(100)
//<Desc>
// This code query lists the 100 *application* and *third-party*
// namespaces, with the higher number of namespaces users.
//</Desc>
./rules/assets/img/Info32x32.png
Most used types (#TypesUsingMe ) - Statistics
This code query lists the 100 application and third-party types, with the higher number of types users.
// <Name>Most used types (#TypesUsingMe )</Name>
(from t in Types orderby t.NbTypesUsingMe descending
where !t.IsGeneratedByCompiler
select new { t, t.TypesUsingMe }).Take(100)
//<Desc>
// This code query lists the 100 *application* and *third-party*
// types, with the higher number of types users.
//</Desc>
./rules/assets/img/Info32x32.png
Most used methods (#MethodsCallingMe ) - Statistics
This code query lists the 100 application and third-party methods, with the higher number of methods callers.
// <Name>Most used methods (#MethodsCallingMe )</Name>
(from m in Methods orderby m.NbMethodsCallingMe
where !m.IsGeneratedByCompiler
select new { m, m.MethodsCallingMe }).Take(100)
//<Desc>
// This code query lists the 100 *application* and *third-party*
// methods, with the higher number of methods callers.
//</Desc>
./rules/assets/img/Info32x32.png
Namespaces that use many other namespaces (#NamespacesUsed ) - Statistics
This code query lists the 100 application namespaces with the higher number of namespaces used.
// <Name>Namespaces that use many other namespaces (#NamespacesUsed )</Name>
(from n in Application.Namespaces orderby n.NbNamespacesUsed descending
select new { n, n.NamespacesUsed }).Take(100)
//<Desc>
// This code query lists the 100 *application* namespaces
// with the higher number of namespaces used.
//</Desc>
./rules/assets/img/Info32x32.png
Types that use many other types (#TypesUsed ) - Statistics
This code query lists the 100 application types with the higher number of types used.
// <Name>Types that use many other types (#TypesUsed )</Name>
(from t in Application.Types orderby t.NbTypesUsed descending
select new { t, t.TypesUsed, isMyCode = JustMyCode.Contains(t) }).Take(100)
//<Desc>
// This code query lists the 100 *application* types
// with the higher number of types used.
//</Desc>
./rules/assets/img/Info32x32.png
Methods that use many other methods (#MethodsCalled ) - Statistics
This code query lists the 100 application methods with the higher number of methods called.
// <Name>Methods that use many other methods (#MethodsCalled )</Name>
(from m in Application.Methods orderby m.NbMethodsCalled descending
select new { m, m.MethodsCalled, isMyCode = JustMyCode.Contains(m) }).Take(100)
//<Desc>
// This code query lists the 100 *application* methods
// with the higher number of methods called.
//</Desc>
./rules/assets/img/Info32x32.png
High-level to low-level assemblies (Level) - Statistics
This code query lists assemblies ordered by
Level values. See the definition of the
AssemblyLevel metric here:
https://www.ndepend.com/docs/code-metrics#Level
// <Name>High-level to low-level assemblies (Level)</Name>
from a in Application.Assemblies orderby a.Level descending
select new { a, a.Level }
//<Desc>
// This code query lists assemblies ordered by **Level** values.
// See the definition of the *AssemblyLevel* metric here:
// https://www.ndepend.com/docs/code-metrics#Level
//</Desc>
./rules/assets/img/Info32x32.png
High-level to low-level namespaces (Level) - Statistics
This code query lists namespaces ordered by
Level values. See the definition of the
NamespaceLevel metric here:
https://www.ndepend.com/docs/code-metrics#Level
// <Name>High-level to low-level namespaces (Level)</Name>
from n in Application.Namespaces orderby n.Level descending
select new { n, n.Level }
//<Desc>
// This code query lists namespaces ordered by **Level** values.
// See the definition of the *NamespaceLevel* metric here:
// https://www.ndepend.com/docs/code-metrics#Level
//</Desc>
./rules/assets/img/Info32x32.png
High-level to low-level types (Level) - Statistics
This code query lists types ordered by
Level values. See the definition of the
TypeLevel metric here:
https://www.ndepend.com/docs/code-metrics#Level
// <Name>High-level to low-level types (Level)</Name>
from t in Application.Types orderby t.Level descending
select new { t, t.Level }
//<Desc>
// This code query lists types ordered by **Level** values.
// See the definition of the *TypeLevel* metric here:
// https://www.ndepend.com/docs/code-metrics#Level
//</Desc>
./rules/assets/img/Info32x32.png
High-level to low-level methods (Level) - Statistics
This code query lists methods ordered by
Level values. See the definition of the
MethodLevel metric here:
https://www.ndepend.com/docs/code-metrics#Level
// <Name>High-level to low-level methods (Level)</Name>
from m in Application.Methods orderby m.Level descending
select new { m, m.Level }
//<Desc>
// This code query lists methods ordered by **Level** values.
// See the definition of the *MethodLevel* metric here:
// https://www.ndepend.com/docs/code-metrics#Level
//</Desc>
./rules/assets/img/StatusBigOk32x32.png
Check that the assembly Asm1 is not using the assembly Asm2 - Check Architecture
A sample rule to adapt to your need: warns if a particular assembly uses another particular assembly.
Generate such a rule for assemblies A and B by right-clicking the Dependency Matrix cell (B in row, A in column) or the Dependency Graph arrow from A to B, then clicking Generate a code rule that warns if this dependency exists. Adapt the generated rule to your needs.
How to Fix:
This is a sample rule, there is nothing to fix as is.
// <Name>Check that the assembly Asm1 is not using the assembly Asm2</Name>
// <Id>ND9901:ExplicitId9901</Id>
warnif count > 0 from a in Application.Assemblies where
a.IsUsing ("Asm2".AllowNoMatch().MatchAssembly()) &&
(a.Name == @"Asm1")
select new {
a,
Debt = 30.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* shouldn't use the *assembly Asm2* because *your reason*.
//</Expl>
//<Desc>
// A *sample rule to adapt to your need*: warns if a particular assembly
// uses another particular assembly.
//
// Generate such a rule for assemblies **A** and **B** by right-clicking the
// *Dependency Matrix* cell (**B** in row, **A** in column) or the
// *Dependency Graph* arrow from **A** to **B**, then clicking
// **Generate a code rule that warns if this dependency exists**.
// Adapt the generated rule to your needs.
//</Desc>
//<HowToFix>
// This is a *sample rule*, there is nothing to fix *as is*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Check that the namespace N1.N2 is not using the namespace N3.N4.N5 - Check Architecture
A sample rule to adapt to your need: warns if a particular namespace uses another particular namespace.
Generate such a rule for namespaces A and B by right-clicking the Dependency Matrix cell (B in row, A in column) or the Dependency Graph arrow from A to B, then clicking Generate a code rule that warns if this dependency exists. Adapt the generated rule to your needs.
How to Fix:
This is a sample rule, there is nothing to fix as is.
// <Name>Check that the namespace N1.N2 is not using the namespace N3.N4.N5</Name>
// <Id>ND9902:ExplicitId9902</Id>
warnif count > 0 from n in Application.Namespaces where
n.IsUsing ("N3.N4.N5".AllowNoMatch().MatchNamespace()) &&
(n.Name == @"N1.N2")
select new {
n,
Debt = 30.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* shouldn't use the namespace *N3.N4.N5* because *put your reason*.
//</Expl>
//<Desc>
// A *sample rule to adapt to your need*: warns if a particular namespace
// uses another particular namespace.
//
// Generate such a rule for namespaces **A** and **B** by right-clicking the
// *Dependency Matrix* cell (**B** in row, **A** in column) or the
// *Dependency Graph* arrow from **A** to **B**, then clicking
// **Generate a code rule that warns if this dependency exists**.
// Adapt the generated rule to your needs.
//</Desc>
//<HowToFix>
// This is a *sample rule*, there is nothing to fix *as is*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Check that the assembly Asm1 is only using the assemblies Asm2, Asm3 or System.Runtime - Check Architecture
A sample rule to adapt to your need: enforces that a particular assembly is only using a particular set of assemblies.
How to Fix:
This is a sample rule, there is nothing to fix as is.
// <Name>Check that the assembly Asm1 is only using the assemblies Asm2, Asm3 or System.Runtime</Name>
// <Id>ND9903:ExplicitId9903</Id>
warnif count > 0 from a in Application.Assemblies where
( !a.IsUsing ("Asm2".AllowNoMatch().MatchAssembly()) ||
!a.IsUsing ("Asm3".AllowNoMatch().MatchAssembly()) ||
!a.IsUsing ("System.Runtime".MatchAssembly()) ||
a.AssembliesUsed.Count() != 3) // Must not be used more than 3 assemblies
&&
(a.Name == @"Asm1")
select new {
a,
a.AssembliesUsed,
Debt = 30.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* is using the *{1}* but should only use the assemblies *Asm2*, *Asm3* or *System.Runtime*.
//</Expl>
//<Desc>
// A *sample rule to adapt to your need*: enforces that a particular
// assembly **is only using** a particular set of assemblies.
//</Desc>
//<HowToFix>
// This is a *sample rule*, there is nothing to fix *as is*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Check that the namespace N1.N2 is only using the namespaces N3.N4, N5 or System - Check Architecture
A sample rule to adapt to your need: enforces that a particular namespace is only using a particular set of namespaces.
How to Fix:
This is a sample rule, there is nothing to fix as is.
// <Name>Check that the namespace N1.N2 is only using the namespaces N3.N4, N5 or System</Name>
// <Id>ND9904:ExplicitId9904</Id>
warnif count > 0 from n in Application.Namespaces where
( !n.IsUsing("N3.N4".AllowNoMatch().MatchNamespace()) ||
!n.IsUsing("N5".AllowNoMatch().MatchNamespace()) ||
!n.IsUsing("System".MatchNamespace()) ||
n.NamespacesUsed.Count() != 3) // Must not be used more than 3 assemblies
// AsmCe = Efferent Coupling for assembly
&&
(n.Name == @"N1.N2")
select new {
n,
n.NamespacesUsed,
Debt = 30.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* is using the *{1}* but should only use the assemblies *N3.N4, N5* or *System*.
//</Expl>
//<Desc>
// A *sample rule to adapt to your need*: enforces that a particular
// namespace **is only using** a particular set of namespaces.
//</Desc>
//<HowToFix>
// This is a *sample rule*, there is nothing to fix *as is*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Check that AsmABC is the only assembly that is using System.Collections.Concurrent - Check Architecture
A sample rule to adapt to your need: enforces that a particular assembly is only used by another particular assembly.
How to Fix:
This is a sample rule, there is nothing to fix as is.
// <Name>Check that AsmABC is the only assembly that is using System.Collections.Concurrent</Name>
// <Id>ND9905:ExplicitId9905</Id>
warnif count> 0 from a in Application.Assemblies where
a.IsUsing ("System.Collections.Concurrent".AllowNoMatch().MatchAssembly()) &&
!(a.Name == @"AsmABC")
select new {
a,
Debt = 30.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* is using *System.Collections.Concurrent* but only the *assembly AsmABC* should use it.
//</Expl>
//<Desc>
// A *sample rule to adapt to your need*: enforces that a particular
// assembly is **only used by** another particular assembly.
//</Desc>
//<HowToFix>
// This is a *sample rule*, there is nothing to fix *as is*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Check that only 3 assemblies are using System.Collections.Concurrent - Check Architecture
A sample rule to adapt to your need: enforces that a particular assembly is only used by any 3 other assemblies.
How to Fix:
This is a sample rule, there is nothing to fix as is.
// <Name>Check that only 3 assemblies are using System.Collections.Concurrent</Name>
// <Id>ND9906:ExplicitId9906</Id>
warnif count != 3 from a in Application.Assemblies where
a.IsUsing ("System.Collections.Concurrent".AllowNoMatch().MatchAssembly())
select new {
a,
Debt = 30.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* are using *System.Collections.Concurrent* but only 3 assemblies should be using it.
//</Expl>
//<Desc>
// A *sample rule to adapt to your need*: enforces that a particular
// assembly is **only used by** any 3 other assemblies.
//</Desc>
//<HowToFix>
// This is a *sample rule*, there is nothing to fix *as is*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Check that all methods that call Foo.Fct1() also call Foo.Fct2(Int32) - Check Architecture
A sample rule to adapt to your need: enforces that a method calling a particular method must also call another particular method.
How to Fix:
This is a sample rule, there is nothing to fix as is.
// <Name>Check that all methods that call Foo.Fct1() also call Foo.Fct2(Int32)</Name>
// <Id>ND9907:ExplicitId9907</Id>
warnif count > 0 from m in Application.Methods where
m.IsUsing ("Foo.Fct1()".AllowNoMatch()) &&
!m.IsUsing ("Foo.Fct2(Int32)".AllowNoMatch())
select new {
m,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* calls *Foo.Fct1()* and thus, should also call *Foo.Fct2(Int32)* because *your reason**.
//</Expl>
//<Desc>
// A *sample rule to adapt to your need*: enforces that a method calling a
// particular method must also call another particular method.
//</Desc>
//<HowToFix>
// This is a *sample rule*, there is nothing to fix *as is*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Check that all types that derive from Foo, also implement IFoo - Check Architecture
A sample rule to adapt to your need: enforces that all classes deriving from a particular base class also implement a particular interface.
How to Fix:
This is a sample rule, there is nothing to fix as is.
// <Name>Check that all types that derive from Foo, also implement IFoo</Name>
// <Id>ND9908:ExplicitId9908</Id>
warnif count > 0 from t in Application.Types where
t.DeriveFrom ("Foo".AllowNoMatch().MatchType()) &&
!t.Implement ("IFoo".AllowNoMatch().MatchType())
select new {
t,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* derives from *Foo* and should also implement *IFoo* because *your reason*.
//</Expl>
//<Desc>
// A *sample rule to adapt to your need*: enforces that all classes deriving
// from a particular base class also implement a particular interface.
//</Desc>
//<HowToFix>
// This is a *sample rule*, there is nothing to fix *as is*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Check that all types that has the attribute FooAttribute are declared in the namespace N1.N2* - Check Architecture
A sample rule to adapt to your need: enforces that all types tagged with a particular attribute are declared in a particular namespace.
How to Fix:
This is a sample rule, there is nothing to fix as is.
// <Name>Check that all types that has the attribute FooAttribute are declared in the namespace N1.N2*</Name>
// <Id>ND9909:ExplicitId9909</Id>
warnif count > 0 from t in
Application.Namespaces.WithNameWildcardMatchNotIn("N1.N2*").ChildTypes()
where
t.HasAttribute ("FooAttribute".AllowNoMatch())
select new {
t,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* has the attribute *FooAttribute* and thus should be declared in namespace *N1.N2* because *your reason*.
//</Expl>
//<Desc>
// A *sample rule to adapt to your need*: enforces that all types tagged
// with a particular attribute are declared in a particular namespace.
//</Desc>
//<HowToFix>
// This is a *sample rule*, there is nothing to fix *as is*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Check that all synchronization objects are only used from the namespaces under MyNamespace.Sync - Check Architecture
A sample rule to adapt to your need: enforces that all synchronization objects are used from a particular namespace.
How to Fix:
This is a sample rule, there is nothing to fix as is.
// <Name>Check that all synchronization objects are only used from the namespaces under MyNamespace.Sync</Name>
// <Id>ND9910:ExplicitId9910</Id>
warnif count > 0 from n in Application.Namespaces
where
(n.IsUsing ("System.Threading.Monitor".AllowNoMatch()) ||
n.IsUsing ("System.Threading.ReaderWriterLock".AllowNoMatch()) ||
n.IsUsing ("System.Threading.Mutex".AllowNoMatch()) ||
n.IsUsing ("System.Threading.EventWaitHandle".AllowNoMatch()) ||
n.IsUsing ("System.Threading.Semaphore".AllowNoMatch()) ||
n.IsUsing ("System.Threading.Interlocked".AllowNoMatch()))
&& !n.NameLike (@"^MyNamespace.Sync")
select new {
n,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* is using a synchronization and thus, should be declared in *MyNamespace.Sync*.
//</Expl>
//<Desc>
// A *sample rule to adapt to your need*: enforces that all synchronization
// objects are used from a particular namespace.
//</Desc>
//<HowToFix>
// This is a *sample rule*, there is nothing to fix *as is*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Check that the namespace N1.N2 is 100% covered by tests - Check Coverage on particular Code Elements
A sample rule checking that a particular namespace is 100% covered by tests; adapt
@"N1.N2" and the threshold
100. Requires coverage data imported:
https://www.ndepend.com/docs/code-coverageHow to Fix:This is a
sample rule, there is nothing to fix
as is.
// <Name>Check that the namespace N1.N2 is 100% covered by tests</Name>
// <Id>ND9911:ExplicitId9911</Id>
warnif count > 0 from n in Application.Namespaces where
(n.Name == @"N1.N2") &&
n.PercentageCoverage < 100
select new {
n,
n.PercentageCoverage,
Debt = n.NbLinesOfCodeNotCovered.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* is only *{1}* covered by tests while it should be 100% covered.
//</Expl>
//<Desc>
// A sample rule checking that a particular namespace is 100% covered by
// tests; adapt **@"N1.N2"** and the threshold **100**. Requires coverage
// data imported: https://www.ndepend.com/docs/code-coverage
//</Desc>
//<HowToFix>
// This is a *sample rule*, there is nothing to fix *as is*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Check that the assembly Asm is 100% covered by tests - Check Coverage on particular Code Elements
A sample rule checking that a particular assembly is 100% covered by tests; adapt
@"Asm" and the threshold
100. Requires coverage data imported:
https://www.ndepend.com/docs/code-coverageHow to Fix:This is a
sample rule, there is nothing to fix
as is.
// <Name>Check that the assembly Asm is 100% covered by tests</Name>
// <Id>ND9912:ExplicitId9912</Id>
warnif count > 0 from a in Application.Assemblies where
(a.Name == @"Asm") &&
a.PercentageCoverage < 100
select new {
a,
a.PercentageCoverage,
Debt = a.NbLinesOfCodeNotCovered.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* is only *{1}* covered by tests while it should be 100% covered.
//</Expl>
//<Desc>
// A sample rule checking that a particular assembly is 100% covered by
// tests; adapt **@"Asm"** and the threshold **100**. Requires coverage
// data imported: https://www.ndepend.com/docs/code-coverage
//</Desc>
//<HowToFix>
// This is a *sample rule*, there is nothing to fix *as is*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Check that the class Namespace.Foo is 100% covered by tests - Check Coverage on particular Code Elements
A sample rule checking that a particular class is 100% covered by tests; adapt
@"Namespace.Foo" and the threshold
100. Requires coverage data imported:
https://www.ndepend.com/docs/code-coverageHow to Fix:This is a
sample rule, there is nothing to fix
as is.
// <Name>Check that the class Namespace.Foo is 100% covered by tests</Name>
// <Id>ND9913:ExplicitId9913</Id>
warnif count > 0 from t in Application.Types where
(t.FullName == @"Namespace.Foo") &&
t.PercentageCoverage < 100
select new {
t,
t.PercentageCoverage,
Debt = t.NbLinesOfCodeNotCovered.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* is only *{1}* covered by tests while it should be 100% covered.
//</Expl>
//<Desc>
// A sample rule checking that a particular class is 100% covered by tests;
// adapt **@"Namespace.Foo"** and the threshold **100**. Requires coverage
// data imported: https://www.ndepend.com/docs/code-coverage
//</Desc>
//<HowToFix>
// This is a *sample rule*, there is nothing to fix *as is*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Check that the method Namespace.Foo.Method(Int32) is 100% covered by tests - Check Coverage on particular Code Elements
A sample rule checking that a particular method is 100% covered by tests; adapt
@"Namespace.Foo.Method(Int32)" and the threshold
100. Requires coverage data imported:
https://www.ndepend.com/docs/code-coverageHow to Fix:This is a
sample rule, there is nothing to fix
as is.
// <Name>Check that the method Namespace.Foo.Method(Int32) is 100% covered by tests</Name>
// <Id>ND9914:ExplicitId9914</Id>
warnif count > 0 from t in Application.Types where
(t.FullName == @"Namespace.Foo.Method(Int32)") &&
t.PercentageCoverage < 100
select new {
t,
t.PercentageCoverage,
Debt = t.NbLinesOfCodeNotCovered.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* is only *{1}* covered by tests while it should be 100% covered.
//</Expl>
//<Desc>
// A sample rule checking that a particular method is 100% covered by tests;
// adapt **@"Namespace.Foo.Method(Int32)"** and the threshold **100**.
// Requires coverage data imported: https://www.ndepend.com/docs/code-coverage
//</Desc>
//<HowToFix>
// This is a *sample rule*, there is nothing to fix *as is*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Check that all types that derive from Foo, has a name that ends up with Foo - Custom Naming Conventions
A sample rule to adapt to your need: enforces that all classes deriving from a particular class are named with a particular suffix.
How to Fix:
This is a sample rule, there is nothing to fix as is.
// <Name>Check that all types that derive from Foo, has a name that ends up with Foo</Name>
// <Id>ND9915:ExplicitId9915</Id>
warnif count > 0 from t in Application.Types where
t.DeriveFrom ("Foo".AllowNoMatch().MatchType()) &&
!t.NameLike (@"Foo$")
select new {
t,
t.NbLinesOfCode,
Debt = 5.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* derives from *Foo* and thus, its name should end up with *Foo*.
//</Expl>
//<Desc>
// A *sample rule to adapt to your need*: enforces that all classes deriving
// from a particular class are named with a particular suffix.
//</Desc>
//<HowToFix>
// This is a *sample rule*, there is nothing to fix *as is*.
//</HowToFix>
./rules/assets/img/StatusBigOk32x32.png
Check that all namespaces begins with CompanyName.ProductName - Custom Naming Conventions
A widely adopted practice: all namespaces of a product start with "CompanyName.ProductName". Adapt the rule with your own "CompanyName.ProductName".
How to Fix:
Update all namespaces definitions in source code to satisfy this rule.
// <Name>Check that all namespaces begins with CompanyName.ProductName</Name>
// <Id>ND9916:ExplicitId9916</Id>
warnif count > 0 from n in Application.Namespaces where
!n.NameLike (@"^CompanyName.ProductName")
select new {
n,
n.NbLinesOfCode,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *{0}* name should start with *CompanyName.ProductName*.
//</Expl>
//<Desc>
// A widely adopted practice: all namespaces of a product start with
// "CompanyName.ProductName". Adapt the rule with your own
// **"CompanyName.ProductName"**.
//</Desc>
//<HowToFix>
// Update all namespaces definitions in source code to satisfy this rule.
//</HowToFix>