Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions src/Schematron/EvaluableExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ namespace Schematron;
/// <progress amount="100" />
public abstract class EvaluableExpression
{
string xpath = null!;
XPathExpression expr = null!;
string? xpath;
XPathExpression? expr;
XmlNamespaceManager? ns;

/// <summary>
Expand All @@ -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);
}

Expand All @@ -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.
/// </remarks>
public XPathExpression CompiledExpression => expr != null ? expr.Clone() : null!;
public XPathExpression? CompiledExpression => expr?.Clone();

/// <summary>Contains the string version of the expression.</summary>
public string Expression => xpath;
public string Expression => xpath ?? string.Empty;

/// <summary>Contains the string version of the expression.</summary>
public XPathResultType ReturnType => ret;
Expand All @@ -62,7 +62,7 @@ protected void InitializeExpression(string xpathExpression)
/// <summary>Sets the manager to use to resolve expression namespaces.</summary>
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
Expand Down
13 changes: 8 additions & 5 deletions src/Schematron/EvaluationContextBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public abstract class EvaluationContextBase
/// strategy for matching nodes is initialized, depending on the specific
/// implementation of the <see cref="XPathNavigator"/> in use.
/// </remarks>
protected IMatchedNodes? Matched { get; set; }
protected IMatchedNodes Matched { get; set; } = NullMatchedNodes.Instance;

/// <summary>Gets or sets the class to use to format messages.</summary>
/// <remarks>
Expand Down Expand Up @@ -66,15 +66,15 @@ public abstract class EvaluationContextBase
public string Phase { get; set; } = string.Empty;

/// <summary>Gets or sets the schema to use for the validation.</summary>
public Schema? Schema { get; set; }
public Schema Schema { get; set; } = Schema.Empty;

/// <remarks>
/// When this property is set, the appropriate <see cref="IMatchedNodes"/>
/// strategy is picked, to perform optimum for various navigator implementations.
/// </remarks>
public XPathNavigator Source
{
get => source!;
get => source ?? throw new InvalidOperationException("Source has not been set.");
set
{
source = value;
Expand Down Expand Up @@ -108,6 +108,9 @@ public XPathNavigator Source
/// <remarks>
/// By default, it clears the <see cref="Messages"/> and sets <see cref="HasErrors"/> to false.
/// </remarks>
protected void Reset() => Messages = new StringBuilder();
protected void Reset()
{
Messages.Clear();
HasErrors = false;
}
}

12 changes: 6 additions & 6 deletions src/Schematron/Formatters/FormatterBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);

Expand All @@ -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 <name> element.
Expand All @@ -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)
{
Expand Down
10 changes: 10 additions & 0 deletions src/Schematron/IMatchedNodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,13 @@ public interface IMatchedNodes
void Clear();
}

/// <summary>No-op sentinel used as a default before a real matching strategy is selected.</summary>
class NullMatchedNodes : IMatchedNodes
{
public static NullMatchedNodes Instance { get; } = new();

public bool IsMatched(XPathNavigator node) => false;
public void AddMatched(XPathNavigator node) { }
public void Clear() { }
}

5 changes: 4 additions & 1 deletion src/Schematron/Schema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public class Schema
/// <summary>The default Schematron namespace. Kept for backward compatibility; prefer <see cref="IsoNamespace"/>.</summary>
public const string Namespace = LegacyNamespace;

/// <summary>A shared empty schema instance used as a default sentinel.</summary>
public static Schema Empty { get; } = new();

/// <summary>Returns <see langword="true"/> if <paramref name="uri"/> is a recognized Schematron namespace URI.</summary>
public static bool IsSchematronNamespace(string? uri) => uri == IsoNamespace || uri == LegacyNamespace;

Expand Down Expand Up @@ -109,6 +112,6 @@ public void Load(XmlReader schema)
public bool IsLibrary { get; set; }

/// <summary />
public XmlNamespaceManager NsManager { get; set; } = null!;
public XmlNamespaceManager NsManager { get; set; } = new(new NameTable());
}

51 changes: 32 additions & 19 deletions src/Schematron/SchemaLoader.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Xml;
using System.Xml.XPath;

Expand All @@ -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;

/// <summary />
/// <param name="source"></param>
[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);
Expand Down Expand Up @@ -84,6 +91,12 @@ public virtual void LoadSchema(XPathNavigator source)
/// Detects the Schematron namespace used in <paramref name="source"/> and compiles all
/// instance-level XPath expressions against that namespace.
/// </summary>
[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);
Expand Down Expand Up @@ -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.");
Expand Down
45 changes: 29 additions & 16 deletions src/Schematron/SyncEvaluationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ 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.
// If no default phase is defined, all patterns will be tested.
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))
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -176,7 +177,8 @@ bool Evaluate(Rule rule, StringBuilder output, Dictionary<string, string>? 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
Expand Down Expand Up @@ -253,7 +255,7 @@ bool Evaluate(Rule rule, StringBuilder output, Dictionary<string, string>? patte
bool EvaluateAssert(Assert assert, XPathNavigator context, StringBuilder output,
Dictionary<string, string>? 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;

Expand Down Expand Up @@ -293,7 +295,7 @@ bool EvaluateAssert(Assert assert, XPathNavigator context, StringBuilder output,
bool EvaluateReport(Report report, XPathNavigator context, StringBuilder output,
Dictionary<string, string>? 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;

Expand Down Expand Up @@ -322,13 +324,18 @@ bool EvaluateReport(Report report, XPathNavigator context, StringBuilder output,

Dictionary<string, string> BuildLets(LetCollection? extraLets = null)
{
var d = new Dictionary<string, string>(StringComparer.Ordinal);
var result = new Dictionary<string, string>(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.
Expand All @@ -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<string, string>(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;

Expand Down
Loading
Loading