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); } }