diff --git a/src/Schematron/EvaluableExpression.cs b/src/Schematron/EvaluableExpression.cs
index 971aff0..f626d98 100644
--- a/src/Schematron/EvaluableExpression.cs
+++ b/src/Schematron/EvaluableExpression.cs
@@ -14,8 +14,8 @@ namespace Schematron;
///
public abstract class EvaluableExpression
{
- string xpath = null!;
- XPathExpression expr = null!;
+ string? xpath;
+ XPathExpression? expr;
XmlNamespaceManager? ns;
///
@@ -39,7 +39,7 @@ protected void InitializeExpression(string xpathExpression)
expr = Config.DefaultNavigator.Compile(xpathExpression);
ret = expr.ReturnType;
- if (ns != null)
+ if (ns is not null)
expr.SetContext(ns);
}
@@ -48,10 +48,10 @@ protected void InitializeExpression(string xpathExpression)
/// A clone of the expression is always returned, because the compiled
/// expression is not thread-safe for evaluation.
///
- public XPathExpression CompiledExpression => expr != null ? expr.Clone() : null!;
+ public XPathExpression? CompiledExpression => expr?.Clone();
/// Contains the string version of the expression.
- public string Expression => xpath;
+ public string Expression => xpath ?? string.Empty;
/// Contains the string version of the expression.
public XPathResultType ReturnType => ret;
@@ -62,7 +62,7 @@ protected void InitializeExpression(string xpathExpression)
/// Sets the manager to use to resolve expression namespaces.
public void SetContext(XmlNamespaceManager nsManager)
{
- if (expr != null)
+ if (expr is not null)
{
// When the expression contains variable references ($name), .NET requires an
// XsltContext (not just XmlNamespaceManager). Use a load-time stub that satisfies
diff --git a/src/Schematron/EvaluationContextBase.cs b/src/Schematron/EvaluationContextBase.cs
index a00ae75..5c5f380 100644
--- a/src/Schematron/EvaluationContextBase.cs
+++ b/src/Schematron/EvaluationContextBase.cs
@@ -37,7 +37,7 @@ public abstract class EvaluationContextBase
/// strategy for matching nodes is initialized, depending on the specific
/// implementation of the in use.
///
- protected IMatchedNodes? Matched { get; set; }
+ protected IMatchedNodes Matched { get; set; } = NullMatchedNodes.Instance;
/// Gets or sets the class to use to format messages.
///
@@ -66,7 +66,7 @@ public abstract class EvaluationContextBase
public string Phase { get; set; } = string.Empty;
/// Gets or sets the schema to use for the validation.
- public Schema? Schema { get; set; }
+ public Schema Schema { get; set; } = Schema.Empty;
///
/// When this property is set, the appropriate
@@ -74,7 +74,7 @@ public abstract class EvaluationContextBase
///
public XPathNavigator Source
{
- get => source!;
+ get => source ?? throw new InvalidOperationException("Source has not been set.");
set
{
source = value;
@@ -108,6 +108,9 @@ public XPathNavigator Source
///
/// By default, it clears the and sets to false.
///
- protected void Reset() => Messages = new StringBuilder();
+ protected void Reset()
+ {
+ Messages.Clear();
+ HasErrors = false;
+ }
}
-
diff --git a/src/Schematron/Formatters/FormatterBase.cs b/src/Schematron/Formatters/FormatterBase.cs
index ee7502a..7243b85 100644
--- a/src/Schematron/Formatters/FormatterBase.cs
+++ b/src/Schematron/Formatters/FormatterBase.cs
@@ -104,7 +104,7 @@ protected static StringBuilder FormatMessage(Test source, XPathNavigator context
sb.Append(msg[offset..name.Index]);
// Does the name element have a path attribute?
- if (nameExpr != null)
+ if (nameExpr is not null)
{
SetExpressionContext(nameExpr, source, ambientCtx);
@@ -122,11 +122,11 @@ protected static StringBuilder FormatMessage(Test source, XPathNavigator context
result = context.Evaluate(nameExpr) as string;
}
- if (result != null)
+ if (result is not null)
sb.Append(result);
}
// Does the value-of element have a select attribute?
- else if (selectExpr != null)
+ else if (selectExpr is not null)
{
SetExpressionContext(selectExpr, source, ambientCtx);
@@ -143,7 +143,7 @@ protected static StringBuilder FormatMessage(Test source, XPathNavigator context
result = context.Evaluate(selectExpr) as string;
}
- if (result != null)
+ if (result is not null)
sb.Append(result);
}
// If there is no path or select expression, there is an empty element.
@@ -159,14 +159,14 @@ protected static StringBuilder FormatMessage(Test source, XPathNavigator context
static void SetExpressionContext(XPathExpression expr, Test source, SchematronXsltContext? ambientCtx)
{
- if (ambientCtx != null)
+ if (ambientCtx is not null)
{
try { expr.SetContext(ambientCtx); return; }
catch (System.Xml.XPath.XPathException) { /* fall through */ }
}
var ns = source.GetContext();
- if (ns == null) return;
+ if (ns is null) return;
try { expr.SetContext(ns); }
catch (System.Xml.XPath.XPathException)
{
diff --git a/src/Schematron/IMatchedNodes.cs b/src/Schematron/IMatchedNodes.cs
index 08e4a93..c1e425d 100644
--- a/src/Schematron/IMatchedNodes.cs
+++ b/src/Schematron/IMatchedNodes.cs
@@ -35,3 +35,13 @@ public interface IMatchedNodes
void Clear();
}
+/// No-op sentinel used as a default before a real matching strategy is selected.
+class NullMatchedNodes : IMatchedNodes
+{
+ public static NullMatchedNodes Instance { get; } = new();
+
+ public bool IsMatched(XPathNavigator node) => false;
+ public void AddMatched(XPathNavigator node) { }
+ public void Clear() { }
+}
+
diff --git a/src/Schematron/Schema.cs b/src/Schematron/Schema.cs
index b0f0ae4..1a21869 100644
--- a/src/Schematron/Schema.cs
+++ b/src/Schematron/Schema.cs
@@ -23,6 +23,9 @@ public class Schema
/// The default Schematron namespace. Kept for backward compatibility; prefer .
public const string Namespace = LegacyNamespace;
+ /// A shared empty schema instance used as a default sentinel.
+ public static Schema Empty { get; } = new();
+
/// Returns if is a recognized Schematron namespace URI.
public static bool IsSchematronNamespace(string? uri) => uri == IsoNamespace || uri == LegacyNamespace;
@@ -109,6 +112,6 @@ public void Load(XmlReader schema)
public bool IsLibrary { get; set; }
///
- public XmlNamespaceManager NsManager { get; set; } = null!;
+ public XmlNamespaceManager NsManager { get; set; } = new(new NameTable());
}
diff --git a/src/Schematron/SchemaLoader.cs b/src/Schematron/SchemaLoader.cs
index 45998b8..8f98ab7 100644
--- a/src/Schematron/SchemaLoader.cs
+++ b/src/Schematron/SchemaLoader.cs
@@ -1,4 +1,5 @@
using System.Collections;
+using System.Diagnostics.CodeAnalysis;
using System.Xml;
using System.Xml.XPath;
@@ -14,31 +15,37 @@ namespace Schematron;
public class SchemaLoader(Schema schema)
{
XPathNavigator filenav = null!;
- Hashtable? abstracts = null;
+ Hashtable? abstracts;
// Detected Schematron namespace and the namespace manager derived from the source document.
- string schNs = null!;
- XmlNamespaceManager mgr = null!;
+ string? schNs;
+ XmlNamespaceManager? mgr;
// Instance-level XPath expressions compiled against the detected namespace.
- XPathExpression exprSchema = null!;
- XPathExpression exprEmbeddedSchema = null!;
- XPathExpression exprPhase = null!;
- XPathExpression exprPattern = null!;
- XPathExpression exprAbstractRule = null!;
- XPathExpression exprConcreteRule = null!;
- XPathExpression exprRuleExtends = null!;
- XPathExpression exprAssert = null!;
- XPathExpression exprReport = null!;
- XPathExpression exprLet = null!;
- XPathExpression exprDiagnostic = null!;
- XPathExpression exprParam = null!;
- XPathExpression exprLibrary = null!;
- XPathExpression exprRulesContainer = null!;
- XPathExpression exprGroup = null!;
+ XPathExpression? exprSchema;
+ XPathExpression? exprEmbeddedSchema;
+ XPathExpression? exprPhase;
+ XPathExpression? exprPattern;
+ XPathExpression? exprAbstractRule;
+ XPathExpression? exprConcreteRule;
+ XPathExpression? exprRuleExtends;
+ XPathExpression? exprAssert;
+ XPathExpression? exprReport;
+ XPathExpression? exprLet;
+ XPathExpression? exprDiagnostic;
+ XPathExpression? exprParam;
+ XPathExpression? exprLibrary;
+ XPathExpression? exprRulesContainer;
+ XPathExpression? exprGroup;
///
///
+ [MemberNotNull(nameof(schNs), nameof(mgr),
+ nameof(exprSchema), nameof(exprEmbeddedSchema), nameof(exprPhase),
+ nameof(exprPattern), nameof(exprAbstractRule), nameof(exprConcreteRule),
+ nameof(exprRuleExtends), nameof(exprAssert), nameof(exprReport),
+ nameof(exprLet), nameof(exprDiagnostic), nameof(exprParam),
+ nameof(exprLibrary), nameof(exprRulesContainer), nameof(exprGroup))]
public virtual void LoadSchema(XPathNavigator source)
{
schema.NsManager = new XmlNamespaceManager(source.NameTable);
@@ -84,6 +91,12 @@ public virtual void LoadSchema(XPathNavigator source)
/// Detects the Schematron namespace used in and compiles all
/// instance-level XPath expressions against that namespace.
///
+ [MemberNotNull(nameof(schNs), nameof(mgr),
+ nameof(exprSchema), nameof(exprEmbeddedSchema), nameof(exprPhase),
+ nameof(exprPattern), nameof(exprAbstractRule), nameof(exprConcreteRule),
+ nameof(exprRuleExtends), nameof(exprAssert), nameof(exprReport),
+ nameof(exprLet), nameof(exprDiagnostic), nameof(exprParam),
+ nameof(exprLibrary), nameof(exprRulesContainer), nameof(exprGroup))]
void DetectAndBuildExpressions(XPathNavigator source)
{
schNs = DetectSchematronNamespace(source);
@@ -428,7 +441,7 @@ void LoadExtends(Rule rule, XPathNavigator context)
while (extends.MoveNext())
{
var ruleName = extends.Current.GetAttribute("rule", string.Empty);
- if (abstracts != null && abstracts.ContainsKey(ruleName))
+ if (abstracts?.ContainsKey(ruleName) == true)
rule.Extend((Rule)abstracts[ruleName]!);
else
throw new BadSchemaException("The abstract rule with id=\"" + ruleName + "\" is used but not defined.");
diff --git a/src/Schematron/SyncEvaluationContext.cs b/src/Schematron/SyncEvaluationContext.cs
index 3f1d067..52e3d41 100644
--- a/src/Schematron/SyncEvaluationContext.cs
+++ b/src/Schematron/SyncEvaluationContext.cs
@@ -27,7 +27,7 @@ public override void Start()
Reset();
// Is there something to evaluate at all?
- if (Schema is null || Schema.Patterns.Count == 0)
+ if (Schema.Patterns.Count == 0)
return;
// If no phase was received, try the default phase defined for the schema.
@@ -35,7 +35,7 @@ public override void Start()
if (Phase == string.Empty)
Phase = Schema.DefaultPhase is { Length: > 0 } phase ? phase : Schematron.Phase.All;
- if (Phase != Schematron.Phase.All && Schema.Phases[Phase] == null)
+ if (Phase != Schematron.Phase.All && Schema.Phases[Phase] is null)
throw new ArgumentException("The specified Phase isn't defined for the current schema.");
if (Evaluate(Schema.Phases[Phase], Messages))
@@ -118,13 +118,14 @@ bool Evaluate(Pattern pattern, StringBuilder output)
// Reset matched nodes, as across patters, nodes can be
// evaluated more than once.
Matched.Clear();
- var isGroup = pattern is Group;
foreach (var rule in pattern.Rules)
{
// For groups (ISO Schematron 2025), each rule evaluates independently.
- if (isGroup) Matched.Clear();
- if (Evaluate(rule, sb, BuildLets(pattern.Lets))) failed = true;
+ if (pattern is Group)
+ Matched.Clear();
+ if (Evaluate(rule, sb, BuildLets(pattern.Lets)))
+ failed = true;
}
if (failed)
{
@@ -176,7 +177,8 @@ bool Evaluate(Rule rule, StringBuilder output, Dictionary? patte
var failed = false;
var sb = new StringBuilder();
Source.MoveToRoot();
- var nodes = Source.Select(rule.CompiledExpression);
+ // Non-abstract rules always have a compiled expression (guarded by IsAbstract check above).
+ var nodes = Source.Select(rule.CompiledExpression!);
var evaluables = new ArrayList(nodes.Count);
// The navigator doesn't contain line info
@@ -253,7 +255,7 @@ bool Evaluate(Rule rule, StringBuilder output, Dictionary? patte
bool EvaluateAssert(Assert assert, XPathNavigator context, StringBuilder output,
Dictionary? patternLets = null, LetCollection? ruleLets = null)
{
- var expr = PrepareExpression(assert.CompiledExpression, context, patternLets, ruleLets, out var xsltCtx);
+ var expr = PrepareExpression(assert.CompiledExpression!, context, patternLets, ruleLets, out var xsltCtx);
var eval = context.Evaluate(expr);
var result = true;
@@ -293,7 +295,7 @@ bool EvaluateAssert(Assert assert, XPathNavigator context, StringBuilder output,
bool EvaluateReport(Report report, XPathNavigator context, StringBuilder output,
Dictionary? patternLets = null, LetCollection? ruleLets = null)
{
- var expr = PrepareExpression(report.CompiledExpression, context, patternLets, ruleLets, out var xsltCtx);
+ var expr = PrepareExpression(report.CompiledExpression!, context, patternLets, ruleLets, out var xsltCtx);
var eval = context.Evaluate(expr);
var result = false;
@@ -322,13 +324,18 @@ bool EvaluateReport(Report report, XPathNavigator context, StringBuilder output,
Dictionary BuildLets(LetCollection? extraLets = null)
{
- var d = new Dictionary(StringComparer.Ordinal);
+ var result = new Dictionary(StringComparer.Ordinal);
+
foreach (var let in Schema.Lets)
- if (let.Value is not null) d[let.Name] = let.Value;
- if (extraLets != null)
+ if (let.Value is not null)
+ result[let.Name] = let.Value;
+
+ if (extraLets is not null)
foreach (var let in extraLets)
- if (let.Value is not null) d[let.Name] = let.Value;
- return d;
+ if (let.Value is not null)
+ result[let.Name] = let.Value;
+
+ return result;
}
// Prepares an XPathExpression to be evaluated with variable support.
@@ -341,14 +348,20 @@ XPathExpression PrepareExpression(
out SchematronXsltContext? xsltCtx)
{
var hasVars = (Schema.Lets.Count + (patternLets?.Count ?? 0) + (ruleLets?.Count ?? 0)) > 0;
- if (!hasVars) { xsltCtx = null; return expr; }
+ if (!hasVars)
+ {
+ xsltCtx = null;
+ return expr;
+ }
var vars = new Dictionary(StringComparer.Ordinal);
foreach (var let in Schema.Lets)
if (let.Value is not null) vars[let.Name] = let.Value;
- if (patternLets != null)
+
+ if (patternLets is not null)
foreach (var kv in patternLets) vars[kv.Key] = kv.Value;
- if (ruleLets != null)
+
+ if (ruleLets is not null)
foreach (var let in ruleLets)
if (let.Value is not null) vars[let.Name] = let.Value;
diff --git a/src/Schematron/Validator.cs b/src/Schematron/Validator.cs
index cc5b5bd..90cf9b5 100644
--- a/src/Schematron/Validator.cs
+++ b/src/Schematron/Validator.cs
@@ -20,7 +20,7 @@ public class Validator
readonly SchemaCollection schematrons = [];
NavigableType navtype = NavigableType.XPathDocument;
- StringBuilder? errors;
+ StringBuilder errors = new();
bool haserrors;
///
@@ -191,7 +191,7 @@ public void AddSchema(XmlReader reader)
if (wxs)
{
haserrors = false;
- errors = new StringBuilder();
+ errors.Clear();
var xs = XmlSchema.Read(new XmlTextReader(r, reader.NameTable), new ValidationEventHandler(OnValidation));
@@ -228,11 +228,11 @@ public void AddSchema(XmlReader reader)
var sch = new Schema();
sch.Load(nav);
schematrons.Add(sch);
- errors = null;
+ errors.Clear();
}
- #region WORK IN PROGRESS :: The need the for the signature AddSchema(string targetNamespace, string schemaUri) comes from resolving imported (schemaLocation hinted) partial schemas
+ #region WORK IN PROGRESS:: The need the for the signature AddSchema(string targetNamespace, string schemaUri) comes from resolving imported (schemaLocation hinted) partial schemas
bool TryAddXmlSchema(
string targetNamespace,
@@ -258,7 +258,7 @@ bool TryAddXmlSchema(
if (!IsStandardSchema(namespaceUri))
return false;
- errors ??= new StringBuilder();
+ errors.Clear();
var set = new XmlSchemaSet
{
@@ -307,7 +307,7 @@ public void AddSchema(string targetNamespace, string schemaUri)
sch.Load(nav);
schematrons.Add(sch);
- errors = null;
+ errors.Clear();
}
#endregion
@@ -334,7 +334,7 @@ public void AddSchema(string targetNamespace, string schemaUri)
///
public void ValidateSchematron(XPathNavigator file)
{
- errors = new StringBuilder();
+ errors.Clear();
Context.Source = file;
foreach (var sch in schematrons)
@@ -394,12 +394,12 @@ public IXPathNavigable Validate(string uri)
/// The loaded instance.
public IXPathNavigable Validate(XmlReader reader)
{
- errors = new StringBuilder();
+ errors.Clear();
var hasxml = false;
- StringBuilder? xmlerrors = null;
+ string? xmlErrorText = null;
var hassch = false;
- StringBuilder? scherrors = null;
+ string? schErrorText = null;
var settings = new XmlReaderSettings
{
@@ -439,14 +439,14 @@ public IXPathNavigable Validate(XmlReader reader)
Context.Formatter.Format(r.Settings.Schemas, errors);
Context.Formatter.Format(r, errors);
hasxml = true;
- xmlerrors = errors;
+ xmlErrorText = errors.ToString();
}
Context.Source = nav;
// Reset shared variables
haserrors = false;
- errors = new StringBuilder();
+ errors.Clear();
foreach (var sch in schematrons)
{
@@ -462,12 +462,14 @@ public IXPathNavigable Validate(XmlReader reader)
{
Context.Formatter.Format(schematrons, errors);
hassch = true;
- scherrors = errors;
+ schErrorText = errors.ToString();
}
- errors = new StringBuilder();
- if (hasxml) errors.Append(xmlerrors!.ToString());
- if (hassch) errors.Append(scherrors!.ToString());
+ errors.Clear();
+ if (hasxml)
+ errors.Append(xmlErrorText);
+ if (hassch)
+ errors.Append(schErrorText);
if (hasxml || hassch)
{
@@ -487,7 +489,7 @@ void PerformValidation(Schema schema)
void OnValidation(object sender, ValidationEventArgs e)
{
haserrors = true;
- Context.Formatter.Format(e, errors!);
+ Context.Formatter.Format(e, errors);
}
}