From 9c0948fb89117d3e01ed465bb9eda3ff2232fbe0 Mon Sep 17 00:00:00 2001 From: Daniel Jonathan Date: Fri, 31 Oct 2025 17:06:39 +0100 Subject: [PATCH 1/4] updated the code --- .../Integration/{ => xml}/AdvanceFile.xml | 0 .../Integration/{ => xml}/ItemsSample.xml | 0 TestData/Integration/xml/ShipmentConf-lml.xml | 19 ++ .../{ => xml}/ShipmentConf-proper.xml | 0 TestData/Integration/xml/ShipmentConf_ns.xml | 50 +++++ .../VariableLoggingSampleV1Input.xml | 0 .../Integration/{ => xml}/message-test.xml | 0 .../Integration/{ => xml}/sample copy.xml | 0 .../Integration/{ => xml}/sample-data.xml | 0 .../sample-inline-cs-with-usings.xml | 0 TestData/Integration/{ => xml}/sample.xml | 0 .../Integration/{ => xml}/step-into-test.xml | 0 .../{ => xslt/tests}/InvalidFunctionCall.xslt | 0 .../{ => xslt/tests}/message-test.xslt | 0 .../tests}/step-into-test-simple.xslt | 0 .../{ => xslt/tests}/step-into-test.xslt | 0 .../{ => xslt/tests}/test-guardrails.xslt | 0 .../v1}/VariableLoggingSampleV1.xslt | 0 .../v1}/sample-inline-cs-with-usings.xslt | 0 .../{ => xslt/v1}/sample-inline-cs.xslt | 0 .../Integration/{ => xslt/v1}/sample.xslt | 0 .../{ => xslt/v2}/AdvanceXslt2.xslt | 0 .../{ => xslt/v2}/VariableLoggingSample.xslt | 0 .../{ => xslt/v2}/sample-xslt2.xslt | 0 .../{ => xslt/v3}/AdvanceXslt3.xslt | 0 .../Integration/xslt/v3/LmlBasedXslt.xslt | 53 ++++++ .../{ => xslt/v3}/ShipmentConf3.xslt | 0 .../xslt/v3/ShipmentConfv2_ns.xslt | 89 +++++++++ .../{ => xslt/v3}/sample-xslt3.xslt | 0 .../LmlCompilationTest.cs | 111 +++++++++++ .../NamespaceExtractionTest.cs | 174 ++++++++++++++++++ .../ProgramUsingEngineType.cs | 34 +++- XsltDebugger.ConsoleTest/StepIntoTest.cs | 4 +- .../XsltDebugger.ConsoleTest.csproj | 8 +- XsltDebugger.DebugAdapter/DapServer.cs | 18 ++ XsltDebugger.DebugAdapter/SaxonEngine.cs | 42 +++++ .../XsltCompiledEngine.cs | 42 +++++ .../XsltEngineManager.cs | 23 +++ .../CompiledEngineIntegrationTests.cs | 8 +- .../SaxonEngineIntegrationTests.cs | 20 +- XsltDebugger.Tests/StepIntoTests.cs | 16 +- test-namespace-support.sh | 42 +++++ 42 files changed, 723 insertions(+), 30 deletions(-) rename TestData/Integration/{ => xml}/AdvanceFile.xml (100%) rename TestData/Integration/{ => xml}/ItemsSample.xml (100%) create mode 100644 TestData/Integration/xml/ShipmentConf-lml.xml rename TestData/Integration/{ => xml}/ShipmentConf-proper.xml (100%) create mode 100644 TestData/Integration/xml/ShipmentConf_ns.xml rename TestData/Integration/{ => xml}/VariableLoggingSampleV1Input.xml (100%) rename TestData/Integration/{ => xml}/message-test.xml (100%) rename TestData/Integration/{ => xml}/sample copy.xml (100%) rename TestData/Integration/{ => xml}/sample-data.xml (100%) rename TestData/Integration/{ => xml}/sample-inline-cs-with-usings.xml (100%) rename TestData/Integration/{ => xml}/sample.xml (100%) rename TestData/Integration/{ => xml}/step-into-test.xml (100%) rename TestData/Integration/{ => xslt/tests}/InvalidFunctionCall.xslt (100%) rename TestData/Integration/{ => xslt/tests}/message-test.xslt (100%) rename TestData/Integration/{ => xslt/tests}/step-into-test-simple.xslt (100%) rename TestData/Integration/{ => xslt/tests}/step-into-test.xslt (100%) rename TestData/Integration/{ => xslt/tests}/test-guardrails.xslt (100%) rename TestData/Integration/{ => xslt/v1}/VariableLoggingSampleV1.xslt (100%) rename TestData/Integration/{ => xslt/v1}/sample-inline-cs-with-usings.xslt (100%) rename TestData/Integration/{ => xslt/v1}/sample-inline-cs.xslt (100%) rename TestData/Integration/{ => xslt/v1}/sample.xslt (100%) rename TestData/Integration/{ => xslt/v2}/AdvanceXslt2.xslt (100%) rename TestData/Integration/{ => xslt/v2}/VariableLoggingSample.xslt (100%) rename TestData/Integration/{ => xslt/v2}/sample-xslt2.xslt (100%) rename TestData/Integration/{ => xslt/v3}/AdvanceXslt3.xslt (100%) create mode 100644 TestData/Integration/xslt/v3/LmlBasedXslt.xslt rename TestData/Integration/{ => xslt/v3}/ShipmentConf3.xslt (100%) create mode 100644 TestData/Integration/xslt/v3/ShipmentConfv2_ns.xslt rename TestData/Integration/{ => xslt/v3}/sample-xslt3.xslt (100%) create mode 100644 XsltDebugger.ConsoleTest/LmlCompilationTest.cs create mode 100644 XsltDebugger.ConsoleTest/NamespaceExtractionTest.cs create mode 100755 test-namespace-support.sh diff --git a/TestData/Integration/AdvanceFile.xml b/TestData/Integration/xml/AdvanceFile.xml similarity index 100% rename from TestData/Integration/AdvanceFile.xml rename to TestData/Integration/xml/AdvanceFile.xml diff --git a/TestData/Integration/ItemsSample.xml b/TestData/Integration/xml/ItemsSample.xml similarity index 100% rename from TestData/Integration/ItemsSample.xml rename to TestData/Integration/xml/ItemsSample.xml diff --git a/TestData/Integration/xml/ShipmentConf-lml.xml b/TestData/Integration/xml/ShipmentConf-lml.xml new file mode 100644 index 0000000..52325bb --- /dev/null +++ b/TestData/Integration/xml/ShipmentConf-lml.xml @@ -0,0 +1,19 @@ + + + REF-12345 + 1500.50 + ABC-123 + + ORD-001 + Acme Corporation + 2025-10-31 14:30:00 + + 1 + SHIP + + + 2 + LOAD + + + diff --git a/TestData/Integration/ShipmentConf-proper.xml b/TestData/Integration/xml/ShipmentConf-proper.xml similarity index 100% rename from TestData/Integration/ShipmentConf-proper.xml rename to TestData/Integration/xml/ShipmentConf-proper.xml diff --git a/TestData/Integration/xml/ShipmentConf_ns.xml b/TestData/Integration/xml/ShipmentConf_ns.xml new file mode 100644 index 0000000..e766b90 --- /dev/null +++ b/TestData/Integration/xml/ShipmentConf_ns.xml @@ -0,0 +1,50 @@ + + + TRUCK + Truck + Road + Outbound + REF-REDACTED + 2025-07-15T08:30:00Z + 2025-07-15T10:45:00+02:00 + PLATE-REDACTED + 12500.756 + Approved + + + ORD-XXXX + 2025-07-15T00:00:00 + CUST-XXXX + LogicApps - iPaaS Tool Helper + + 10 + Load truck + LD + + + 2025-07-14T12:00:00 + + + 2025-07-13T09:00:00 + + + 2025-06-15T06:30:00 + + + + + + 20 + Second load + LD2 + + + 2023-07-12 + + + 2025-07-16T00:00:00 + + + + + \ No newline at end of file diff --git a/TestData/Integration/VariableLoggingSampleV1Input.xml b/TestData/Integration/xml/VariableLoggingSampleV1Input.xml similarity index 100% rename from TestData/Integration/VariableLoggingSampleV1Input.xml rename to TestData/Integration/xml/VariableLoggingSampleV1Input.xml diff --git a/TestData/Integration/message-test.xml b/TestData/Integration/xml/message-test.xml similarity index 100% rename from TestData/Integration/message-test.xml rename to TestData/Integration/xml/message-test.xml diff --git a/TestData/Integration/sample copy.xml b/TestData/Integration/xml/sample copy.xml similarity index 100% rename from TestData/Integration/sample copy.xml rename to TestData/Integration/xml/sample copy.xml diff --git a/TestData/Integration/sample-data.xml b/TestData/Integration/xml/sample-data.xml similarity index 100% rename from TestData/Integration/sample-data.xml rename to TestData/Integration/xml/sample-data.xml diff --git a/TestData/Integration/sample-inline-cs-with-usings.xml b/TestData/Integration/xml/sample-inline-cs-with-usings.xml similarity index 100% rename from TestData/Integration/sample-inline-cs-with-usings.xml rename to TestData/Integration/xml/sample-inline-cs-with-usings.xml diff --git a/TestData/Integration/sample.xml b/TestData/Integration/xml/sample.xml similarity index 100% rename from TestData/Integration/sample.xml rename to TestData/Integration/xml/sample.xml diff --git a/TestData/Integration/step-into-test.xml b/TestData/Integration/xml/step-into-test.xml similarity index 100% rename from TestData/Integration/step-into-test.xml rename to TestData/Integration/xml/step-into-test.xml diff --git a/TestData/Integration/InvalidFunctionCall.xslt b/TestData/Integration/xslt/tests/InvalidFunctionCall.xslt similarity index 100% rename from TestData/Integration/InvalidFunctionCall.xslt rename to TestData/Integration/xslt/tests/InvalidFunctionCall.xslt diff --git a/TestData/Integration/message-test.xslt b/TestData/Integration/xslt/tests/message-test.xslt similarity index 100% rename from TestData/Integration/message-test.xslt rename to TestData/Integration/xslt/tests/message-test.xslt diff --git a/TestData/Integration/step-into-test-simple.xslt b/TestData/Integration/xslt/tests/step-into-test-simple.xslt similarity index 100% rename from TestData/Integration/step-into-test-simple.xslt rename to TestData/Integration/xslt/tests/step-into-test-simple.xslt diff --git a/TestData/Integration/step-into-test.xslt b/TestData/Integration/xslt/tests/step-into-test.xslt similarity index 100% rename from TestData/Integration/step-into-test.xslt rename to TestData/Integration/xslt/tests/step-into-test.xslt diff --git a/TestData/Integration/test-guardrails.xslt b/TestData/Integration/xslt/tests/test-guardrails.xslt similarity index 100% rename from TestData/Integration/test-guardrails.xslt rename to TestData/Integration/xslt/tests/test-guardrails.xslt diff --git a/TestData/Integration/VariableLoggingSampleV1.xslt b/TestData/Integration/xslt/v1/VariableLoggingSampleV1.xslt similarity index 100% rename from TestData/Integration/VariableLoggingSampleV1.xslt rename to TestData/Integration/xslt/v1/VariableLoggingSampleV1.xslt diff --git a/TestData/Integration/sample-inline-cs-with-usings.xslt b/TestData/Integration/xslt/v1/sample-inline-cs-with-usings.xslt similarity index 100% rename from TestData/Integration/sample-inline-cs-with-usings.xslt rename to TestData/Integration/xslt/v1/sample-inline-cs-with-usings.xslt diff --git a/TestData/Integration/sample-inline-cs.xslt b/TestData/Integration/xslt/v1/sample-inline-cs.xslt similarity index 100% rename from TestData/Integration/sample-inline-cs.xslt rename to TestData/Integration/xslt/v1/sample-inline-cs.xslt diff --git a/TestData/Integration/sample.xslt b/TestData/Integration/xslt/v1/sample.xslt similarity index 100% rename from TestData/Integration/sample.xslt rename to TestData/Integration/xslt/v1/sample.xslt diff --git a/TestData/Integration/AdvanceXslt2.xslt b/TestData/Integration/xslt/v2/AdvanceXslt2.xslt similarity index 100% rename from TestData/Integration/AdvanceXslt2.xslt rename to TestData/Integration/xslt/v2/AdvanceXslt2.xslt diff --git a/TestData/Integration/VariableLoggingSample.xslt b/TestData/Integration/xslt/v2/VariableLoggingSample.xslt similarity index 100% rename from TestData/Integration/VariableLoggingSample.xslt rename to TestData/Integration/xslt/v2/VariableLoggingSample.xslt diff --git a/TestData/Integration/sample-xslt2.xslt b/TestData/Integration/xslt/v2/sample-xslt2.xslt similarity index 100% rename from TestData/Integration/sample-xslt2.xslt rename to TestData/Integration/xslt/v2/sample-xslt2.xslt diff --git a/TestData/Integration/AdvanceXslt3.xslt b/TestData/Integration/xslt/v3/AdvanceXslt3.xslt similarity index 100% rename from TestData/Integration/AdvanceXslt3.xslt rename to TestData/Integration/xslt/v3/AdvanceXslt3.xslt diff --git a/TestData/Integration/xslt/v3/LmlBasedXslt.xslt b/TestData/Integration/xslt/v3/LmlBasedXslt.xslt new file mode 100644 index 0000000..e4e6155 --- /dev/null +++ b/TestData/Integration/xslt/v3/LmlBasedXslt.xslt @@ -0,0 +1,53 @@ + + + + + + + + + + {../CustomerName} + {../../Reference} + {concat(../Number, '/', Sequence)} + {if (../../Net castable as xs:decimal) then + format-number(xs:decimal(../../Net), + '0.00') else '0.00'} + {ef:toUtcDateTime(/ShipmentConfirmation/Orders/Date)} + {../../LicensePlate} + {OperationCode} + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestData/Integration/ShipmentConf3.xslt b/TestData/Integration/xslt/v3/ShipmentConf3.xslt similarity index 100% rename from TestData/Integration/ShipmentConf3.xslt rename to TestData/Integration/xslt/v3/ShipmentConf3.xslt diff --git a/TestData/Integration/xslt/v3/ShipmentConfv2_ns.xslt b/TestData/Integration/xslt/v3/ShipmentConfv2_ns.xslt new file mode 100644 index 0000000..5d97401 --- /dev/null +++ b/TestData/Integration/xslt/v3/ShipmentConfv2_ns.xslt @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + COMPANY-REDACTED + + + + + + + + / + + + + + + hhh: + + + + + + KG + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestData/Integration/sample-xslt3.xslt b/TestData/Integration/xslt/v3/sample-xslt3.xslt similarity index 100% rename from TestData/Integration/sample-xslt3.xslt rename to TestData/Integration/xslt/v3/sample-xslt3.xslt diff --git a/XsltDebugger.ConsoleTest/LmlCompilationTest.cs b/XsltDebugger.ConsoleTest/LmlCompilationTest.cs new file mode 100644 index 0000000..fef12e4 --- /dev/null +++ b/XsltDebugger.ConsoleTest/LmlCompilationTest.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml; +using Saxon.Api; + +namespace XsltDebugger.ConsoleTest; + +/// +/// Test for LmlBasedXslt.xslt compilation +/// +class LmlCompilationTest +{ + static void Main(string[] args) + { + Console.WriteLine("=== XSLT Compilation Test ===\n"); + + var stylesheetPath = Path.GetFullPath("TestData/Integration/xslt/v3/LmlBasedXslt.xslt"); + var xmlPath = Path.GetFullPath("TestData/Integration/xml/ShipmentConf-lml.xml"); + + if (!File.Exists(stylesheetPath)) + { + Console.WriteLine($"ERROR: Stylesheet not found: {stylesheetPath}"); + return; + } + + if (!File.Exists(xmlPath)) + { + Console.WriteLine($"ERROR: XML not found: {xmlPath}"); + return; + } + + Console.WriteLine($"πŸ“„ Stylesheet: {Path.GetFileName(stylesheetPath)}"); + Console.WriteLine($"πŸ“„ XML: {Path.GetFileName(xmlPath)}"); + Console.WriteLine("\nAttempting to compile XSLT with Saxon (XSLT 3.0)...\n"); + + // Try with Saxon directly + try + { + var processor = new Processor(); + processor.SetProperty("http://saxon.sf.net/feature/xsltVersion", "3.0"); + + var compiler = processor.NewXsltCompiler(); + compiler.XsltLanguageVersion = "3.0"; + + // Capture compilation errors + var errorList = new List(); + compiler.ErrorList = errorList; + + using var styleStream = File.OpenRead(stylesheetPath); + compiler.BaseUri = new Uri(stylesheetPath); + var executable = compiler.Compile(styleStream); + + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("βœ“ Compilation successful!"); + Console.ResetColor(); + + // Try to run the transformation + var transformer = executable.Load(); + + using var xmlStream = File.OpenRead(xmlPath); + var docBuilder = processor.NewDocumentBuilder(); + docBuilder.BaseUri = new Uri(xmlPath); + var inputDoc = docBuilder.Build(xmlStream); + + transformer.InitialContextNode = inputDoc; + + var output = new StringWriter(); + var serializer = processor.NewSerializer(output); + + transformer.Run(serializer); + + Console.WriteLine("\nβœ“ Transformation successful!"); + Console.WriteLine("\nOutput:"); + Console.WriteLine(output.ToString()); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("βœ— Compilation/Transformation FAILED!"); + Console.WriteLine($"\n{ex.GetType().Name}: {ex.Message}"); + + if (ex.InnerException != null) + { + Console.WriteLine($"\nInner Exception: {ex.InnerException.Message}"); + } + + Console.ResetColor(); + + Console.WriteLine("\n=== DIAGNOSIS ==="); + Console.WriteLine("The XSLT has a syntax error on lines 20-22:"); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(" {if (../../Net castable as xs:decimal) then"); + Console.WriteLine(" format-number(xs:decimal(../../Net),"); + Console.WriteLine(" '0.00') else }"); + Console.ResetColor(); + Console.WriteLine("\nThe 'else' clause is empty! It needs a value."); + Console.WriteLine("\nSuggested fixes:"); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine(" 1) Return empty string:"); + Console.WriteLine(" {if (../../Net castable as xs:decimal) then"); + Console.WriteLine(" format-number(xs:decimal(../../Net), '0.00')"); + Console.WriteLine(" else ''}"); + Console.WriteLine("\n 2) Return default value:"); + Console.WriteLine(" {if (../../Net castable as xs:decimal) then"); + Console.WriteLine(" format-number(xs:decimal(../../Net), '0.00')"); + Console.WriteLine(" else '0.00'}"); + Console.ResetColor(); + } + } +} diff --git a/XsltDebugger.ConsoleTest/NamespaceExtractionTest.cs b/XsltDebugger.ConsoleTest/NamespaceExtractionTest.cs new file mode 100644 index 0000000..158e956 --- /dev/null +++ b/XsltDebugger.ConsoleTest/NamespaceExtractionTest.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; + +namespace XsltDebugger.ConsoleTest; + +/// +/// Test namespace extraction from various XSLT files +/// +class NamespaceExtractionTest +{ + static void Main(string[] args) + { + Console.WriteLine("=== XSLT Namespace Extraction Test ===\n"); + + var testFiles = new[] + { + "TestData/Integration/xslt/v3/LmlBasedXslt.xslt", + "TestData/Integration/xslt/v2/VariableLoggingSample.xslt", + "TestData/Integration/xslt/v1/VariableLoggingSampleV1.xslt", + "TestData/Integration/xslt/v2/AdvanceXslt2.xslt", + "TestData/Integration/xslt/v3/AdvanceXslt3.xslt", + "TestData/Integration/xslt/v3/ShipmentConf3.xslt" + }; + + foreach (var testFile in testFiles) + { + TestNamespaceExtraction(testFile); + } + + Console.WriteLine("\n=== ALL TESTS COMPLETE ===\n"); + } + + static void TestNamespaceExtraction(string stylesheetPath) + { + var fullPath = Path.GetFullPath(stylesheetPath); + var fileName = Path.GetFileName(stylesheetPath); + + Console.WriteLine($"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + Console.WriteLine($"Testing: {fileName}"); + Console.WriteLine($"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + + if (!File.Exists(fullPath)) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"βœ— File not found: {fullPath}"); + Console.ResetColor(); + Console.WriteLine(); + return; + } + + try + { + var xdoc = XDocument.Load(fullPath); + + if (xdoc.Root == null) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("βœ— No root element found"); + Console.ResetColor(); + Console.WriteLine(); + return; + } + + // Extract namespaces + var namespaces = ExtractNamespaces(xdoc); + + Console.WriteLine($"Found {namespaces.Count} namespace(s):"); + Console.WriteLine(); + + if (namespaces.Count == 0) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(" (No custom namespaces - XML uses no namespaces)"); + Console.ResetColor(); + } + else + { + foreach (var ns in namespaces.OrderBy(kvp => kvp.Key)) + { + var prefix = string.IsNullOrEmpty(ns.Key) ? "(default)" : ns.Key; + var prefixColor = string.IsNullOrEmpty(ns.Key) ? ConsoleColor.Yellow : ConsoleColor.Cyan; + + Console.ForegroundColor = prefixColor; + Console.Write($" {prefix,-15}"); + Console.ResetColor(); + Console.Write(" β†’ "); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine(ns.Value); + Console.ResetColor(); + } + } + + Console.WriteLine(); + + // Show sample XPath expressions + if (namespaces.Count > 0) + { + Console.WriteLine("Sample watch expressions:"); + var sampleNamespaces = namespaces + .Where(kvp => !string.IsNullOrEmpty(kvp.Key)) + .Take(2); + + foreach (var ns in sampleNamespaces) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine($" /{ns.Key}:Element/SubElement"); + Console.ResetColor(); + } + + // If there's a default namespace + if (namespaces.ContainsKey("default")) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine($" /default:Element/SubElement"); + Console.ResetColor(); + } + } + + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("\nβœ“ Extraction successful"); + Console.ResetColor(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"βœ— Error: {ex.Message}"); + Console.ResetColor(); + } + + Console.WriteLine(); + } + + static Dictionary ExtractNamespaces(XDocument xdoc) + { + if (xdoc.Root == null) + { + return new Dictionary(); + } + + const string DebugNamespace = "urn:xslt-debugger"; + var namespaces = new Dictionary(); + + // Extract all namespace declarations from the root element + foreach (var attr in xdoc.Root.Attributes()) + { + if (attr.IsNamespaceDeclaration) + { + var prefix = attr.Name.LocalName == "xmlns" ? string.Empty : attr.Name.LocalName; + var uri = attr.Value; + + // Skip XSLT namespace and debug namespace + if (uri != "http://www.w3.org/1999/XSL/Transform" && + uri != DebugNamespace) + { + // For default namespace (no prefix), also register with "default" prefix + if (string.IsNullOrEmpty(prefix)) + { + namespaces[prefix] = uri; + namespaces["default"] = uri; + } + else + { + namespaces[prefix] = uri; + } + } + } + } + + return namespaces; + } +} diff --git a/XsltDebugger.ConsoleTest/ProgramUsingEngineType.cs b/XsltDebugger.ConsoleTest/ProgramUsingEngineType.cs index 2f47fa2..3606fdd 100644 --- a/XsltDebugger.ConsoleTest/ProgramUsingEngineType.cs +++ b/XsltDebugger.ConsoleTest/ProgramUsingEngineType.cs @@ -35,9 +35,9 @@ static async Task Main(string[] args) // Use different defaults based on engine type var defaultXslt = engineType == "compiled" - ? Path.Combine(DefaultTestDataFolder, "sample-inline-cs-with-usings.xslt") - : Path.Combine(DefaultTestDataFolder, "ShipmentConf3.xslt"); - var defaultXml = Path.Combine(DefaultTestDataFolder, engineType == "compiled" ? "sample-inline-cs-with-usings.xml" : "ShipmentConf-proper.xml"); + ? Path.Combine(DefaultTestDataFolder, "xslt/v1/sample-inline-cs-with-usings.xslt") + : Path.Combine(DefaultTestDataFolder, "xslt/v3/ShipmentConf3.xslt"); + var defaultXml = Path.Combine(DefaultTestDataFolder, engineType == "compiled" ? "xml/sample-inline-cs-with-usings.xml" : "xml/ShipmentConf-proper.xml"); var stylesheetPath = ResolveInput(filteredArgs.ToArray(), 0, defaultXslt); var xmlPath = ResolveInput(filteredArgs.ToArray(), 1, defaultXml); @@ -77,7 +77,8 @@ static async Task Main(string[] args) var fullStylesheetPath = Path.GetFullPath(stylesheetPath); int breakpointLine = stylesheetPath.Contains("message-test") ? 9 : stylesheetPath.Contains("ShipmentConf3") ? 55 : - stylesheetPath.Contains("VariableLoggingSampleV1") ? 8 : 26; + stylesheetPath.Contains("VariableLoggingSampleV1") ? 8 : + stylesheetPath.Contains("step-into-test") ? 12 : 26; engine.SetBreakpoints(new[] { (fullStylesheetPath, breakpointLine) }); Console.WriteLine($" >> Breakpoint set at line {breakpointLine}\n"); @@ -159,6 +160,31 @@ static async Task Main(string[] args) } } + // Display registered stylesheet namespaces + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine("\n 🌐 REGISTERED NAMESPACES:"); + Console.ResetColor(); + if (XsltEngineManager.StylesheetNamespaces.Count > 0) + { + foreach (var kvp in XsltEngineManager.StylesheetNamespaces) + { + var prefix = string.IsNullOrEmpty(kvp.Key) ? "(no prefix)" : kvp.Key; + Console.ForegroundColor = ConsoleColor.Cyan; + Console.Write($" {prefix}"); + Console.ResetColor(); + Console.Write(" β†’ "); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine(kvp.Value); + Console.ResetColor(); + } + } + else + { + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine(" (no custom namespaces)"); + Console.ResetColor(); + } + // Display XSLT variables Console.ForegroundColor = ConsoleColor.White; Console.WriteLine("\n πŸ“Š XSLT VARIABLES:"); diff --git a/XsltDebugger.ConsoleTest/StepIntoTest.cs b/XsltDebugger.ConsoleTest/StepIntoTest.cs index a4b2c55..4a99416 100644 --- a/XsltDebugger.ConsoleTest/StepIntoTest.cs +++ b/XsltDebugger.ConsoleTest/StepIntoTest.cs @@ -18,8 +18,8 @@ static async Task Main(string[] args) Console.WriteLine("=== XSLT Step-Into Test ===\n"); // Use the step-into test files - var stylesheetPath = Path.GetFullPath("TestData/Integration/step-into-test.xslt"); - var xmlPath = Path.GetFullPath("TestData/Integration/step-into-test.xml"); + var stylesheetPath = Path.GetFullPath("TestData/Integration/xslt/tests/step-into-test.xslt"); + var xmlPath = Path.GetFullPath("TestData/Integration/xml/step-into-test.xml"); if (!File.Exists(stylesheetPath)) { diff --git a/XsltDebugger.ConsoleTest/XsltDebugger.ConsoleTest.csproj b/XsltDebugger.ConsoleTest/XsltDebugger.ConsoleTest.csproj index e891e63..12c6258 100644 --- a/XsltDebugger.ConsoleTest/XsltDebugger.ConsoleTest.csproj +++ b/XsltDebugger.ConsoleTest/XsltDebugger.ConsoleTest.csproj @@ -20,12 +20,16 @@ - + + + - + + + diff --git a/XsltDebugger.DebugAdapter/DapServer.cs b/XsltDebugger.DebugAdapter/DapServer.cs index e6169b0..7d4451b 100644 --- a/XsltDebugger.DebugAdapter/DapServer.cs +++ b/XsltDebugger.DebugAdapter/DapServer.cs @@ -728,11 +728,29 @@ private object EvaluateXPath(XPathNavigator navigator, string expression) private static XmlNamespaceManager CreateNamespaceManager(XPathNavigator navigator) { var manager = new XmlNamespaceManager(navigator.NameTable); + + // Add namespaces from the current XML context foreach (var kvp in navigator.GetNamespacesInScope(XmlNamespaceScope.All)) { var prefix = string.IsNullOrEmpty(kvp.Key) ? string.Empty : kvp.Key; manager.AddNamespace(prefix, kvp.Value); } + + // Add namespaces from the XSLT stylesheet + // These take precedence for XPath evaluation in watch expressions + foreach (var kvp in XsltEngineManager.StylesheetNamespaces) + { + try + { + // AddNamespace will update if prefix already exists + manager.AddNamespace(kvp.Key, kvp.Value); + } + catch + { + // Ignore errors from duplicate/invalid namespace registrations + } + } + return manager; } diff --git a/XsltDebugger.DebugAdapter/SaxonEngine.cs b/XsltDebugger.DebugAdapter/SaxonEngine.cs index 2799034..f0915e5 100644 --- a/XsltDebugger.DebugAdapter/SaxonEngine.cs +++ b/XsltDebugger.DebugAdapter/SaxonEngine.cs @@ -131,6 +131,9 @@ await Task.Run(() => return; } + // Extract and register stylesheet namespaces for XPath evaluation in watch expressions + ExtractAndRegisterNamespaces(xdoc); + // Instrument the stylesheet for debugging var version = XsltCompiledEngine.GetXsltVersion(xdoc.Root); if (XsltEngineManager.IsLogEnabled) @@ -667,6 +670,45 @@ private static string NormalizePath(string path) return result; } + private static void ExtractAndRegisterNamespaces(XDocument xdoc) + { + if (xdoc.Root == null) + { + return; + } + + var namespaces = new Dictionary(); + + // Extract all namespace declarations from the root element + foreach (var attr in xdoc.Root.Attributes()) + { + if (attr.IsNamespaceDeclaration) + { + var prefix = attr.Name.LocalName == "xmlns" ? string.Empty : attr.Name.LocalName; + var uri = attr.Value; + + // Skip XSLT namespace and debug namespace + if (uri != "http://www.w3.org/1999/XSL/Transform" && + uri != DebugNamespace) + { + // For default namespace (no prefix), also register with "default" prefix + // This allows XPath expressions to use "default:ElementName" syntax + if (string.IsNullOrEmpty(prefix)) + { + namespaces[prefix] = uri; + namespaces["default"] = uri; + } + else + { + namespaces[prefix] = uri; + } + } + } + } + + XsltEngineManager.RegisterStylesheetNamespaces(namespaces); + } + private void EnsureDebugNamespace(XDocument doc) { if (doc.Root == null) diff --git a/XsltDebugger.DebugAdapter/XsltCompiledEngine.cs b/XsltDebugger.DebugAdapter/XsltCompiledEngine.cs index a4159b8..5288cf0 100644 --- a/XsltDebugger.DebugAdapter/XsltCompiledEngine.cs +++ b/XsltDebugger.DebugAdapter/XsltCompiledEngine.cs @@ -137,6 +137,9 @@ await Task.Run(() => return; } + // Extract and register stylesheet namespaces for XPath evaluation in watch expressions + ExtractAndRegisterNamespaces(xdoc); + // XSLT 1.0 with XslCompiledTransform XNamespace msxsl = "urn:schemas-microsoft-com:xslt"; var scripts = xdoc.Descendants(msxsl + "script").ToList(); @@ -453,6 +456,45 @@ private static string NormalizePath(string path) return result; } + private static void ExtractAndRegisterNamespaces(XDocument xdoc) + { + if (xdoc.Root == null) + { + return; + } + + var namespaces = new Dictionary(); + + // Extract all namespace declarations from the root element + foreach (var attr in xdoc.Root.Attributes()) + { + if (attr.IsNamespaceDeclaration) + { + var prefix = attr.Name.LocalName == "xmlns" ? string.Empty : attr.Name.LocalName; + var uri = attr.Value; + + // Skip XSLT namespace and debug namespace + if (uri != "http://www.w3.org/1999/XSL/Transform" && + uri != DebugNamespace) + { + // For default namespace (no prefix), also register with "default" prefix + // This allows XPath expressions to use "default:ElementName" syntax + if (string.IsNullOrEmpty(prefix)) + { + namespaces[prefix] = uri; + namespaces["default"] = uri; + } + else + { + namespaces[prefix] = uri; + } + } + } + } + + XsltEngineManager.RegisterStylesheetNamespaces(namespaces); + } + private void EnsureDebugNamespace(XDocument doc) { if (doc.Root == null) diff --git a/XsltDebugger.DebugAdapter/XsltEngineManager.cs b/XsltDebugger.DebugAdapter/XsltEngineManager.cs index 4e185f5..508f69b 100644 --- a/XsltDebugger.DebugAdapter/XsltEngineManager.cs +++ b/XsltDebugger.DebugAdapter/XsltEngineManager.cs @@ -18,6 +18,9 @@ public static class XsltEngineManager // XSLT Variables storage public static Dictionary Variables { get; private set; } = new(); + // XSLT Stylesheet namespaces (prefix -> URI mapping) + public static Dictionary StylesheetNamespaces { get; private set; } = new(); + public static bool DebugEnabled { get; private set; } = true; public static LogLevel CurrentLogLevel { get; private set; } = LogLevel.Log; @@ -104,6 +107,25 @@ public static void ClearVariables() } } + public static void RegisterStylesheetNamespaces(Dictionary namespaces) + { + StylesheetNamespaces = new Dictionary(namespaces); + if (IsTraceEnabled) + { + var count = namespaces.Count; + NotifyOutput($"[trace] Registered {count} stylesheet namespace(s)"); + } + } + + public static void ClearStylesheetNamespaces() + { + StylesheetNamespaces.Clear(); + if (IsTraceAllEnabled) + { + NotifyOutput("[traceall] Stylesheet namespaces cleared"); + } + } + private static string FormatValue(object? value) { if (value == null) return "null"; @@ -120,6 +142,7 @@ public static void Reset() DebugEnabled = true; CurrentLogLevel = LogLevel.Log; ClearVariables(); + ClearStylesheetNamespaces(); } } diff --git a/XsltDebugger.Tests/CompiledEngineIntegrationTests.cs b/XsltDebugger.Tests/CompiledEngineIntegrationTests.cs index 19184fa..e4f50c1 100644 --- a/XsltDebugger.Tests/CompiledEngineIntegrationTests.cs +++ b/XsltDebugger.Tests/CompiledEngineIntegrationTests.cs @@ -27,8 +27,8 @@ private static string GetTestDataPath(string relativePath) [Fact] public async Task CompiledEngine_ShouldTransformInlineScriptSample_WithTraceLogging() { - var stylesheetPath = GetTestDataPath("Integration/sample-inline-cs-with-usings.xslt"); - var xmlPath = GetTestDataPath("Integration/sample-inline-cs-with-usings.xml"); + var stylesheetPath = GetTestDataPath("Integration/xslt/v1/sample-inline-cs-with-usings.xslt"); + var xmlPath = GetTestDataPath("Integration/xml/sample-inline-cs-with-usings.xml"); var fullStylesheetPath = Path.GetFullPath(stylesheetPath); var fullXmlPath = Path.GetFullPath(xmlPath); var breakpoints = new[] { (fullStylesheetPath, 24) }; @@ -108,8 +108,8 @@ void OnStopped(string file, int line, DebugStopReason reason) [Fact] public async Task CompiledEngine_ShouldCaptureVariablesAndHitBreakpoints() { - var stylesheetPath = GetTestDataPath("Integration/VariableLoggingSampleV1.xslt"); - var xmlPath = GetTestDataPath("Integration/VariableLoggingSampleV1Input.xml"); + var stylesheetPath = GetTestDataPath("Integration/xslt/v1/VariableLoggingSampleV1.xslt"); + var xmlPath = GetTestDataPath("Integration/xml/VariableLoggingSampleV1Input.xml"); var fullStylesheetPath = Path.GetFullPath(stylesheetPath); var fullXmlPath = Path.GetFullPath(xmlPath); var breakpoints = new[] { (fullStylesheetPath, 8) }; diff --git a/XsltDebugger.Tests/SaxonEngineIntegrationTests.cs b/XsltDebugger.Tests/SaxonEngineIntegrationTests.cs index c4f4527..eb5750a 100644 --- a/XsltDebugger.Tests/SaxonEngineIntegrationTests.cs +++ b/XsltDebugger.Tests/SaxonEngineIntegrationTests.cs @@ -82,8 +82,8 @@ void OnOutput(string message) [Fact] public async Task SaxonEngine_ShouldCaptureVariablesAndHitBreakpoints() { - var stylesheetPath = GetTestDataPath("Integration/VariableLoggingSample.xslt"); - var xmlPath = GetTestDataPath("Integration/ItemsSample.xml"); + var stylesheetPath = GetTestDataPath("Integration/xslt/v2/VariableLoggingSample.xslt"); + var xmlPath = GetTestDataPath("Integration/xml/ItemsSample.xml"); var fullStylesheetPath = Path.GetFullPath(stylesheetPath); var fullXmlPath = Path.GetFullPath(xmlPath); var breakpoints = new[] { (fullStylesheetPath, 8) }; @@ -159,8 +159,8 @@ void OnStopped(string file, int line, DebugStopReason reason) [Fact] public async Task SaxonEngine_ShouldReportCompilationErrors() { - var stylesheetPath = GetTestDataPath("Integration/InvalidFunctionCall.xslt"); - var xmlPath = GetTestDataPath("Integration/ItemsSample.xml"); + var stylesheetPath = GetTestDataPath("Integration/xslt/tests/InvalidFunctionCall.xslt"); + var xmlPath = GetTestDataPath("Integration/xml/ItemsSample.xml"); var fullStylesheetPath = Path.GetFullPath(stylesheetPath); var fullXmlPath = Path.GetFullPath(xmlPath); var engine = new SaxonEngine(); @@ -210,8 +210,8 @@ void OnOutput(string message) [Fact] public async Task SaxonEngine_ShouldTransformShipmentSample_WithTraceLogging() { - var stylesheetPath = GetTestDataPath("Integration/ShipmentConf3.xslt"); - var xmlPath = GetTestDataPath("Integration/ShipmentConf-proper.xml"); + var stylesheetPath = GetTestDataPath("Integration/xslt/v3/ShipmentConf3.xslt"); + var xmlPath = GetTestDataPath("Integration/xml/ShipmentConf-proper.xml"); var fullStylesheetPath = Path.GetFullPath(stylesheetPath); var fullXmlPath = Path.GetFullPath(xmlPath); var engine = new SaxonEngine(); @@ -264,8 +264,8 @@ void OnOutput(string message) [Fact] public async Task SaxonEngine_ShouldRespectVariableInstrumentationGuardrails() { - var stylesheetPath = GetTestDataPath("Integration/test-guardrails.xslt"); - var xmlPath = GetTestDataPath("Integration/sample.xml"); + var stylesheetPath = GetTestDataPath("Integration/xslt/tests/test-guardrails.xslt"); + var xmlPath = GetTestDataPath("Integration/xml/sample.xml"); var fullStylesheetPath = Path.GetFullPath(stylesheetPath); var fullXmlPath = Path.GetFullPath(xmlPath); var engine = new SaxonEngine(); @@ -321,7 +321,7 @@ void OnOutput(string message) [Fact] public async Task SaxonEngine_ShouldTransformAdvancedXslt2_WithInstrumentation() { - var (log, outFile, exitCode) = await RunSaxonAsync("Integration/AdvanceXslt2.xslt", "Integration/AdvanceFile.xml"); + var (log, outFile, exitCode) = await RunSaxonAsync("Integration/xslt/v2/AdvanceXslt2.xslt", "Integration/xml/AdvanceFile.xml"); try { exitCode.Should().Be(0); @@ -344,7 +344,7 @@ public async Task SaxonEngine_ShouldTransformAdvancedXslt2_WithInstrumentation() [Fact] public async Task SaxonEngine_ShouldTransformAdvancedXslt3_WithAccumulatorInstrumentation() { - var (log, outFile, exitCode) = await RunSaxonAsync("Integration/AdvanceXslt3.xslt", "Integration/AdvanceFile.xml"); + var (log, outFile, exitCode) = await RunSaxonAsync("Integration/xslt/v3/AdvanceXslt3.xslt", "Integration/xml/AdvanceFile.xml"); try { exitCode.Should().Be(0); diff --git a/XsltDebugger.Tests/StepIntoTests.cs b/XsltDebugger.Tests/StepIntoTests.cs index 9484a3e..0f8a43e 100644 --- a/XsltDebugger.Tests/StepIntoTests.cs +++ b/XsltDebugger.Tests/StepIntoTests.cs @@ -267,8 +267,8 @@ public async Task SaxonEngine_StepOver_ShouldStayAtSameDepth() [Fact] public async Task XsltCompiledEngine_StepOver_CallTemplate_ShouldPauseAfterReturn() { - var xsltPath = GetTestDataPath("Integration/step-into-test.xslt"); - var xmlPath = GetTestDataPath("Integration/step-into-test.xml"); + var xsltPath = GetTestDataPath("Integration/xslt/tests/step-into-test.xslt"); + var xmlPath = GetTestDataPath("Integration/xml/step-into-test.xml"); var lines = File.ReadAllLines(xsltPath); var callLine = Array.FindIndex(lines, line => line.Contains(@" line.Contains(@" line.Contains(@" line.Contains(@"&1 | \ + grep -A 20 "REGISTERED NAMESPACES" | head -25 +echo "" + +# Test 2: AdvanceXslt2 (no namespaces in XML) +echo "Test 2: AdvanceXslt2.xslt (XSLT 2.0, XML without namespaces)" +echo "----------------------------------------" +dotnet run --project XsltDebugger.ConsoleTest/XsltDebugger.ConsoleTest.csproj -- \ + AdvanceXslt2.xslt AdvanceFile.xml --engine saxon 2>&1 | \ + grep -A 20 "REGISTERED NAMESPACES" | head -25 +echo "" + +# Test 3: ShipmentConf3 (XSLT 3.0) +echo "Test 3: ShipmentConf3.xslt (XSLT 3.0 with custom functions)" +echo "----------------------------------------" +dotnet run --project XsltDebugger.ConsoleTest/XsltDebugger.ConsoleTest.csproj -- \ + ShipmentConf3.xslt ShipmentConf-proper.xml --engine saxon 2>&1 | \ + grep -A 20 "REGISTERED NAMESPACES" | head -25 +echo "" + +# Test 4: VariableLoggingSampleV1 (XSLT 1.0) +echo "Test 4: VariableLoggingSampleV1.xslt (XSLT 1.0)" +echo "----------------------------------------" +dotnet run --project XsltDebugger.ConsoleTest/XsltDebugger.ConsoleTest.csproj -- \ + VariableLoggingSampleV1.xslt VariableLoggingSampleV1Input.xml --engine compiled 2>&1 | \ + grep -A 20 "REGISTERED NAMESPACES" | head -25 +echo "" + +echo "==================================" +echo "All tests complete!" +echo "==================================" From 194b9d805a8f0f67d823a40b2ef11d94b4120206 Mon Sep 17 00:00:00 2001 From: Daniel Jonathan Date: Fri, 31 Oct 2025 20:34:30 +0100 Subject: [PATCH 2/4] updated to xpath ns support --- .../xslt/v1/ns-mapping-sample.xslt | 47 ++++++ XsltDebugger.DebugAdapter/SaxonEngine.cs | 99 +++++++++--- .../XsltCompiledEngine.cs | 153 +++++++++++++++++- package.json | 6 +- 4 files changed, 279 insertions(+), 26 deletions(-) create mode 100644 TestData/Integration/xslt/v1/ns-mapping-sample.xslt diff --git a/TestData/Integration/xslt/v1/ns-mapping-sample.xslt b/TestData/Integration/xslt/v1/ns-mapping-sample.xslt new file mode 100644 index 0000000..f0b78af --- /dev/null +++ b/TestData/Integration/xslt/v1/ns-mapping-sample.xslt @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/XsltDebugger.DebugAdapter/SaxonEngine.cs b/XsltDebugger.DebugAdapter/SaxonEngine.cs index f0915e5..35ee6d5 100644 --- a/XsltDebugger.DebugAdapter/SaxonEngine.cs +++ b/XsltDebugger.DebugAdapter/SaxonEngine.cs @@ -490,21 +490,22 @@ private bool IsBreakpointHit(string file, int line) xmlDoc.LoadXml(xmlString); // Create navigator from the .NET XmlDocument - var navigator = xmlDoc.CreateNavigator(); + var navigator = xmlDoc.CreateNavigator() ?? throw new InvalidOperationException("Failed to create XPath navigator for serialized Saxon context."); // Now navigate to the equivalent position of the original context node // We'll use the XPath to the original node to position the navigator correctly var pathToContext = GetXPathToNode(context); if (!string.IsNullOrEmpty(pathToContext) && pathToContext != "/") { - var contextNavigator = navigator.SelectSingleNode(pathToContext); + var nsManager = BuildNamespaceManager(xmlDoc); + var contextNavigator = navigator.SelectSingleNode(pathToContext, nsManager); if (contextNavigator != null) { if (XsltEngineManager.TraceEnabled) { XsltEngineManager.NotifyOutput($"[trace] ConvertSaxonNodeToNavigator: positioned at {pathToContext}"); } - return contextNavigator; + return contextNavigator.Clone(); } } @@ -521,6 +522,35 @@ private bool IsBreakpointHit(string file, int line) } } + private static XmlNamespaceManager BuildNamespaceManager(XmlDocument xmlDoc) + { + var manager = new XmlNamespaceManager(xmlDoc.NameTable); + if (xmlDoc.DocumentElement == null) + { + return manager; + } + + try + { + var navigator = xmlDoc.CreateNavigator(); + if (navigator != null && navigator.MoveToFirstChild()) + { + foreach (var kvp in navigator.GetNamespacesInScope(XmlNamespaceScope.All)) + { + var prefix = kvp.Key ?? string.Empty; + try { manager.AddNamespace(prefix, kvp.Value); } + catch { /* ignore duplicates */ } + } + } + } + catch + { + // Ignore namespace extraction issues; fall back to empty manager + } + + return manager; + } + private string GetXPathToNode(XdmNode node) { // Build the XPath to this node from the root @@ -529,27 +559,31 @@ private string GetXPathToNode(XdmNode node) while (current != null && current.NodeKind != XmlNodeType.Document) { - if (current.NodeKind == XmlNodeType.Element) + switch (current.NodeKind) { - var name = current.NodeName?.LocalName ?? ""; - // Count preceding siblings with the same name for position predicate - var position = 1; - var sibling = current; - - // Move to parent, then iterate children to find position - var parent = current.Parent as XdmNode; - if (parent != null) + case XmlNodeType.Element: { - var children = parent.Children().ToList(); - var index = 0; - foreach (var child in children) + var nodeName = current.NodeName; + var localName = nodeName?.LocalName ?? string.Empty; + var prefix = nodeName?.Prefix ?? string.Empty; + var qualifiedName = string.IsNullOrEmpty(prefix) ? localName : $"{prefix}:{localName}"; + + var position = 1; + var parent = current.Parent as XdmNode; + if (parent != null) { - if (child is XdmNode childNode && childNode.NodeKind == XmlNodeType.Element) + var siblings = parent.Children().OfType() + .Where(child => child.NodeKind == XmlNodeType.Element) + .ToList(); + + var index = 0; + foreach (var sibling in siblings) { - if (childNode.NodeName?.LocalName == name) + var siblingName = sibling.NodeName; + if (nodeName != null && nodeName.Equals(siblingName)) { index++; - if (childNode.Implementation == current.Implementation) + if (sibling.Implementation == current.Implementation) { position = index; break; @@ -557,9 +591,27 @@ private string GetXPathToNode(XdmNode node) } } } - } - pathParts.Insert(0, $"{name}[{position}]"); + pathParts.Insert(0, $"{qualifiedName}[{position}]"); + break; + } + case XmlNodeType.Attribute: + { + var nodeName = current.NodeName; + var localName = nodeName?.LocalName ?? string.Empty; + var prefix = nodeName?.Prefix ?? string.Empty; + var qualifiedName = string.IsNullOrEmpty(prefix) ? localName : $"{prefix}:{localName}"; + pathParts.Insert(0, $"@{qualifiedName}"); + break; + } + case XmlNodeType.Text: + { + pathParts.Insert(0, "text()"); + break; + } + default: + pathParts.Insert(0, current.NodeKind.ToString()); + break; } current = current.Parent as XdmNode; @@ -814,6 +866,13 @@ private void InstrumentStylesheet(XDocument doc) var isXsltElement = element.Name.Namespace == xsltNamespace; var lineNumber = line!.Value; + if (isXsltElement && + string.Equals(element.Name.LocalName, "template", StringComparison.OrdinalIgnoreCase) && + element.IsEmpty) + { + continue; + } + if (parentIsXslt && string.Equals(parent.Name.LocalName, "choose", StringComparison.OrdinalIgnoreCase)) { if (!(isXsltElement && diff --git a/XsltDebugger.DebugAdapter/XsltCompiledEngine.cs b/XsltDebugger.DebugAdapter/XsltCompiledEngine.cs index 5288cf0..14f32ba 100644 --- a/XsltDebugger.DebugAdapter/XsltCompiledEngine.cs +++ b/XsltDebugger.DebugAdapter/XsltCompiledEngine.cs @@ -334,19 +334,20 @@ internal void RegisterBreakpointHit(string file, int line, XPathNavigator? conte } // Always update the context for evaluation, even when not pausing - XsltEngineManager.UpdateContext(contextNode); + var clonedContext = CloneContextNavigator(contextNode); + XsltEngineManager.UpdateContext(clonedContext); // Check if we hit a user-set breakpoint if (!isTemplateExit && IsBreakpointHit(normalized, line)) { - PauseForBreakpoint(normalized, line, DebugStopReason.Breakpoint, contextNode); + PauseForBreakpoint(normalized, line, DebugStopReason.Breakpoint, clonedContext); return; } // Check if we should stop based on step mode if (ShouldStopForStep(normalized, line, isTemplateExit)) { - PauseForBreakpoint(normalized, line, DebugStopReason.Step, contextNode); + PauseForBreakpoint(normalized, line, DebugStopReason.Step, clonedContext); } } @@ -628,6 +629,152 @@ private static void EnsureTemplateExitBreakpoint(XElement templateElement, int l templateElement.Add(exitCall); } + private static XPathNavigator? CloneContextNavigator(XPathNavigator? context) + { + if (context == null) + { + return null; + } + + try + { + var originalClone = context.Clone(); + var pathToContext = GetXPathToNavigator(originalClone); + + // Move to the document root and capture the XML backing the navigator + originalClone.MoveToRoot(); + if (!originalClone.MoveToFirstChild()) + { + return context.Clone(); + } + + while (originalClone.NodeType != XPathNodeType.Element) + { + if (!originalClone.MoveToNext()) + { + return context.Clone(); + } + } + + var xml = originalClone.OuterXml; + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(xml); + + var navigator = xmlDoc.CreateNavigator(); + if (navigator == null) + { + return context.Clone(); + } + + if (!string.IsNullOrEmpty(pathToContext) && pathToContext != "/") + { + var nsManager = BuildNamespaceManager(xmlDoc); + var positioned = navigator.SelectSingleNode(pathToContext, nsManager); + if (positioned != null) + { + return positioned.Clone(); + } + } + + return navigator; + } + catch + { + try + { + return context.Clone(); + } + catch + { + return null; + } + } + } + + private static XmlNamespaceManager BuildNamespaceManager(XmlDocument xmlDoc) + { + var manager = new XmlNamespaceManager(xmlDoc.NameTable); + if (xmlDoc.DocumentElement == null) + { + return manager; + } + + try + { + var navigator = xmlDoc.CreateNavigator(); + if (navigator != null && navigator.MoveToFirstChild()) + { + foreach (var kvp in navigator.GetNamespacesInScope(XmlNamespaceScope.All)) + { + var prefix = kvp.Key ?? string.Empty; + try { manager.AddNamespace(prefix, kvp.Value); } + catch { /* ignore duplicates */ } + } + } + } + catch + { + // Ignore namespace extraction issues; fall back to empty manager + } + + return manager; + } + + private static string GetXPathToNavigator(XPathNavigator navigator) + { + try + { + var pathParts = new List(); + var current = navigator.Clone(); + + while (current.NodeType != XPathNodeType.Root) + { + switch (current.NodeType) + { + case XPathNodeType.Element: + { + var position = 1; + var sibling = current.Clone(); + while (sibling.MoveToPrevious()) + { + if (sibling.Name == current.Name) + { + position++; + } + } + + pathParts.Insert(0, $"{current.Name}[{position}]"); + break; + } + case XPathNodeType.Attribute: + { + pathParts.Insert(0, $"@{current.Name}"); + break; + } + case XPathNodeType.Text: + { + pathParts.Insert(0, "text()"); + break; + } + default: + pathParts.Insert(0, current.NodeType.ToString()); + break; + } + + if (!current.MoveToParent()) + { + break; + } + } + + return pathParts.Count > 0 ? "/" + string.Join("/", pathParts) : "/"; + } + catch + { + return "/"; + } + } + private static bool ShouldInstrument(XElement element, XNamespace xsltNamespace) { if (element.Parent == null) diff --git a/package.json b/package.json index c2654d0..60c9378 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "xsltdebugger", - "displayName": "XSLT Debugger", + "name": "xsltdebugger-darwin", + "displayName": "XSLT Debugger for macOS-arm64", "description": "A VS Code extension for debugging XSLT using a .NET Debug Adapter.", - "version": "0.6.0", + "version": "0.7.0", "publisher": "DanielJonathan", "repository": { "type": "git", From 84ba125072feb7b00ff7f96b22d2484f81513994 Mon Sep 17 00:00:00 2001 From: Daniel Jonathan Date: Sat, 1 Nov 2025 13:41:47 +0100 Subject: [PATCH 3/4] updated for xslt 1.0 inline supportin for logs... and foreach xsl message and variable mapping --- TestData/Integration/xml/foreach-test.xml | 13 + .../xml/sample-inline-cs-auto-instrument.xml | 4 + .../Integration/xslt/v1/ShipmentConfv1.xslt | 98 +++++ .../Integration/xslt/v1/foreach-test.xslt | 26 ++ .../v1/sample-inline-cs-auto-instrument.xslt | 42 ++ .../xslt/v1/sample-inline-cs-with-usings.xslt | 8 +- .../Integration/xslt/v1/sample-inline-cs.xslt | 6 +- .../InlineCSharpInstrumenter.cs | 139 ++++++ XsltDebugger.DebugAdapter/InlineXsltLogger.cs | 75 ++++ .../XsltCompiledEngine.cs | 124 +++++- .../XsltEngineManager.cs | 7 + .../CompiledEngineIntegrationTests.cs | 401 +++++++++++++++++- docs/EngineFeatureComparison.md | 48 +++ 13 files changed, 968 insertions(+), 23 deletions(-) create mode 100644 TestData/Integration/xml/foreach-test.xml create mode 100644 TestData/Integration/xml/sample-inline-cs-auto-instrument.xml create mode 100644 TestData/Integration/xslt/v1/ShipmentConfv1.xslt create mode 100644 TestData/Integration/xslt/v1/foreach-test.xslt create mode 100644 TestData/Integration/xslt/v1/sample-inline-cs-auto-instrument.xslt create mode 100644 XsltDebugger.DebugAdapter/InlineCSharpInstrumenter.cs create mode 100644 XsltDebugger.DebugAdapter/InlineXsltLogger.cs create mode 100644 docs/EngineFeatureComparison.md diff --git a/TestData/Integration/xml/foreach-test.xml b/TestData/Integration/xml/foreach-test.xml new file mode 100644 index 0000000..b1df97a --- /dev/null +++ b/TestData/Integration/xml/foreach-test.xml @@ -0,0 +1,13 @@ + + + + First + Second + Third + + + Low + High + Medium + + diff --git a/TestData/Integration/xml/sample-inline-cs-auto-instrument.xml b/TestData/Integration/xml/sample-inline-cs-auto-instrument.xml new file mode 100644 index 0000000..1b07c33 --- /dev/null +++ b/TestData/Integration/xml/sample-inline-cs-auto-instrument.xml @@ -0,0 +1,4 @@ + + + Auto-instrumentation test data + diff --git a/TestData/Integration/xslt/v1/ShipmentConfv1.xslt b/TestData/Integration/xslt/v1/ShipmentConfv1.xslt new file mode 100644 index 0000000..c07d106 --- /dev/null +++ b/TestData/Integration/xslt/v1/ShipmentConfv1.xslt @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + Logic App Tool Shipper + + + + + Hello + + + / + + + + + + + + KG + + + + + + + , + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestData/Integration/xslt/v1/foreach-test.xslt b/TestData/Integration/xslt/v1/foreach-test.xslt new file mode 100644 index 0000000..1e55632 --- /dev/null +++ b/TestData/Integration/xslt/v1/foreach-test.xslt @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TestData/Integration/xslt/v1/sample-inline-cs-auto-instrument.xslt b/TestData/Integration/xslt/v1/sample-inline-cs-auto-instrument.xslt new file mode 100644 index 0000000..065ba19 --- /dev/null +++ b/TestData/Integration/xslt/v1/sample-inline-cs-auto-instrument.xslt @@ -0,0 +1,42 @@ + + + + + + + + + + + Auto-Instrumentation Test + + +

C# Method Auto-Instrumentation Test

+

Add 5 + 3 =

+

Multiply 4 * 7 =

+

Formatted:

+ + +
+ +
diff --git a/TestData/Integration/xslt/v1/sample-inline-cs-with-usings.xslt b/TestData/Integration/xslt/v1/sample-inline-cs-with-usings.xslt index e5462fc..cd76ddf 100644 --- a/TestData/Integration/xslt/v1/sample-inline-cs-with-usings.xslt +++ b/TestData/Integration/xslt/v1/sample-inline-cs-with-usings.xslt @@ -10,12 +10,14 @@ using System.Globalization; public class DateFormatter { public string FormatCurrentDate() { - return DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + LogEntry(); + return LogReturn(DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); } public string AddDays(string dateStr, int days) { + LogEntry(new { dateStr, days }); DateTime date = DateTime.Parse(dateStr); - return date.AddDays(days).ToString("yyyy-MM-dd"); + return LogReturn(date.AddDays(days).ToString("yyyy-MM-dd")); } } ]]> @@ -42,4 +44,4 @@ public class DateFormatter { - \ No newline at end of file + diff --git a/TestData/Integration/xslt/v1/sample-inline-cs.xslt b/TestData/Integration/xslt/v1/sample-inline-cs.xslt index 6d4c754..017de9e 100644 --- a/TestData/Integration/xslt/v1/sample-inline-cs.xslt +++ b/TestData/Integration/xslt/v1/sample-inline-cs.xslt @@ -6,7 +6,7 @@ exclude-result-prefixes="msxsl user"> - + @@ -21,8 +21,8 @@ - + - + \ No newline at end of file diff --git a/XsltDebugger.DebugAdapter/InlineCSharpInstrumenter.cs b/XsltDebugger.DebugAdapter/InlineCSharpInstrumenter.cs new file mode 100644 index 0000000..8e6f623 --- /dev/null +++ b/XsltDebugger.DebugAdapter/InlineCSharpInstrumenter.cs @@ -0,0 +1,139 @@ +using System; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace XsltDebugger.DebugAdapter; + +/// +/// Instruments inline C# methods to automatically log entry and return values +/// using InlineXsltLogger. +/// +public class InlineCSharpInstrumenter : CSharpSyntaxRewriter +{ + private int _instrumentedMethodCount = 0; + + public int InstrumentedMethodCount => _instrumentedMethodCount; + + public static string Instrument(string sourceCode) + { + var tree = CSharpSyntaxTree.ParseText(sourceCode); + var root = tree.GetRoot(); + + var instrumenter = new InlineCSharpInstrumenter(); + var instrumentedRoot = instrumenter.Visit(root); + + if (XsltEngineManager.IsLogEnabled && instrumenter.InstrumentedMethodCount > 0) + { + XsltEngineManager.NotifyOutput($"[debug] Instrumented {instrumenter.InstrumentedMethodCount} inline C# method(s)"); + } + + return instrumentedRoot.ToFullString(); + } + + public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node) + { + // Skip if not public or if void return type + if (!node.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword)) || + node.ReturnType is PredefinedTypeSyntax predefined && predefined.Keyword.IsKind(SyntaxKind.VoidKeyword)) + { + return base.VisitMethodDeclaration(node); + } + + // Skip if method body is null (abstract/interface methods) + if (node.Body == null) + { + return base.VisitMethodDeclaration(node); + } + + // Check if already instrumented (contains LogEntry or LogReturn calls) + if (AlreadyInstrumented(node)) + { + return base.VisitMethodDeclaration(node); + } + + _instrumentedMethodCount++; + + // Create LogEntry statement with parameters + var logEntryStatement = CreateLogEntryStatement(node); + + // Instrument all return statements with LogReturn + var instrumentedBody = (BlockSyntax)new ReturnStatementInstrumenter().Visit(node.Body); + + // Add LogEntry as first statement in method body + var newStatements = instrumentedBody.Statements.Insert(0, logEntryStatement); + var newBody = instrumentedBody.WithStatements(newStatements); + + return node.WithBody(newBody); + } + + private bool AlreadyInstrumented(MethodDeclarationSyntax node) + { + if (node.Body == null) return false; + + // Check if method contains LogEntry or LogReturn calls + var hasLogCalls = node.Body.DescendantNodes() + .OfType() + .Any(invocation => + { + var identifierName = invocation.Expression as IdentifierNameSyntax; + return identifierName?.Identifier.Text is "LogEntry" or "LogReturn"; + }); + + return hasLogCalls; + } + + private StatementSyntax CreateLogEntryStatement(MethodDeclarationSyntax method) + { + var parameters = method.ParameterList.Parameters; + + if (parameters.Count == 0) + { + // LogEntry(); + return SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.IdentifierName("LogEntry"))); + } + else + { + // LogEntry(new { param1, param2, ... }); + var anonymousObjectProperties = parameters.Select(p => + SyntaxFactory.AnonymousObjectMemberDeclarator( + SyntaxFactory.IdentifierName(p.Identifier.Text))); + + var anonymousObject = SyntaxFactory.AnonymousObjectCreationExpression( + SyntaxFactory.SeparatedList(anonymousObjectProperties)); + + return SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.IdentifierName("LogEntry"), + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument(anonymousObject))))); + } + } + + /// + /// Rewrites return statements to wrap the return value with LogReturn() + /// + private class ReturnStatementInstrumenter : CSharpSyntaxRewriter + { + public override SyntaxNode? VisitReturnStatement(ReturnStatementSyntax node) + { + if (node.Expression == null) + { + return base.VisitReturnStatement(node); + } + + // Wrap: return expr; => return LogReturn(expr); + var logReturnInvocation = SyntaxFactory.InvocationExpression( + SyntaxFactory.IdentifierName("LogReturn"), + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument(node.Expression)))); + + return node.WithExpression(logReturnInvocation); + } + } +} diff --git a/XsltDebugger.DebugAdapter/InlineXsltLogger.cs b/XsltDebugger.DebugAdapter/InlineXsltLogger.cs new file mode 100644 index 0000000..ce6bfcc --- /dev/null +++ b/XsltDebugger.DebugAdapter/InlineXsltLogger.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections; +using System.Linq; + +namespace XsltDebugger.DebugAdapter; + +public static class InlineXsltLogger +{ + public static void Log(string message) + { + if (!string.IsNullOrWhiteSpace(message)) + { + XsltEngineManager.NotifyOutput($"[inline] {message}"); + } + } + + public static void Log(string format, params object[] args) + { + try + { + Log(string.Format(format, args)); + } + catch (FormatException) + { + Log(format); + } + } + + public static void LogEntry(object? parameters = null, + [System.Runtime.CompilerServices.CallerMemberName] string member = "", + [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) + { + var xsltLine = GetXsltCallerLine(); + var prefix = xsltLine.HasValue ? $"[{member}:L{lineNumber}@XSLT:{xsltLine.Value}]" : $"[{member}:L{lineNumber}]"; + + if (parameters == null) + { + Log($"{prefix} entered."); + } + else + { + Log($"{prefix} args = {FormatObject(parameters)}"); + } + } + + public static T LogReturn(T value, + [System.Runtime.CompilerServices.CallerMemberName] string member = "", + [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) + { + var xsltLine = GetXsltCallerLine(); + var prefix = xsltLine.HasValue ? $"[{member}:L{lineNumber}@XSLT:{xsltLine.Value}]" : $"[{member}:L{lineNumber}]"; + Log($"{prefix} return = {FormatObject(value)}"); + return value; + } + + private static int? GetXsltCallerLine() + { + return XsltEngineManager.LastStop?.line; + } + + private static string FormatObject(object? value) + { + if (value == null) + { + return "null"; + } + + return value switch + { + string s => s, + System.Collections.IEnumerable enumerable and not string => "[" + string.Join(", ", enumerable.Cast().Select(FormatObject)) + "]", + _ => value.ToString() ?? value.GetType().Name + }; + } +} diff --git a/XsltDebugger.DebugAdapter/XsltCompiledEngine.cs b/XsltDebugger.DebugAdapter/XsltCompiledEngine.cs index 14f32ba..3509ee5 100644 --- a/XsltDebugger.DebugAdapter/XsltCompiledEngine.cs +++ b/XsltDebugger.DebugAdapter/XsltCompiledEngine.cs @@ -185,6 +185,16 @@ await Task.Run(() => InstrumentStylesheet(xdoc); InstrumentVariables(xdoc); + // DEBUG: Save instrumented XSLT for inspection + if (XsltEngineManager.IsTraceEnabled) + { + var debugPath = Path.Combine(Path.GetDirectoryName(_currentStylesheet) ?? ".", "out", + Path.GetFileNameWithoutExtension(_currentStylesheet) + ".instrumented.xslt"); + Directory.CreateDirectory(Path.GetDirectoryName(debugPath) ?? "."); + xdoc.Save(debugPath); + XsltEngineManager.NotifyOutput($"[trace] Saved instrumented XSLT to: {debugPath}"); + } + var settings = new XsltSettings(enableDocumentFunction: false, enableScript: false); args.AddExtensionObject(DebugNamespace, new XsltDebugExtension(this, _currentStylesheet)); @@ -337,6 +347,9 @@ internal void RegisterBreakpointHit(string file, int line, XPathNavigator? conte var clonedContext = CloneContextNavigator(contextNode); XsltEngineManager.UpdateContext(clonedContext); + // Track current line for inline C# method logging + XsltEngineManager.UpdateCurrentLine(normalized, line); + // Check if we hit a user-set breakpoint if (!isTemplateExit && IsBreakpointHit(normalized, line)) { @@ -543,10 +556,14 @@ private void InstrumentStylesheet(XDocument doc) } var isXsltElement = element.Name.Namespace == xsltNamespace; + var localName = element.Name.LocalName; + + // Check if this is a for-each element (needs special position logging) + var isForEach = isXsltElement && string.Equals(localName, "for-each", StringComparison.OrdinalIgnoreCase); // Check if this is a named template (for step-into support) var isNamedTemplate = isXsltElement && - string.Equals(element.Name.LocalName, "template", StringComparison.OrdinalIgnoreCase) && + string.Equals(localName, "template", StringComparison.OrdinalIgnoreCase) && element.Attribute("name") != null; // Create breakpoint call with template-entry marker if it's a named template @@ -556,6 +573,20 @@ private void InstrumentStylesheet(XDocument doc) : new XElement(xsltNamespace + "value-of", new XAttribute("select", $"dbg:break({line!.Value}, .)")); + // Create for-each position logging message (XSLT 1.0 compatible) + XElement? forEachMessage = null; + if (isForEach) + { + var selectAttr = element.Attribute("select")?.Value ?? "(none)"; + + // XSLT 1.0 compatible: use xsl:text and xsl:value-of inside xsl:message + // Cannot use 'select' attribute on xsl:message in XSLT 1.0 (only available in XSLT 3.0) + forEachMessage = new XElement(xsltNamespace + "message", + new XElement(xsltNamespace + "text", $"[DBG] for-each line={line!.Value} select={selectAttr} pos="), + new XElement(xsltNamespace + "value-of", new XAttribute("select", "position()")) + ); + } + var parent = element.Parent; var parentIsXslt = parent?.Name.Namespace == xsltNamespace; @@ -569,9 +600,44 @@ private void InstrumentStylesheet(XDocument doc) continue; } + // Special handling for for-each elements + // Must insert AFTER any xsl:sort elements + if (isForEach) + { + // Find the last xsl:sort element + var lastSort = element.Elements() + .Where(e => e.Name.Namespace == xsltNamespace && + string.Equals(e.Name.LocalName, "sort", StringComparison.OrdinalIgnoreCase)) + .LastOrDefault(); + + if (lastSort != null) + { + // Insert after the last sort + // AddAfterSelf inserts immediately after, so we add in reverse order + // Want: + // So: add message first (goes after sort), then add breakCall (goes between sort and message) + if (forEachMessage != null) + { + lastSort.AddAfterSelf(forEachMessage); + } + lastSort.AddAfterSelf(breakCall); + } + else + { + // No sorts, insert as first child + // AddFirst inserts at position 0, so we add in reverse order + // Want: + // So: add message first (position 0), then breakCall (new position 0, pushing message to 1) + if (forEachMessage != null) + { + element.AddFirst(forEachMessage); + } + element.AddFirst(breakCall); + } + } // Special handling for named templates with params/variables // In XSLT 1.0, ALL params and variables must come first - if (isNamedTemplate) + else if (isNamedTemplate) { // Find the last param/variable in this template var lastParamOrVar = element.Elements() @@ -798,6 +864,8 @@ private static bool ShouldInstrument(XElement element, XNamespace xsltNamespace) "param" or "variable" or "with-param" => false, // Exclude xsl:message to avoid instrumentation conflicts "message" => false, + // Exclude xsl:sort because it must be the first child of xsl:for-each + "sort" => false, _ => true }; } @@ -1064,7 +1132,9 @@ private object CompileAndCreateExtensionObject(string code) "using System.Text;", "using System.Xml;", "using System.Xml.XPath;", - "using System.Xml.Xsl;" + "using System.Xml.Xsl;", + "using XsltDebugger.DebugAdapter;", + "using static XsltDebugger.DebugAdapter.InlineXsltLogger;" }; var existingUsings = ExtractUsingStatements(classCode); @@ -1072,14 +1142,34 @@ private object CompileAndCreateExtensionObject(string code) requiredUsings.Where(u => !existingUsings.Contains(u, StringComparer.OrdinalIgnoreCase))); var sourceCode = string.Concat(prelude, Environment.NewLine, classCode); + + // Instrument inline C# methods when debugging is enabled + if (XsltEngineManager.DebugEnabled) + { + sourceCode = InlineCSharpInstrumenter.Instrument(sourceCode); + } + var syntaxTree = Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(sourceCode); var references = new List { MetadataReference.CreateFromFile(typeof(object).Assembly.Location), MetadataReference.CreateFromFile(typeof(XsltArgumentList).Assembly.Location), MetadataReference.CreateFromFile(typeof(System.Xml.XmlDocument).Assembly.Location), - MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location) + MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Runtime.GCSettings).Assembly.Location), + MetadataReference.CreateFromFile(typeof(XsltEngineManager).Assembly.Location), + MetadataReference.CreateFromFile(typeof(InlineXsltLogger).Assembly.Location) }; + + var coreLibDirectory = Path.GetDirectoryName(typeof(object).Assembly.Location); + if (!string.IsNullOrEmpty(coreLibDirectory)) + { + var runtimeAssemblyPath = Path.Combine(coreLibDirectory, "System.Runtime.dll"); + if (File.Exists(runtimeAssemblyPath)) + { + references.Add(MetadataReference.CreateFromFile(runtimeAssemblyPath)); + } + } var compilation = Microsoft.CodeAnalysis.CSharp.CSharpCompilation.Create( "InlineXsltExtAssembly", new[] { syntaxTree }, @@ -1095,18 +1185,22 @@ private object CompileAndCreateExtensionObject(string code) ms.Seek(0, SeekOrigin.Begin); var assembly = Assembly.Load(ms.ToArray()); - Type? chosen = null; - foreach (var t in assembly.GetTypes()) - { - var methods = t.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); - if (methods.Length > 0) - { - chosen = t; - break; - } - } + var allTypes = assembly.GetTypes(); + var usableTypes = allTypes + .Where(t => + !t.IsAbstract && + !t.IsInterface && + !t.IsGenericType && + !t.IsGenericTypeDefinition && + !Attribute.IsDefined(t, typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute), inherit: false)) + .ToList(); + + Type? chosen = usableTypes + .FirstOrDefault(t => t.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).Length > 0) + ?? usableTypes.FirstOrDefault(t => !t.IsNested) + ?? usableTypes.FirstOrDefault() + ?? allTypes.FirstOrDefault(t => !t.IsAbstract && !t.IsInterface); - chosen ??= assembly.GetTypes().FirstOrDefault(t => !t.IsNested); if (chosen == null) { throw new Exception("Compiled assembly does not contain a usable type for XSLT extension."); diff --git a/XsltDebugger.DebugAdapter/XsltEngineManager.cs b/XsltDebugger.DebugAdapter/XsltEngineManager.cs index 508f69b..268671e 100644 --- a/XsltDebugger.DebugAdapter/XsltEngineManager.cs +++ b/XsltDebugger.DebugAdapter/XsltEngineManager.cs @@ -76,6 +76,13 @@ public static void UpdateContext(XPathNavigator? context) } } + public static void UpdateCurrentLine(string file, int line) + { + // Update LastStop to track the current executing line (not just when paused) + // This allows inline C# methods to report the XSLT line that called them + LastStop = (file, line); + } + public static void NotifyOutput(string message) { if (!string.IsNullOrWhiteSpace(message)) diff --git a/XsltDebugger.Tests/CompiledEngineIntegrationTests.cs b/XsltDebugger.Tests/CompiledEngineIntegrationTests.cs index e4f50c1..47e3241 100644 --- a/XsltDebugger.Tests/CompiledEngineIntegrationTests.cs +++ b/XsltDebugger.Tests/CompiledEngineIntegrationTests.cs @@ -31,7 +31,9 @@ public async Task CompiledEngine_ShouldTransformInlineScriptSample_WithTraceLogg var xmlPath = GetTestDataPath("Integration/xml/sample-inline-cs-with-usings.xml"); var fullStylesheetPath = Path.GetFullPath(stylesheetPath); var fullXmlPath = Path.GetFullPath(xmlPath); - var breakpoints = new[] { (fullStylesheetPath, 24) }; + var lines = File.ReadAllLines(fullStylesheetPath); + var templateLine = Array.FindIndex(lines, line => line.Contains("", StringComparison.Ordinal)) + 1; + var breakpoints = new[] { (fullStylesheetPath, templateLine) }; var engine = new XsltCompiledEngine(); var outputLog = new List(); @@ -74,7 +76,7 @@ void OnStopped(string file, int line, DebugStopReason reason) var breakpointHit = await breakpointHitSource.Task.WaitAsync(TimeSpan.FromSeconds(5)); breakpointHit.file.Should().Be(fullStylesheetPath); - breakpointHit.line.Should().Be(24); + breakpointHit.line.Should().Be(templateLine); breakpointHit.reason.Should().Be(DebugStopReason.Breakpoint); var exitCode = await terminatedSource.Task.WaitAsync(TimeSpan.FromSeconds(5)); @@ -186,6 +188,401 @@ void OnStopped(string file, int line, DebugStopReason reason) } } + [Fact] + public async Task CompiledEngine_ShouldInstrumentInlineCSharpMethods() + { + var stylesheetPath = GetTestDataPath("Integration/xslt/v1/sample-inline-cs-auto-instrument.xslt"); + var xmlPath = GetTestDataPath("Integration/xml/sample-inline-cs-auto-instrument.xml"); + var fullStylesheetPath = Path.GetFullPath(stylesheetPath); + var fullXmlPath = Path.GetFullPath(xmlPath); + + var engine = new XsltCompiledEngine(); + var outputLog = new List(); + var outputLock = new object(); + var terminatedSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + void OnOutput(string message) + { + lock (outputLock) + { + outputLog.Add(message); + } + } + + void OnTerminated(int code) => terminatedSource.TrySetResult(code); + + XsltEngineManager.Reset(); + XsltEngineManager.SetDebugFlags(true, LogLevel.Log); + XsltEngineManager.EngineOutput += OnOutput; + XsltEngineManager.EngineTerminated += OnTerminated; + + try + { + await engine.StartAsync(fullStylesheetPath, fullXmlPath, stopOnEntry: false); + + var exitCode = await terminatedSource.Task.WaitAsync(TimeSpan.FromSeconds(5)); + exitCode.Should().Be(0, "transformation should complete successfully"); + + List snapshot; + lock (outputLock) + { + snapshot = outputLog.ToList(); + } + + // Verify instrumentation occurred + snapshot.Should().Contain(message => message.Contains("Instrumented 3 inline C# method(s)", StringComparison.OrdinalIgnoreCase), + "should report instrumenting 3 C# methods"); + + // Verify Add method logging + snapshot.Should().Contain(message => message.Contains("[inline] [Add:", StringComparison.Ordinal) && message.Contains("args = { a = 5, b = 3 }"), + "should log Add method entry with parameters"); + snapshot.Should().Contain(message => message.Contains("[inline] [Add:", StringComparison.Ordinal) && message.Contains("return = 8"), + "should log Add method return value"); + + // Verify Multiply method logging + snapshot.Should().Contain(message => message.Contains("[inline] [Multiply:", StringComparison.Ordinal) && message.Contains("args = { a = 4, b = 7 }"), + "should log Multiply method entry with parameters"); + snapshot.Should().Contain(message => message.Contains("[inline] [Multiply:", StringComparison.Ordinal) && message.Contains("return = 28"), + "should log Multiply method return value"); + + // Verify FormatNumber method logging + snapshot.Should().Contain(message => message.Contains("[inline] [FormatNumber:", StringComparison.Ordinal) && message.Contains("args = { num = 1000000 }"), + "should log FormatNumber method entry with parameters"); + snapshot.Should().Contain(message => message.Contains("[inline] [FormatNumber:", StringComparison.Ordinal) && message.Contains("return = 1,000,000"), + "should log FormatNumber method return value"); + } + finally + { + XsltEngineManager.EngineOutput -= OnOutput; + XsltEngineManager.EngineTerminated -= OnTerminated; + XsltEngineManager.Reset(); + } + } + + [Fact] + public async Task CompiledEngine_ShouldIncludeXsltLineNumbersInInlineCSharpLogs() + { + var stylesheetPath = GetTestDataPath("Integration/xslt/v1/ShipmentConfv1.xslt"); + var xmlPath = GetTestDataPath("Integration/xml/ShipmentConf-proper.xml"); + var fullStylesheetPath = Path.GetFullPath(stylesheetPath); + var fullXmlPath = Path.GetFullPath(xmlPath); + + var engine = new XsltCompiledEngine(); + var outputLog = new List(); + var outputLock = new object(); + var terminatedSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + void OnOutput(string message) + { + lock (outputLock) + { + outputLog.Add(message); + } + } + + void OnTerminated(int code) => terminatedSource.TrySetResult(code); + + XsltEngineManager.Reset(); + XsltEngineManager.SetDebugFlags(true, LogLevel.Log); + XsltEngineManager.EngineOutput += OnOutput; + XsltEngineManager.EngineTerminated += OnTerminated; + + try + { + await engine.StartAsync(fullStylesheetPath, fullXmlPath, stopOnEntry: false); + + var exitCode = await terminatedSource.Task.WaitAsync(TimeSpan.FromSeconds(5)); + exitCode.Should().Be(0, "transformation should complete successfully"); + + List snapshot; + lock (outputLock) + { + snapshot = outputLog.ToList(); + } + + // Verify XSLT line numbers are included in inline C# logs + snapshot.Should().Contain(message => message.Contains("@XSLT:75", StringComparison.Ordinal), + "should include XSLT line 75 for RoundToTwoDecimals call"); + snapshot.Should().Contain(message => message.Contains("@XSLT:88", StringComparison.Ordinal), + "should include XSLT line 88 for MinDate call"); + + // Verify C# line numbers are also included + snapshot.Should().Contain(message => message.Contains("[RoundToTwoDecimals:L", StringComparison.Ordinal), + "should include C# line number for RoundToTwoDecimals"); + snapshot.Should().Contain(message => message.Contains("[MinDate:L", StringComparison.Ordinal), + "should include C# line number for MinDate"); + } + finally + { + XsltEngineManager.EngineOutput -= OnOutput; + XsltEngineManager.EngineTerminated -= OnTerminated; + XsltEngineManager.Reset(); + } + } + + [Fact] + public async Task CompiledEngine_ShouldNotDoubleInstrumentManuallyLoggedMethods() + { + var stylesheetPath = GetTestDataPath("Integration/xslt/v1/sample-inline-cs-with-usings.xslt"); + var xmlPath = GetTestDataPath("Integration/xml/sample-inline-cs-with-usings.xml"); + var fullStylesheetPath = Path.GetFullPath(stylesheetPath); + var fullXmlPath = Path.GetFullPath(xmlPath); + + var engine = new XsltCompiledEngine(); + var outputLog = new List(); + var outputLock = new object(); + var terminatedSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + void OnOutput(string message) + { + lock (outputLock) + { + outputLog.Add(message); + } + } + + void OnTerminated(int code) => terminatedSource.TrySetResult(code); + + XsltEngineManager.Reset(); + XsltEngineManager.SetDebugFlags(true, LogLevel.Log); + XsltEngineManager.EngineOutput += OnOutput; + XsltEngineManager.EngineTerminated += OnTerminated; + + try + { + await engine.StartAsync(fullStylesheetPath, fullXmlPath, stopOnEntry: false); + + var exitCode = await terminatedSource.Task.WaitAsync(TimeSpan.FromSeconds(5)); + exitCode.Should().Be(0, "transformation should complete successfully"); + + List snapshot; + lock (outputLock) + { + snapshot = outputLog.ToList(); + } + + // Should NOT see "Instrumented N inline C# method(s)" because methods already use LogEntry/LogReturn + snapshot.Should().NotContain(message => message.Contains("Instrumented 2 inline C# method(s)", StringComparison.OrdinalIgnoreCase), + "should not instrument already manually logged methods"); + + // But should still see the manual logging output (with XSLT line numbers now) + snapshot.Should().Contain(message => message.Contains("[inline] [FormatCurrentDate:", StringComparison.Ordinal), + "should still have manual logging from FormatCurrentDate"); + snapshot.Should().Contain(message => message.Contains("[inline] [AddDays:", StringComparison.Ordinal), + "should still have manual logging from AddDays"); + } + finally + { + XsltEngineManager.EngineOutput -= OnOutput; + XsltEngineManager.EngineTerminated -= OnTerminated; + XsltEngineManager.Reset(); + } + } + + [Fact] + public async Task CompiledEngine_ShouldLogForEachPositionWithoutSort() + { + var stylesheetPath = GetTestDataPath("Integration/xslt/v1/foreach-test.xslt"); + var xmlPath = GetTestDataPath("Integration/xml/foreach-test.xml"); + var fullStylesheetPath = Path.GetFullPath(stylesheetPath); + var fullXmlPath = Path.GetFullPath(xmlPath); + + var engine = new XsltCompiledEngine(); + var outputLog = new List(); + var outputLock = new object(); + var terminatedSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + void OnOutput(string message) + { + lock (outputLock) + { + outputLog.Add(message); + } + } + + void OnTerminated(int code) => terminatedSource.TrySetResult(code); + + XsltEngineManager.Reset(); + XsltEngineManager.SetDebugFlags(true, LogLevel.Log); + XsltEngineManager.EngineOutput += OnOutput; + XsltEngineManager.EngineTerminated += OnTerminated; + + try + { + await engine.StartAsync(fullStylesheetPath, fullXmlPath, stopOnEntry: false); + + var exitCode = await terminatedSource.Task.WaitAsync(TimeSpan.FromSeconds(5)); + exitCode.Should().Be(0, "transformation should complete successfully"); + + List snapshot; + lock (outputLock) + { + snapshot = outputLog.ToList(); + } + + // Verify for-each position logging for simple loop (line 10) + snapshot.Should().Contain(message => message.Contains("[xsl:message] [DBG] for-each line=10", StringComparison.Ordinal) && message.Contains("select=/root/items/item", StringComparison.Ordinal) && message.Contains("pos=1", StringComparison.Ordinal), + "should log position 1 for first iteration"); + snapshot.Should().Contain(message => message.Contains("[xsl:message] [DBG] for-each line=10", StringComparison.Ordinal) && message.Contains("pos=2", StringComparison.Ordinal), + "should log position 2 for second iteration"); + snapshot.Should().Contain(message => message.Contains("[xsl:message] [DBG] for-each line=10", StringComparison.Ordinal) && message.Contains("pos=3", StringComparison.Ordinal), + "should log position 3 for third iteration"); + + // Verify for-each variable was captured + // Note: The variable will contain the LAST for-each that executed + // Since this test file has two for-each loops, it will be line 17 (the second one) + XsltEngineManager.Variables.Should().ContainKey("for-each"); + XsltEngineManager.Variables["for-each"].Should().NotBeNull(); + var forEachValue = XsltEngineManager.Variables["for-each"]!.ToString(); + (forEachValue.Contains("line=10") || forEachValue.Contains("line=17")).Should().BeTrue( + "for-each variable should contain either line 10 or line 17"); + + // Verify variable capture logging occurred (should see captures for BOTH loops) + snapshot.Should().Contain(message => message.Contains("Captured variable: $for-each", StringComparison.Ordinal) && message.Contains("line=10"), + "should log variable capture for line 10 for-each"); + snapshot.Should().Contain(message => message.Contains("Captured variable: $for-each", StringComparison.Ordinal) && message.Contains("line=17"), + "should log variable capture for line 17 for-each"); + } + finally + { + XsltEngineManager.EngineOutput -= OnOutput; + XsltEngineManager.EngineTerminated -= OnTerminated; + XsltEngineManager.Reset(); + } + } + + [Fact] + public async Task CompiledEngine_ShouldLogForEachPositionWithSort() + { + var stylesheetPath = GetTestDataPath("Integration/xslt/v1/foreach-test.xslt"); + var xmlPath = GetTestDataPath("Integration/xml/foreach-test.xml"); + var fullStylesheetPath = Path.GetFullPath(stylesheetPath); + var fullXmlPath = Path.GetFullPath(xmlPath); + + var engine = new XsltCompiledEngine(); + var outputLog = new List(); + var outputLock = new object(); + var terminatedSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + void OnOutput(string message) + { + lock (outputLock) + { + outputLog.Add(message); + } + } + + void OnTerminated(int code) => terminatedSource.TrySetResult(code); + + XsltEngineManager.Reset(); + XsltEngineManager.SetDebugFlags(true, LogLevel.Log); + XsltEngineManager.EngineOutput += OnOutput; + XsltEngineManager.EngineTerminated += OnTerminated; + + try + { + await engine.StartAsync(fullStylesheetPath, fullXmlPath, stopOnEntry: false); + + var exitCode = await terminatedSource.Task.WaitAsync(TimeSpan.FromSeconds(5)); + exitCode.Should().Be(0, "transformation should complete successfully"); + + List snapshot; + lock (outputLock) + { + snapshot = outputLog.ToList(); + } + + // Verify for-each position logging for loop with sort (line 17) + snapshot.Should().Contain(message => message.Contains("[xsl:message] [DBG] for-each line=17", StringComparison.Ordinal) && message.Contains("select=/root/sorted/item", StringComparison.Ordinal) && message.Contains("pos=1", StringComparison.Ordinal), + "should log position 1 for first iteration (after sort)"); + snapshot.Should().Contain(message => message.Contains("[xsl:message] [DBG] for-each line=17", StringComparison.Ordinal) && message.Contains("pos=2", StringComparison.Ordinal), + "should log position 2 for second iteration (after sort)"); + snapshot.Should().Contain(message => message.Contains("[xsl:message] [DBG] for-each line=17", StringComparison.Ordinal) && message.Contains("pos=3", StringComparison.Ordinal), + "should log position 3 for third iteration (after sort)"); + + // Verify for-each variable was captured (will contain the last iteration's value) + XsltEngineManager.Variables.Should().ContainKey("for-each"); + XsltEngineManager.Variables["for-each"].Should().NotBeNull(); + XsltEngineManager.Variables["for-each"]!.ToString().Should().Contain("line=17"); + XsltEngineManager.Variables["for-each"]!.ToString().Should().Contain("select=/root/sorted/item"); + } + finally + { + XsltEngineManager.EngineOutput -= OnOutput; + XsltEngineManager.EngineTerminated -= OnTerminated; + XsltEngineManager.Reset(); + } + } + + [Fact] + public async Task CompiledEngine_ShouldLogForEachInShipmentConfv1() + { + var stylesheetPath = GetTestDataPath("Integration/xslt/v1/ShipmentConfv1.xslt"); + var xmlPath = GetTestDataPath("Integration/xml/ShipmentConf-proper.xml"); + var fullStylesheetPath = Path.GetFullPath(stylesheetPath); + var fullXmlPath = Path.GetFullPath(xmlPath); + + var engine = new XsltCompiledEngine(); + var outputLog = new List(); + var outputLock = new object(); + var terminatedSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + void OnOutput(string message) + { + lock (outputLock) + { + outputLog.Add(message); + } + } + + void OnTerminated(int code) => terminatedSource.TrySetResult(code); + + XsltEngineManager.Reset(); + XsltEngineManager.SetDebugFlags(true, LogLevel.Log); + XsltEngineManager.EngineOutput += OnOutput; + XsltEngineManager.EngineTerminated += OnTerminated; + + try + { + await engine.StartAsync(fullStylesheetPath, fullXmlPath, stopOnEntry: false); + + var exitCode = await terminatedSource.Task.WaitAsync(TimeSpan.FromSeconds(5)); + exitCode.Should().Be(0, "transformation should complete successfully"); + + List snapshot; + lock (outputLock) + { + snapshot = outputLog.ToList(); + } + + // Verify for-each position logging for outer loop (line 60) + snapshot.Should().Contain(message => message.Contains("[xsl:message] [DBG] for-each line=60", StringComparison.Ordinal) && message.Contains("select=/ShipmentConfirmation/Orders/OrderItems", StringComparison.Ordinal), + "should log position for outer for-each at line 60"); + + // Verify for-each position logging for nested loop (line 83) + snapshot.Should().Contain(message => message.Contains("[xsl:message] [DBG] for-each line=83", StringComparison.Ordinal) && message.Contains("select=OperationReports/ReportInfo/OperationReportDate", StringComparison.Ordinal), + "should log position for nested for-each at line 83"); + + // Verify the existing xsl:message at line 67 is still present + snapshot.Should().Contain(message => message.Contains("[xsl:message] Hello", StringComparison.Ordinal), + "should preserve existing xsl:message elements"); + + // Verify for-each variable was captured + XsltEngineManager.Variables.Should().ContainKey("for-each"); + XsltEngineManager.Variables["for-each"].Should().NotBeNull(); + // The variable will contain the last for-each that executed (could be line 60 or line 83) + var forEachValue = XsltEngineManager.Variables["for-each"]!.ToString(); + (forEachValue.Contains("line=60") || forEachValue.Contains("line=83")).Should().BeTrue( + "for-each variable should contain either line 60 or line 83"); + } + finally + { + XsltEngineManager.EngineOutput -= OnOutput; + XsltEngineManager.EngineTerminated -= OnTerminated; + XsltEngineManager.Reset(); + } + } + private static void TryDeleteOutput(string outFile, string outDir) { try diff --git a/docs/EngineFeatureComparison.md b/docs/EngineFeatureComparison.md new file mode 100644 index 0000000..e4cfe33 --- /dev/null +++ b/docs/EngineFeatureComparison.md @@ -0,0 +1,48 @@ +# XSLT Debug Engine Comparison + +## Engine Profiles + +### XsltCompiledEngine +- Executes via .NET `XslCompiledTransform`, delivering full XSLT 1.0 compliance and best-effort support for 2.0+ stylesheets with compatibility warnings. +- Loads and instruments stylesheets on every debug run, saving an `out/.instrumented.xslt` copy when trace logging is enabled. +- Supports inline C# through `msxsl:script`, compiling snippets with Roslyn, auto-registering extension namespaces, and optionally instrumenting generated helper code. + +### SaxonEngine +- Builds a Saxon HE/PE processor, enables XSLT 3.0 features, and compiles stylesheets to a reusable executable for full 2.0/3.0 behavior. +- Only instruments stylesheets when debugging is enabled, registering a custom Saxon extension function to surface probes. +- Blocks inline C# by reusing engine validation logic and reports static compilation issues (line/column) via Saxon’s `ErrorList`. + +## Shared Debug Surface +- Breakpoints: both inject `dbg:break()` calls, mark template entry/exit, and honor user breakpoints through shared stepping state (`StepMode` with depth tracking). +- Stepping: continue/step-in/step-over/step-out behaviors use identical synchronization and depth logic, pausing when template boundaries or requested lines are reached. +- Watches & Namespaces: both extract stylesheet namespace declarations (adding a synthetic `default` prefix) and register them so watch expressions resolve consistently. +- Pause Context: both convert the current execution node to an `XPathNavigator` clone before notifying the debug adapter, enabling expression evaluation at stop points. +- Message Capture: both parse `[DBG]` prefixed messages into the debugger’s variable store while forwarding ordinary `xsl:message` output to the console. + +## Instrumentation Differences + +### XsltCompiledEngine Specialties +- Probes are injected as `xsl:value-of` calls to stay XSLT 1.0 compliant and include supplemental `xsl:message` blocks for `xsl:for-each` position reporting. +- Variable instrumentation groups declarations per template, inserting `[DBG]` messages after the final `xsl:param`/`xsl:variable` to preserve 1.0 ordering rules. +- Inline Roslyn extension objects allow breakpoints and logging inside user scripts, providing parity with template-level debugging for hybrid XSLT/C# projects. + +### SaxonEngine Specialties +- Probe nodes are created as `xsl:sequence` instructions tagged with `dbg:probe="1"`, allowing the engine to skip reinstrumentation on reentry. +- Maintains extensive β€œfragile” element lists (e.g., `iterate`, `try/catch`, `accumulator-rule`, `merge-*`) so instrumentation never invalidates XSLT 2.0/3.0 constructs. +- Variable probes emit tuples via `xsl:message select="('[DBG]', ...)"`, leveraging 2.0 expressions (`string-join`) to flatten sequences and capture multi-item variables. +- Serializes Saxon `XdmNode` contexts back into .NET DOMs to give the debugger XPath-compatible navigation without exposing Saxon internals. + +## Feature Gap Snapshot + +| Feature Area | Only in XsltCompiledEngine | Only in SaxonEngine | +| --- | --- | --- | +| XSLT version focus | Inline C# compatible XSLT 1.0 runtime | Native XSLT 2.0/3.0 execution with Saxon | +| Inline scripting | Compiles `msxsl:script` via Roslyn and instruments generated code | Explicitly blocked; prompts user to switch engines | +| Instrumentation safety | Tailored to 1.0 structural rules and limited fragile contexts | Expansive guards for 2.0/3.0 constructs (accumulators, iterate/try, merge) with `dbg:probe` markers | +| Variable capture | Text/value-of messages per template, respecting 1.0 ordering | Tuple/sequence messages with `string-join` across sequences | +| Compilation diagnostics | Relies on transform exceptions | Surfaces Saxon static errors with module URI, line, and column | + +## Selection Guidance +- Choose **XsltCompiledEngine** for XSLT 1.0 stylesheets, projects relying on inline C#, or environments tied to the .NET runtime. +- Choose **SaxonEngine** for XSLT 2.0/3.0 transformations, schema-aware features, or when richer compile-time diagnostics and sequence-aware variable logging are required. +- Maintain separate launch configurations so each stylesheet uses the engine aligned with its language level and extension requirements. From d13af295d72b067c0cb6a9235875fd6ed40b2d6b Mon Sep 17 00:00:00 2001 From: Daniel Jonathan Date: Sat, 1 Nov 2025 15:28:29 +0100 Subject: [PATCH 4/4] updated for 1.0.0 version --- .gitignore | 2 + .vscodeignore | 10 + CHANGELOG.md | 19 + README.md | 33 +- .../xslt/{v1 => compiled}/ShipmentConfv1.xslt | 0 .../VariableLoggingSampleV1.xslt | 0 .../xslt/{v1 => compiled}/foreach-test.xslt | 0 .../{v1 => compiled}/ns-mapping-sample.xslt | 0 .../sample-inline-cs-auto-instrument.xslt | 0 .../sample-inline-cs-with-usings.xslt | 0 .../{v1 => compiled}/sample-inline-cs.xslt | 0 .../xslt/{v1 => compiled}/sample.xslt | 0 .../xslt/{v2 => saxon}/AdvanceXslt2.xslt | 0 .../xslt/{v3 => saxon}/AdvanceXslt3.xslt | 0 .../xslt/{v3 => saxon}/LmlBasedXslt.xslt | 0 .../xslt/{v3 => saxon}/ShipmentConf3.xslt | 0 .../xslt/{v3 => saxon}/ShipmentConfv2_ns.xslt | 0 .../{v2 => saxon}/VariableLoggingSample.xslt | 0 .../Integration/xslt/saxon/foreach-test.xslt | 26 ++ .../xslt/saxon/ns-mapping-sample.xslt | 47 ++ .../xslt/{v2 => saxon}/sample-xslt2.xslt | 0 .../xslt/{v3 => saxon}/sample-xslt3.xslt | 0 .../LmlCompilationTest.cs | 2 +- .../NamespaceExtractionTest.cs | 12 +- .../ProgramUsingEngineType.cs | 4 +- XsltDebugger.DebugAdapter/SaxonEngine.cs | 22 +- .../Xslt1Instrumentation.cs | 435 ++++++++++++++++++ .../XsltCompiledEngine.cs | 435 +----------------- .../CompiledEngineIntegrationTests.cs | 16 +- .../SaxonEngineIntegrationTests.cs | 82 +++- docs/EngineFeatureComparison.md | 7 +- package-lock.json | 4 +- package.json | 2 +- 33 files changed, 678 insertions(+), 480 deletions(-) rename TestData/Integration/xslt/{v1 => compiled}/ShipmentConfv1.xslt (100%) rename TestData/Integration/xslt/{v1 => compiled}/VariableLoggingSampleV1.xslt (100%) rename TestData/Integration/xslt/{v1 => compiled}/foreach-test.xslt (100%) rename TestData/Integration/xslt/{v1 => compiled}/ns-mapping-sample.xslt (100%) rename TestData/Integration/xslt/{v1 => compiled}/sample-inline-cs-auto-instrument.xslt (100%) rename TestData/Integration/xslt/{v1 => compiled}/sample-inline-cs-with-usings.xslt (100%) rename TestData/Integration/xslt/{v1 => compiled}/sample-inline-cs.xslt (100%) rename TestData/Integration/xslt/{v1 => compiled}/sample.xslt (100%) rename TestData/Integration/xslt/{v2 => saxon}/AdvanceXslt2.xslt (100%) rename TestData/Integration/xslt/{v3 => saxon}/AdvanceXslt3.xslt (100%) rename TestData/Integration/xslt/{v3 => saxon}/LmlBasedXslt.xslt (100%) rename TestData/Integration/xslt/{v3 => saxon}/ShipmentConf3.xslt (100%) rename TestData/Integration/xslt/{v3 => saxon}/ShipmentConfv2_ns.xslt (100%) rename TestData/Integration/xslt/{v2 => saxon}/VariableLoggingSample.xslt (100%) create mode 100644 TestData/Integration/xslt/saxon/foreach-test.xslt create mode 100644 TestData/Integration/xslt/saxon/ns-mapping-sample.xslt rename TestData/Integration/xslt/{v2 => saxon}/sample-xslt2.xslt (100%) rename TestData/Integration/xslt/{v3 => saxon}/sample-xslt3.xslt (100%) create mode 100644 XsltDebugger.DebugAdapter/Xslt1Instrumentation.cs diff --git a/.gitignore b/.gitignore index 320841a..583e160 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ out +**/out/ dist node_modules .vscode-test/ @@ -13,6 +14,7 @@ obj/ *.log *.tmp *.out.xml +**/*.out.xml # OS generated files .DS_Store diff --git a/.vscodeignore b/.vscodeignore index 0e24bc7..3e44e64 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -5,10 +5,14 @@ src/** .vscodeignore .yarnrc vsc-extension-quickstart.md +package.json.backup +TestData/** +docs/** **/tsconfig.json **/eslint.config.mjs **/*.map **/*.ts +**/*.sh **/.vscode-test.* .github/** @@ -69,6 +73,9 @@ logs npm-debug.log* yarn-debug.log* yarn-error.log* +*.out.xml +**/*.out.xml +TestData/**/out/** # Node modules (should be handled by vsce, but just in case) node_modules/** @@ -116,3 +123,6 @@ VARIABLE-PRINT-GUIDE.md # Packaging scripts package-darwin.sh package-win.sh +package-all.sh +test-debug-output.sh +test-namespace-support.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index e5c213a..bd1634e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to the XSLT Debugger extension will be documented in this file. +## [1.0.0] - 2025 + +### Added + +- Shared XSLT 1.0 instrumentation helper used by both engines so Saxon can now debug XSLT 1.0 stylesheets that do not rely on `msxsl:script`. +- Version-aware Saxon pipeline that switches to the 1.0-safe probes while retaining the existing XSLT 2.0/3.0 instrumentation. +- Integration coverage for the new Saxon 1.0 path (`SaxonEngine_ShouldCaptureVariables_WhenRunningXslt1Stylesheet`) and console smoke tests for both engines. + +### Changed + +- Reorganised integration samples under `TestData/Integration/xslt/compiled/` and `TestData/Integration/xslt/saxon/` to mirror the engine split. +- XsltCompiledEngine now delegates all 1.0 probe insertion to the shared helper, keeping instrumentation logic in one place. +- Bumped the extension version to `1.0.0` and updated packaging docs to reference the new VSIX build numbers. +- `.gitignore` / `.vscodeignore` now filter generated `out/` folders and `*.out.xml` artifacts across the tree. + +### Fixed + +- Ensured Saxon 1.0 runs produce the same breakpoint and variable capture behaviour as the compiled engine by reusing the same probe shapes. + ## [0.6.0] - 2025 ### Added diff --git a/README.md b/README.md index cf6c3c4..40c2fbc 100644 --- a/README.md +++ b/README.md @@ -8,25 +8,25 @@ A powerful Visual Studio Code extension that enables debugging support for XSLT - **Variable Inspection**: Automatically materialises XSLT variables and context nodes inside VSΒ Code’s VARIABLES pane - **XPath Evaluation**: Evaluate XPath expressions in the current context - **Inline C# Scripting**: Debug XSLT stylesheets with embedded C# code using Roslyn -- **Multiple Engines**: Support for compiled XSLT engine (XSLT 1.0) and Saxon engine (XSLT 2.0/3.0) +- **Multiple Engines**: Support for compiled XSLT engine (XSLT 1.0 + inline C#) and Saxon engine (XSLT 1.0 without `msxsl:script`, plus XSLT 2.0/3.0) - **Cross-Platform**: Works on Windows, macOS, and Linux - **Probe Tagging**: Instrumented breakpoints and trace messages are tagged with `dbg:probe="1"` so repeated runs stay idempotent ## XSLT Processing Engines -| Feature | Compiled Engine | Saxon .NET Engine | -| ----------------- | -------------------------------- | ------------------------------------ | -| **XSLT Version** | 1.0 | 2.0, 3.0 | -| **XPath Version** | 1.0 | 2.0, 3.0 | -| **Special Features** | Inline C# via `msxsl:script` | Full XSLT 2.0/3.0 features | -| **Best For** | XSLT 1.0 with inline C# | Modern XSLT 2.0/3.0 stylesheets | +| Feature | Compiled Engine | Saxon .NET Engine | +| ----------------- | -------------------------------- | ---------------------------------------------------- | +| **XSLT Version** | 1.0 | 1.0 (no inline C#), 2.0, 3.0 | +| **XPath Version** | 1.0 | 2.0, 3.0 | +| **Special Features** | Inline C# via `msxsl:script` | Full XSLT 2.0/3.0 features, version-aware probes | +| **Best For** | XSLT 1.0 with inline C# | XSLT 1.0 without `msxsl:script`, plus 2.0/3.0 stylesheets | -**Engine Selection**: Auto-detected based on XSLT version, or manually set with `"engine": "compiled"` or `"engine": "saxonnet"` in launch.json +**Engine Selection**: Auto-detected based on XSLT version and inline script usage, or manually set with `"engine": "compiled"` or `"engine": "saxonnet"` in launch.json ### ⚠️ Current Limitations - Debugging focuses on basic XSLT structures (templates, loops, expressions); complex dynamic calls are not instrumented -- Cannot step into inline C# scripts +- Cannot step into inline C# scripts, though Roslyn instrumentation still logs entry parameters and return values to the console for visibility - Variable inspection uses "falldown" approach: variables are auto-captured via instrumentation as execution progresses forward (cannot re-run or step back to previous lines) - No support for: **step back**, goto targets, set variable, conditional breakpoints, or debug console autocomplete - Variable capture limited to `@select`-based variables; complex variables with content children may not be fully captured @@ -49,10 +49,10 @@ A powerful Visual Studio Code extension that enables debugging support for XSLT ```bash # macOS - code --install-extension xsltdebugger-darwin-darwin-arm64-0.6.0.vsix + code --install-extension xsltdebugger-darwin-darwin-arm64-1.0.0.vsix # Windows - code --install-extension xsltdebugger-windows-win32-x64-0.6.0.vsix + code --install-extension xsltdebugger-windows-win32-x64-1.0.0.vsix ``` 2. **Create a debug configuration** in [.vscode/launch.json](#setting-up-a-debug-configuration) @@ -81,7 +81,7 @@ A powerful Visual Studio Code extension that enables debugging support for XSLT ```bash ./package-all.sh - code --install-extension xsltdebugger-darwin-darwin-arm64-0.6.0.vsix + code --install-extension xsltdebugger-darwin-darwin-arm64-1.0.0.vsix ``` **Platform-specific packaging** (build individually): @@ -89,11 +89,11 @@ A powerful Visual Studio Code extension that enables debugging support for XSLT ```bash # For macOS only ./package-darwin.sh - code --install-extension xsltdebugger-darwin-darwin-arm64-0.6.0.vsix + code --install-extension xsltdebugger-darwin-darwin-arm64-1.0.0.vsix # For Windows only ./package-win.sh - code --install-extension xsltdebugger-windows-win32-x64-0.6.0.vsix + code --install-extension xsltdebugger-windows-win32-x64-1.0.0.vsix ``` ## Usage @@ -202,7 +202,8 @@ Create a `.vscode/launch.json` file in your project workspace: - **Falldown Approach**: Variables appear in the Variables panel as execution flows forward past their declarations (not available before declaration) - **No Step Back**: Since variables are captured via forward instrumentation, you cannot step backward to re-inspect previous values - **XSLT 2.0/3.0**: Full support via Saxon engine with `@select`-based variable capture -- **XSLT 1.0**: Limited variable inspection via Compiled engine +- **XSLT 1.0 (no inline C#)**: Saxon engine reuses 1.0-safe probes (value-of/message) for equivalent capture behaviour +- **XSLT 1.0 + inline C#**: Limited variable inspection via Compiled engine only ### Log Levels @@ -254,7 +255,7 @@ The debugger supports XSLT stylesheets with embedded C# code using `msxsl:script - **Extension** ([src/extension.ts](src/extension.ts)): Registers debug type, resolves paths - **Adapter** ([XsltDebugger.DebugAdapter/](XsltDebugger.DebugAdapter/)): DAP server, engine management, breakpoint/stepping logic -- **Engines**: `XsltCompiledEngine` (XSLT 1.0 + C#), `SaxonEngine` (XSLT 2.0/3.0) +- **Engines**: `XsltCompiledEngine` (XSLT 1.0 + C#), `SaxonEngine` (XSLT 1.0 without `msxsl:script`, plus 2.0/3.0) - **Instrumentation**: - Breakpoints: Both engines inject `dbg:break()` extension function calls at breakpoint lines - Variables: Saxon engine injects `` elements after variable declarations to auto-capture values diff --git a/TestData/Integration/xslt/v1/ShipmentConfv1.xslt b/TestData/Integration/xslt/compiled/ShipmentConfv1.xslt similarity index 100% rename from TestData/Integration/xslt/v1/ShipmentConfv1.xslt rename to TestData/Integration/xslt/compiled/ShipmentConfv1.xslt diff --git a/TestData/Integration/xslt/v1/VariableLoggingSampleV1.xslt b/TestData/Integration/xslt/compiled/VariableLoggingSampleV1.xslt similarity index 100% rename from TestData/Integration/xslt/v1/VariableLoggingSampleV1.xslt rename to TestData/Integration/xslt/compiled/VariableLoggingSampleV1.xslt diff --git a/TestData/Integration/xslt/v1/foreach-test.xslt b/TestData/Integration/xslt/compiled/foreach-test.xslt similarity index 100% rename from TestData/Integration/xslt/v1/foreach-test.xslt rename to TestData/Integration/xslt/compiled/foreach-test.xslt diff --git a/TestData/Integration/xslt/v1/ns-mapping-sample.xslt b/TestData/Integration/xslt/compiled/ns-mapping-sample.xslt similarity index 100% rename from TestData/Integration/xslt/v1/ns-mapping-sample.xslt rename to TestData/Integration/xslt/compiled/ns-mapping-sample.xslt diff --git a/TestData/Integration/xslt/v1/sample-inline-cs-auto-instrument.xslt b/TestData/Integration/xslt/compiled/sample-inline-cs-auto-instrument.xslt similarity index 100% rename from TestData/Integration/xslt/v1/sample-inline-cs-auto-instrument.xslt rename to TestData/Integration/xslt/compiled/sample-inline-cs-auto-instrument.xslt diff --git a/TestData/Integration/xslt/v1/sample-inline-cs-with-usings.xslt b/TestData/Integration/xslt/compiled/sample-inline-cs-with-usings.xslt similarity index 100% rename from TestData/Integration/xslt/v1/sample-inline-cs-with-usings.xslt rename to TestData/Integration/xslt/compiled/sample-inline-cs-with-usings.xslt diff --git a/TestData/Integration/xslt/v1/sample-inline-cs.xslt b/TestData/Integration/xslt/compiled/sample-inline-cs.xslt similarity index 100% rename from TestData/Integration/xslt/v1/sample-inline-cs.xslt rename to TestData/Integration/xslt/compiled/sample-inline-cs.xslt diff --git a/TestData/Integration/xslt/v1/sample.xslt b/TestData/Integration/xslt/compiled/sample.xslt similarity index 100% rename from TestData/Integration/xslt/v1/sample.xslt rename to TestData/Integration/xslt/compiled/sample.xslt diff --git a/TestData/Integration/xslt/v2/AdvanceXslt2.xslt b/TestData/Integration/xslt/saxon/AdvanceXslt2.xslt similarity index 100% rename from TestData/Integration/xslt/v2/AdvanceXslt2.xslt rename to TestData/Integration/xslt/saxon/AdvanceXslt2.xslt diff --git a/TestData/Integration/xslt/v3/AdvanceXslt3.xslt b/TestData/Integration/xslt/saxon/AdvanceXslt3.xslt similarity index 100% rename from TestData/Integration/xslt/v3/AdvanceXslt3.xslt rename to TestData/Integration/xslt/saxon/AdvanceXslt3.xslt diff --git a/TestData/Integration/xslt/v3/LmlBasedXslt.xslt b/TestData/Integration/xslt/saxon/LmlBasedXslt.xslt similarity index 100% rename from TestData/Integration/xslt/v3/LmlBasedXslt.xslt rename to TestData/Integration/xslt/saxon/LmlBasedXslt.xslt diff --git a/TestData/Integration/xslt/v3/ShipmentConf3.xslt b/TestData/Integration/xslt/saxon/ShipmentConf3.xslt similarity index 100% rename from TestData/Integration/xslt/v3/ShipmentConf3.xslt rename to TestData/Integration/xslt/saxon/ShipmentConf3.xslt diff --git a/TestData/Integration/xslt/v3/ShipmentConfv2_ns.xslt b/TestData/Integration/xslt/saxon/ShipmentConfv2_ns.xslt similarity index 100% rename from TestData/Integration/xslt/v3/ShipmentConfv2_ns.xslt rename to TestData/Integration/xslt/saxon/ShipmentConfv2_ns.xslt diff --git a/TestData/Integration/xslt/v2/VariableLoggingSample.xslt b/TestData/Integration/xslt/saxon/VariableLoggingSample.xslt similarity index 100% rename from TestData/Integration/xslt/v2/VariableLoggingSample.xslt rename to TestData/Integration/xslt/saxon/VariableLoggingSample.xslt diff --git a/TestData/Integration/xslt/saxon/foreach-test.xslt b/TestData/Integration/xslt/saxon/foreach-test.xslt new file mode 100644 index 0000000..1e55632 --- /dev/null +++ b/TestData/Integration/xslt/saxon/foreach-test.xslt @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TestData/Integration/xslt/saxon/ns-mapping-sample.xslt b/TestData/Integration/xslt/saxon/ns-mapping-sample.xslt new file mode 100644 index 0000000..f0b78af --- /dev/null +++ b/TestData/Integration/xslt/saxon/ns-mapping-sample.xslt @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TestData/Integration/xslt/v2/sample-xslt2.xslt b/TestData/Integration/xslt/saxon/sample-xslt2.xslt similarity index 100% rename from TestData/Integration/xslt/v2/sample-xslt2.xslt rename to TestData/Integration/xslt/saxon/sample-xslt2.xslt diff --git a/TestData/Integration/xslt/v3/sample-xslt3.xslt b/TestData/Integration/xslt/saxon/sample-xslt3.xslt similarity index 100% rename from TestData/Integration/xslt/v3/sample-xslt3.xslt rename to TestData/Integration/xslt/saxon/sample-xslt3.xslt diff --git a/XsltDebugger.ConsoleTest/LmlCompilationTest.cs b/XsltDebugger.ConsoleTest/LmlCompilationTest.cs index fef12e4..e30fed1 100644 --- a/XsltDebugger.ConsoleTest/LmlCompilationTest.cs +++ b/XsltDebugger.ConsoleTest/LmlCompilationTest.cs @@ -15,7 +15,7 @@ static void Main(string[] args) { Console.WriteLine("=== XSLT Compilation Test ===\n"); - var stylesheetPath = Path.GetFullPath("TestData/Integration/xslt/v3/LmlBasedXslt.xslt"); + var stylesheetPath = Path.GetFullPath("TestData/Integration/xslt/saxon/LmlBasedXslt.xslt"); var xmlPath = Path.GetFullPath("TestData/Integration/xml/ShipmentConf-lml.xml"); if (!File.Exists(stylesheetPath)) diff --git a/XsltDebugger.ConsoleTest/NamespaceExtractionTest.cs b/XsltDebugger.ConsoleTest/NamespaceExtractionTest.cs index 158e956..7181295 100644 --- a/XsltDebugger.ConsoleTest/NamespaceExtractionTest.cs +++ b/XsltDebugger.ConsoleTest/NamespaceExtractionTest.cs @@ -17,12 +17,12 @@ static void Main(string[] args) var testFiles = new[] { - "TestData/Integration/xslt/v3/LmlBasedXslt.xslt", - "TestData/Integration/xslt/v2/VariableLoggingSample.xslt", - "TestData/Integration/xslt/v1/VariableLoggingSampleV1.xslt", - "TestData/Integration/xslt/v2/AdvanceXslt2.xslt", - "TestData/Integration/xslt/v3/AdvanceXslt3.xslt", - "TestData/Integration/xslt/v3/ShipmentConf3.xslt" + "TestData/Integration/xslt/saxon/LmlBasedXslt.xslt", + "TestData/Integration/xslt/saxon/VariableLoggingSample.xslt", + "TestData/Integration/xslt/compiled/VariableLoggingSampleV1.xslt", + "TestData/Integration/xslt/saxon/AdvanceXslt2.xslt", + "TestData/Integration/xslt/saxon/AdvanceXslt3.xslt", + "TestData/Integration/xslt/saxon/ShipmentConf3.xslt" }; foreach (var testFile in testFiles) diff --git a/XsltDebugger.ConsoleTest/ProgramUsingEngineType.cs b/XsltDebugger.ConsoleTest/ProgramUsingEngineType.cs index 3606fdd..e43f422 100644 --- a/XsltDebugger.ConsoleTest/ProgramUsingEngineType.cs +++ b/XsltDebugger.ConsoleTest/ProgramUsingEngineType.cs @@ -35,8 +35,8 @@ static async Task Main(string[] args) // Use different defaults based on engine type var defaultXslt = engineType == "compiled" - ? Path.Combine(DefaultTestDataFolder, "xslt/v1/sample-inline-cs-with-usings.xslt") - : Path.Combine(DefaultTestDataFolder, "xslt/v3/ShipmentConf3.xslt"); + ? Path.Combine(DefaultTestDataFolder, "xslt/compiled/sample-inline-cs-with-usings.xslt") + : Path.Combine(DefaultTestDataFolder, "xslt/saxon/ShipmentConf3.xslt"); var defaultXml = Path.Combine(DefaultTestDataFolder, engineType == "compiled" ? "xml/sample-inline-cs-with-usings.xml" : "xml/ShipmentConf-proper.xml"); var stylesheetPath = ResolveInput(filteredArgs.ToArray(), 0, defaultXslt); diff --git a/XsltDebugger.DebugAdapter/SaxonEngine.cs b/XsltDebugger.DebugAdapter/SaxonEngine.cs index 35ee6d5..1c9817f 100644 --- a/XsltDebugger.DebugAdapter/SaxonEngine.cs +++ b/XsltDebugger.DebugAdapter/SaxonEngine.cs @@ -141,6 +141,8 @@ await Task.Run(() => XsltEngineManager.NotifyOutput($"XSLT version detected: {version}"); } + var useXslt1Instrumentation = version < 2.0m; + // Enable debugging instrumentation only if debugging is enabled if (XsltEngineManager.DebugEnabled) { @@ -153,11 +155,23 @@ await Task.Run(() => _processor.RegisterExtensionFunction(debugExtension); EnsureDebugNamespace(xdoc); - InstrumentStylesheet(xdoc); - InstrumentVariables(xdoc); - if (XsltEngineManager.IsLogEnabled) + if (useXslt1Instrumentation) { - XsltEngineManager.NotifyOutput("Debugging enabled for XSLT 2.0/3.0."); + Xslt1Instrumentation.InstrumentStylesheet(xdoc, _currentStylesheet, DebugNamespace, addProbeAttribute: true); + Xslt1Instrumentation.InstrumentVariables(xdoc, DebugNamespace, addProbeAttribute: true); + if (XsltEngineManager.IsLogEnabled) + { + XsltEngineManager.NotifyOutput("Debugging enabled for XSLT 1.0 (Saxon instrumentation)."); + } + } + else + { + InstrumentStylesheet(xdoc); + InstrumentVariables(xdoc); + if (XsltEngineManager.IsLogEnabled) + { + XsltEngineManager.NotifyOutput("Debugging enabled for XSLT 2.0/3.0."); + } } } else diff --git a/XsltDebugger.DebugAdapter/Xslt1Instrumentation.cs b/XsltDebugger.DebugAdapter/Xslt1Instrumentation.cs new file mode 100644 index 0000000..1d99849 --- /dev/null +++ b/XsltDebugger.DebugAdapter/Xslt1Instrumentation.cs @@ -0,0 +1,435 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml; +using System.Xml.Linq; + +namespace XsltDebugger.DebugAdapter; + +internal static class Xslt1Instrumentation +{ + public static void InstrumentStylesheet(XDocument doc, string stylesheetPath, XNamespace debugNamespace, bool addProbeAttribute) + { + if (doc.Root == null) + { + return; + } + + var xsltNamespace = doc.Root.Name.Namespace; + var candidates = doc + .Descendants() + .Where(e => ShouldInstrument(e, xsltNamespace)) + .Select(e => (Element: e, Line: GetLineNumber(e))) + .Where(tuple => tuple.Line.HasValue) + .ToList(); + + if (XsltEngineManager.TraceEnabled) + { + try + { + var linesText = string.Join(",", candidates.Select(c => c.Line!.Value).Distinct().OrderBy(x => x)); + XsltEngineManager.NotifyOutput($"[trace] instrumented lines (xslt1) for '{stylesheetPath}': [{linesText}]"); + } + catch + { + // Ignore logging issues + } + } + + foreach (var (element, line) in candidates) + { + if (element.Parent == null) + { + continue; + } + + var isXsltElement = element.Name.Namespace == xsltNamespace; + var localName = element.Name.LocalName; + + var isForEach = isXsltElement && string.Equals(localName, "for-each", StringComparison.OrdinalIgnoreCase); + var isNamedTemplate = isXsltElement && + string.Equals(localName, "template", StringComparison.OrdinalIgnoreCase) && + element.Attribute("name") != null; + + var breakSelect = isNamedTemplate + ? $"dbg:break({line!.Value}, ., 'template-entry')" + : $"dbg:break({line!.Value}, .)"; + var breakCall = new XElement(xsltNamespace + "value-of", + new XAttribute("select", breakSelect)); + + if (addProbeAttribute) + { + breakCall.SetAttributeValue(debugNamespace + "probe", "1"); + } + + XElement? forEachMessage = null; + if (isForEach) + { + var selectAttr = element.Attribute("select")?.Value ?? "(none)"; + forEachMessage = new XElement(xsltNamespace + "message", + new XElement(xsltNamespace + "text", $"[DBG] for-each line={line!.Value} select={selectAttr} pos="), + new XElement(xsltNamespace + "value-of", new XAttribute("select", "position()"))); + + if (addProbeAttribute) + { + forEachMessage.SetAttributeValue(debugNamespace + "probe", "1"); + } + } + + var parent = element.Parent; + var parentIsXslt = parent?.Name.Namespace == xsltNamespace; + + if (parentIsXslt && string.Equals(parent!.Name.LocalName, "choose", StringComparison.OrdinalIgnoreCase)) + { + if (isXsltElement && + (string.Equals(element.Name.LocalName, "when", StringComparison.OrdinalIgnoreCase) || + string.Equals(element.Name.LocalName, "otherwise", StringComparison.OrdinalIgnoreCase))) + { + element.AddFirst(breakCall); + } + continue; + } + + if (isForEach) + { + var lastSort = element.Elements() + .Where(e => e.Name.Namespace == xsltNamespace && + string.Equals(e.Name.LocalName, "sort", StringComparison.OrdinalIgnoreCase)) + .LastOrDefault(); + + if (lastSort != null) + { + if (forEachMessage != null) + { + lastSort.AddAfterSelf(forEachMessage); + } + lastSort.AddAfterSelf(breakCall); + } + else + { + if (forEachMessage != null) + { + element.AddFirst(forEachMessage); + } + element.AddFirst(breakCall); + } + } + else if (isNamedTemplate) + { + var lastParamOrVar = element.Elements() + .Where(e => e.Name.Namespace == xsltNamespace && + (e.Name.LocalName == "param" || e.Name.LocalName == "variable")) + .LastOrDefault(); + + if (lastParamOrVar != null) + { + lastParamOrVar.AddAfterSelf(breakCall); + } + else + { + element.AddFirst(breakCall); + } + } + else if (CanInsertAsFirstChild(element, isXsltElement)) + { + element.AddFirst(breakCall); + } + else + { + element.AddBeforeSelf(breakCall); + } + + if (isNamedTemplate) + { + EnsureTemplateExitProbe(element, line!.Value, xsltNamespace, debugNamespace, addProbeAttribute); + } + } + } + + public static void InstrumentVariables(XDocument doc, XNamespace debugNamespace, bool addProbeAttribute) + { + if (doc.Root == null) + { + return; + } + + var xsltNamespace = doc.Root.Name.Namespace; + + var variables = doc + .Descendants() + .Where(e => e.Name.Namespace == xsltNamespace && + (e.Name.LocalName == "variable" || e.Name.LocalName == "param")) + .Where(e => e.Attribute("name") != null) + .Where(e => !IsTopLevelDeclaration(e, xsltNamespace)) + .ToList(); + + if (XsltEngineManager.IsLogEnabled) + { + XsltEngineManager.NotifyOutput($"[debug] Instrumenting {variables.Count} variable(s) for debugging"); + } + + var groupedByParent = variables.GroupBy(v => v.Parent).ToList(); + + foreach (var group in groupedByParent) + { + var parent = group.Key; + if (parent == null) + { + continue; + } + + var safeVars = new List<(XElement element, string name)>(); + foreach (var variable in group) + { + var varName = variable.Attribute("name")?.Value; + if (string.IsNullOrEmpty(varName)) + { + continue; + } + + if (!IsSafeToInstrumentVariable(variable, xsltNamespace)) + { + if (XsltEngineManager.IsLogEnabled) + { + XsltEngineManager.NotifyOutput($"[debug] Skipped unsafe instrumentation: ${varName}"); + } + continue; + } + + safeVars.Add((variable, varName)); + } + + if (safeVars.Count == 0) + { + continue; + } + + var lastParamOrVar = parent.Elements() + .Where(e => e.Name.Namespace == xsltNamespace && + (e.Name.LocalName == "param" || e.Name.LocalName == "variable")) + .LastOrDefault(); + + if (lastParamOrVar == null) + { + continue; + } + + foreach (var (_, varName) in safeVars) + { + var debugMessage = new XElement( + xsltNamespace + "message", + new XElement(xsltNamespace + "text", $"[DBG] {varName} "), + new XElement(xsltNamespace + "value-of", new XAttribute("select", $"${varName}"))); + + if (addProbeAttribute) + { + debugMessage.SetAttributeValue(debugNamespace + "probe", "1"); + } + + lastParamOrVar.AddAfterSelf(debugMessage); + lastParamOrVar = debugMessage; + + if (XsltEngineManager.IsLogEnabled) + { + XsltEngineManager.NotifyOutput($"[debug] Instrumented variable: ${varName}"); + } + } + } + } + + private static bool ShouldInstrument(XElement element, XNamespace xsltNamespace) + { + if (element.Parent == null) + { + return false; + } + + if (element.Ancestors().Any(a => a.Name.Namespace == xsltNamespace && a.Name.LocalName is "message")) + { + return false; + } + + if (element.Name.Namespace == xsltNamespace) + { + var localName = element.Name.LocalName; + return localName switch + { + "stylesheet" or "transform" => false, + "attribute-set" or "decimal-format" or "import" or "include" or "key" or "namespace-alias" or "output" or "preserve-space" or "strip-space" => false, + "param" or "variable" or "with-param" => false, + "message" => false, + "sort" => false, + _ => true + }; + } + + var nearestXsltAncestor = element.Ancestors().FirstOrDefault(a => a.Name.Namespace == xsltNamespace); + if (nearestXsltAncestor == null) + { + return false; + } + + var ancestorLocal = nearestXsltAncestor.Name.LocalName; + if (ancestorLocal is "stylesheet" or "transform") + { + return false; + } + + return true; + } + + private static int? GetLineNumber(XElement element) + { + if (element is IXmlLineInfo info && info.HasLineInfo()) + { + return info.LineNumber; + } + return null; + } + + private static bool CanInsertAsFirstChild(XElement element, bool isXsltElement) + { + if (element == null || !isXsltElement) + { + return false; + } + + var parent = element.Parent; + var localName = element.Name.LocalName; + + if (XsltCompiledEngine.InlineInstrumentationTargets.Contains(localName)) + { + return true; + } + + if (parent != null) + { + var parentLocal = parent.Name.LocalName; + if (string.Equals(parentLocal, "stylesheet", StringComparison.OrdinalIgnoreCase) || + string.Equals(parentLocal, "transform", StringComparison.OrdinalIgnoreCase) || + string.Equals(parentLocal, "choose", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + if (element.IsEmpty) + { + return false; + } + + if (XsltCompiledEngine.ElementsDisallowingChildInstrumentation.Contains(localName)) + { + return false; + } + + return true; + } + + private static void EnsureTemplateExitProbe( + XElement templateElement, + int lineNumber, + XNamespace xsltNamespace, + XNamespace debugNamespace, + bool addProbeAttribute) + { + var exitSelect = $"dbg:break({lineNumber}, ., 'template-exit')"; + + var existing = templateElement + .Elements() + .FirstOrDefault(e => + e.Name.Namespace == xsltNamespace && + string.Equals(e.Name.LocalName, "value-of", StringComparison.OrdinalIgnoreCase) && + string.Equals(e.Attribute("select")?.Value, exitSelect, StringComparison.Ordinal)); + + if (existing != null) + { + if (addProbeAttribute) + { + existing.SetAttributeValue(debugNamespace + "probe", "1"); + } + return; + } + + var exitCall = new XElement( + xsltNamespace + "value-of", + new XAttribute("select", exitSelect)); + + if (addProbeAttribute) + { + exitCall.SetAttributeValue(debugNamespace + "probe", "1"); + } + + templateElement.Add(exitCall); + } + + private static bool IsSafeToInstrumentVariable(XElement variable, XNamespace xsltNamespace) + { + var parent = variable.Parent; + if (parent == null) + { + return false; + } + + if (HasFragileAncestor(variable, xsltNamespace)) + { + return false; + } + + var parentLocalName = parent.Name.LocalName; + var parentIsXslt = parent.Name.Namespace == xsltNamespace; + + if (parentIsXslt) + { + switch (parentLocalName) + { + case "attribute": + case "comment": + case "processing-instruction": + case "namespace": + case "output": + case "key": + case "decimal-format": + case "character-map": + case "variable": + case "param": + case "with-param": + return false; + } + } + + var attributeAncestor = variable.Ancestors() + .FirstOrDefault(a => a.Name.Namespace == xsltNamespace && + a.Name.LocalName == "attribute"); + if (attributeAncestor != null) + { + return false; + } + + return true; + } + + private static bool IsTopLevelDeclaration(XElement element, XNamespace xsltNamespace) + { + var parent = element.Parent; + if (parent != null && parent.Name.Namespace == xsltNamespace) + { + var parentLocal = parent.Name.LocalName; + if (parentLocal == "stylesheet" || parentLocal == "transform") + { + return true; + } + } + + return false; + } + + private static bool HasFragileAncestor(XElement element, XNamespace xsltNamespace) + { + return element.Ancestors() + .Any(a => a.Name.Namespace == xsltNamespace && + (a.Name.LocalName == "attribute" || + a.Name.LocalName == "comment" || + a.Name.LocalName == "processing-instruction" || + a.Name.LocalName == "namespace")); + } +} diff --git a/XsltDebugger.DebugAdapter/XsltCompiledEngine.cs b/XsltDebugger.DebugAdapter/XsltCompiledEngine.cs index 3509ee5..6c4f7fb 100644 --- a/XsltDebugger.DebugAdapter/XsltCompiledEngine.cs +++ b/XsltDebugger.DebugAdapter/XsltCompiledEngine.cs @@ -182,8 +182,8 @@ await Task.Run(() => } EnsureDebugNamespace(xdoc); - InstrumentStylesheet(xdoc); - InstrumentVariables(xdoc); + Xslt1Instrumentation.InstrumentStylesheet(xdoc, _currentStylesheet, DebugNamespace, addProbeAttribute: false); + Xslt1Instrumentation.InstrumentVariables(xdoc, DebugNamespace, addProbeAttribute: false); // DEBUG: Save instrumented XSLT for inspection if (XsltEngineManager.IsTraceEnabled) @@ -523,178 +523,6 @@ private void EnsureDebugNamespace(XDocument doc) } } - private void InstrumentStylesheet(XDocument doc) - { - if (doc.Root == null) - { - return; - } - - var xsltNamespace = doc.Root.Name.Namespace; - var candidates = doc - .Descendants() - .Where(e => ShouldInstrument(e, xsltNamespace)) - .Select(e => (Element: e, Line: GetLineNumber(e))) - .Where(tuple => tuple.Line.HasValue) - .ToList(); - - if (XsltEngineManager.TraceEnabled) - { - try - { - var linesText = string.Join(",", candidates.Select(c => c.Line!.Value).Distinct().OrderBy(x => x)); - XsltEngineManager.NotifyOutput($"[trace] instrumented lines (compiled) for '{_currentStylesheet}': [{linesText}]"); - } - catch { } - } - - foreach (var (element, line) in candidates) - { - if (element.Parent == null) - { - continue; - } - - var isXsltElement = element.Name.Namespace == xsltNamespace; - var localName = element.Name.LocalName; - - // Check if this is a for-each element (needs special position logging) - var isForEach = isXsltElement && string.Equals(localName, "for-each", StringComparison.OrdinalIgnoreCase); - - // Check if this is a named template (for step-into support) - var isNamedTemplate = isXsltElement && - string.Equals(localName, "template", StringComparison.OrdinalIgnoreCase) && - element.Attribute("name") != null; - - // Create breakpoint call with template-entry marker if it's a named template - var breakCall = isNamedTemplate - ? new XElement(xsltNamespace + "value-of", - new XAttribute("select", $"dbg:break({line!.Value}, ., 'template-entry')")) - : new XElement(xsltNamespace + "value-of", - new XAttribute("select", $"dbg:break({line!.Value}, .)")); - - // Create for-each position logging message (XSLT 1.0 compatible) - XElement? forEachMessage = null; - if (isForEach) - { - var selectAttr = element.Attribute("select")?.Value ?? "(none)"; - - // XSLT 1.0 compatible: use xsl:text and xsl:value-of inside xsl:message - // Cannot use 'select' attribute on xsl:message in XSLT 1.0 (only available in XSLT 3.0) - forEachMessage = new XElement(xsltNamespace + "message", - new XElement(xsltNamespace + "text", $"[DBG] for-each line={line!.Value} select={selectAttr} pos="), - new XElement(xsltNamespace + "value-of", new XAttribute("select", "position()")) - ); - } - - var parent = element.Parent; - var parentIsXslt = parent?.Name.Namespace == xsltNamespace; - - if (parentIsXslt && string.Equals(parent!.Name.LocalName, "choose", StringComparison.OrdinalIgnoreCase)) - { - if (isXsltElement && (string.Equals(element.Name.LocalName, "when", StringComparison.OrdinalIgnoreCase) || - string.Equals(element.Name.LocalName, "otherwise", StringComparison.OrdinalIgnoreCase))) - { - element.AddFirst(breakCall); - } - continue; - } - - // Special handling for for-each elements - // Must insert AFTER any xsl:sort elements - if (isForEach) - { - // Find the last xsl:sort element - var lastSort = element.Elements() - .Where(e => e.Name.Namespace == xsltNamespace && - string.Equals(e.Name.LocalName, "sort", StringComparison.OrdinalIgnoreCase)) - .LastOrDefault(); - - if (lastSort != null) - { - // Insert after the last sort - // AddAfterSelf inserts immediately after, so we add in reverse order - // Want: - // So: add message first (goes after sort), then add breakCall (goes between sort and message) - if (forEachMessage != null) - { - lastSort.AddAfterSelf(forEachMessage); - } - lastSort.AddAfterSelf(breakCall); - } - else - { - // No sorts, insert as first child - // AddFirst inserts at position 0, so we add in reverse order - // Want: - // So: add message first (position 0), then breakCall (new position 0, pushing message to 1) - if (forEachMessage != null) - { - element.AddFirst(forEachMessage); - } - element.AddFirst(breakCall); - } - } - // Special handling for named templates with params/variables - // In XSLT 1.0, ALL params and variables must come first - else if (isNamedTemplate) - { - // Find the last param/variable in this template - var lastParamOrVar = element.Elements() - .Where(e => e.Name.Namespace == xsltNamespace && - (e.Name.LocalName == "param" || e.Name.LocalName == "variable")) - .LastOrDefault(); - - if (lastParamOrVar != null) - { - // Insert the template-entry breakpoint AFTER all params/variables - lastParamOrVar.AddAfterSelf(breakCall); - } - else - { - // No params or variables, safe to insert as first child - element.AddFirst(breakCall); - } - } - else if (CanInsertAsFirstChild(element, isXsltElement)) - { - element.AddFirst(breakCall); - } - else - { - element.AddBeforeSelf(breakCall); - } - - if (isNamedTemplate) - { - EnsureTemplateExitBreakpoint(element, line!.Value, xsltNamespace); - } - } - } - - private static void EnsureTemplateExitBreakpoint(XElement templateElement, int lineNumber, XNamespace xsltNamespace) - { - var exitSelect = $"dbg:break({lineNumber}, ., 'template-exit')"; - - var existing = templateElement - .Elements() - .FirstOrDefault(e => - e.Name.Namespace == xsltNamespace && - string.Equals(e.Name.LocalName, "value-of", StringComparison.OrdinalIgnoreCase) && - string.Equals(e.Attribute("select")?.Value, exitSelect, StringComparison.Ordinal)); - - if (existing != null) - { - return; - } - - var exitCall = new XElement( - xsltNamespace + "value-of", - new XAttribute("select", exitSelect)); - - templateElement.Add(exitCall); - } - private static XPathNavigator? CloneContextNavigator(XPathNavigator? context) { if (context == null) @@ -841,265 +669,6 @@ private static string GetXPathToNavigator(XPathNavigator navigator) } } - private static bool ShouldInstrument(XElement element, XNamespace xsltNamespace) - { - if (element.Parent == null) - { - return false; - } - - // Exclude any elements inside xsl:message to avoid instrumentation conflicts - if (element.Ancestors().Any(a => a.Name.Namespace == xsltNamespace && a.Name.LocalName is "message")) - { - return false; - } - - if (element.Name.Namespace == xsltNamespace) - { - var localName = element.Name.LocalName; - return localName switch - { - "stylesheet" or "transform" => false, - "attribute-set" or "decimal-format" or "import" or "include" or "key" or "namespace-alias" or "output" or "preserve-space" or "strip-space" => false, - "param" or "variable" or "with-param" => false, - // Exclude xsl:message to avoid instrumentation conflicts - "message" => false, - // Exclude xsl:sort because it must be the first child of xsl:for-each - "sort" => false, - _ => true - }; - } - - var nearestXsltAncestor = element.Ancestors().FirstOrDefault(a => a.Name.Namespace == xsltNamespace); - if (nearestXsltAncestor == null) - { - return false; - } - - var ancestorLocal = nearestXsltAncestor.Name.LocalName; - if (ancestorLocal is "stylesheet" or "transform") - { - return false; - } - - return true; - } - - private static int? GetLineNumber(XElement element) - { - if (element is IXmlLineInfo info && info.HasLineInfo()) - { - return info.LineNumber; - } - return null; - } - - private static bool CanInsertAsFirstChild(XElement element, bool isXsltElement) - { - if (element == null) - { - return false; - } - - if (!isXsltElement) - { - return false; - } - - var parent = element.Parent; - var localName = element.Name.LocalName; - - if (InlineInstrumentationTargets.Contains(localName)) - { - return true; - } - - if (parent != null) - { - var parentLocal = parent.Name.LocalName; - if (string.Equals(parentLocal, "stylesheet", StringComparison.OrdinalIgnoreCase) || - string.Equals(parentLocal, "transform", StringComparison.OrdinalIgnoreCase) || - string.Equals(parentLocal, "choose", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - if (element.IsEmpty) - { - return false; - } - - if (ElementsDisallowingChildInstrumentation.Contains(localName)) - { - return false; - } - - return true; - } - - private static void InstrumentVariables(XDocument doc) - { - if (doc.Root == null) - { - return; - } - - var xsltNamespace = doc.Root.Name.Namespace; - - // Find all xsl:variable and xsl:param elements - var variables = doc - .Descendants() - .Where(e => e.Name.Namespace == xsltNamespace && - (e.Name.LocalName == "variable" || e.Name.LocalName == "param")) - .Where(e => e.Attribute("name") != null) - .Where(e => !IsTopLevelDeclaration(e, xsltNamespace)) - .ToList(); - - if (XsltEngineManager.IsLogEnabled) - { - XsltEngineManager.NotifyOutput($"[debug] Instrumenting {variables.Count} variable(s) for debugging"); - } - - // Group variables by parent to handle templates correctly - // In XSLT 1.0, ALL params and variables must come first in a template - var groupedByParent = variables.GroupBy(v => v.Parent).ToList(); - - foreach (var group in groupedByParent) - { - var parent = group.Key; - if (parent == null) continue; - - // Collect safe variables in this group - var safeVars = new List<(XElement element, string name)>(); - foreach (var variable in group) - { - var varName = variable.Attribute("name")?.Value; - if (string.IsNullOrEmpty(varName)) - { - continue; - } - - if (!IsSafeToInstrumentVariable(variable, xsltNamespace)) - { - if (XsltEngineManager.IsLogEnabled) - { - XsltEngineManager.NotifyOutput($"[debug] Skipped unsafe instrumentation: ${varName}"); - } - continue; - } - - safeVars.Add((variable, varName)); - } - - if (safeVars.Count == 0) continue; - - // Find the last param/variable in this parent - // This ensures we insert AFTER all params/variables, not between them - var lastParamOrVar = parent.Elements() - .Where(e => e.Name.Namespace == xsltNamespace && - (e.Name.LocalName == "param" || e.Name.LocalName == "variable")) - .LastOrDefault(); - - if (lastParamOrVar == null) continue; - - // Insert debug messages for each variable, but AFTER the last param/variable - foreach (var (variable, varName) in safeVars) - { - var debugMessage = new XElement( - xsltNamespace + "message", - new XElement(xsltNamespace + "text", $"[DBG] {varName} "), - new XElement(xsltNamespace + "value-of", new XAttribute("select", $"${varName}")) - ); - - // Insert after the last param/variable, not after each one - lastParamOrVar.AddAfterSelf(debugMessage); - // Update lastParamOrVar so next message goes after this one - lastParamOrVar = debugMessage; - - if (XsltEngineManager.IsLogEnabled) - { - XsltEngineManager.NotifyOutput($"[debug] Instrumented variable: ${varName}"); - } - } - } - } - - private static bool IsSafeToInstrumentVariable(XElement variable, XNamespace xsltNamespace) - { - var parent = variable.Parent; - if (parent == null) - { - return false; - } - - if (HasFragileAncestor(variable, xsltNamespace)) - { - return false; - } - - var parentLocalName = parent.Name.LocalName; - var parentIsXslt = parent.Name.Namespace == xsltNamespace; - - if (parentIsXslt) - { - switch (parentLocalName) - { - case "attribute": - case "comment": - case "processing-instruction": - case "namespace": - case "output": - case "key": - case "decimal-format": - case "character-map": - case "variable": - case "param": - case "with-param": - return false; - } - } - - // Don't instrument variables inside xsl:attribute - var attributeAncestor = variable.Ancestors() - .FirstOrDefault(a => a.Name.Namespace == xsltNamespace && - a.Name.LocalName == "attribute"); - if (attributeAncestor != null) - { - return false; - } - - return true; - } - - private static bool IsTopLevelDeclaration(XElement element, XNamespace xsltNamespace) - { - // Check if this is a top-level variable/param (direct child of stylesheet/transform) - var parent = element.Parent; - if (parent != null && parent.Name.Namespace == xsltNamespace) - { - var parentLocal = parent.Name.LocalName; - if (parentLocal == "stylesheet" || parentLocal == "transform") - { - return true; - } - } - - return false; - } - - private static bool HasFragileAncestor(XElement element, XNamespace xsltNamespace) - { - // For XSLT 1.0, we have fewer fragile contexts than XSLT 2.0/3.0 - // Main concerns: attribute generation, key/sort contexts - return element.Ancestors() - .Any(a => a.Name.Namespace == xsltNamespace && - (a.Name.LocalName == "attribute" || - a.Name.LocalName == "comment" || - a.Name.LocalName == "processing-instruction" || - a.Name.LocalName == "namespace")); - } - private static List ExtractUsingStatements(string code) { var usings = new List(); diff --git a/XsltDebugger.Tests/CompiledEngineIntegrationTests.cs b/XsltDebugger.Tests/CompiledEngineIntegrationTests.cs index 47e3241..f43ee68 100644 --- a/XsltDebugger.Tests/CompiledEngineIntegrationTests.cs +++ b/XsltDebugger.Tests/CompiledEngineIntegrationTests.cs @@ -27,7 +27,7 @@ private static string GetTestDataPath(string relativePath) [Fact] public async Task CompiledEngine_ShouldTransformInlineScriptSample_WithTraceLogging() { - var stylesheetPath = GetTestDataPath("Integration/xslt/v1/sample-inline-cs-with-usings.xslt"); + var stylesheetPath = GetTestDataPath("Integration/xslt/compiled/sample-inline-cs-with-usings.xslt"); var xmlPath = GetTestDataPath("Integration/xml/sample-inline-cs-with-usings.xml"); var fullStylesheetPath = Path.GetFullPath(stylesheetPath); var fullXmlPath = Path.GetFullPath(xmlPath); @@ -110,7 +110,7 @@ void OnStopped(string file, int line, DebugStopReason reason) [Fact] public async Task CompiledEngine_ShouldCaptureVariablesAndHitBreakpoints() { - var stylesheetPath = GetTestDataPath("Integration/xslt/v1/VariableLoggingSampleV1.xslt"); + var stylesheetPath = GetTestDataPath("Integration/xslt/compiled/VariableLoggingSampleV1.xslt"); var xmlPath = GetTestDataPath("Integration/xml/VariableLoggingSampleV1Input.xml"); var fullStylesheetPath = Path.GetFullPath(stylesheetPath); var fullXmlPath = Path.GetFullPath(xmlPath); @@ -191,7 +191,7 @@ void OnStopped(string file, int line, DebugStopReason reason) [Fact] public async Task CompiledEngine_ShouldInstrumentInlineCSharpMethods() { - var stylesheetPath = GetTestDataPath("Integration/xslt/v1/sample-inline-cs-auto-instrument.xslt"); + var stylesheetPath = GetTestDataPath("Integration/xslt/compiled/sample-inline-cs-auto-instrument.xslt"); var xmlPath = GetTestDataPath("Integration/xml/sample-inline-cs-auto-instrument.xml"); var fullStylesheetPath = Path.GetFullPath(stylesheetPath); var fullXmlPath = Path.GetFullPath(xmlPath); @@ -262,7 +262,7 @@ void OnOutput(string message) [Fact] public async Task CompiledEngine_ShouldIncludeXsltLineNumbersInInlineCSharpLogs() { - var stylesheetPath = GetTestDataPath("Integration/xslt/v1/ShipmentConfv1.xslt"); + var stylesheetPath = GetTestDataPath("Integration/xslt/compiled/ShipmentConfv1.xslt"); var xmlPath = GetTestDataPath("Integration/xml/ShipmentConf-proper.xml"); var fullStylesheetPath = Path.GetFullPath(stylesheetPath); var fullXmlPath = Path.GetFullPath(xmlPath); @@ -323,7 +323,7 @@ void OnOutput(string message) [Fact] public async Task CompiledEngine_ShouldNotDoubleInstrumentManuallyLoggedMethods() { - var stylesheetPath = GetTestDataPath("Integration/xslt/v1/sample-inline-cs-with-usings.xslt"); + var stylesheetPath = GetTestDataPath("Integration/xslt/compiled/sample-inline-cs-with-usings.xslt"); var xmlPath = GetTestDataPath("Integration/xml/sample-inline-cs-with-usings.xml"); var fullStylesheetPath = Path.GetFullPath(stylesheetPath); var fullXmlPath = Path.GetFullPath(xmlPath); @@ -382,7 +382,7 @@ void OnOutput(string message) [Fact] public async Task CompiledEngine_ShouldLogForEachPositionWithoutSort() { - var stylesheetPath = GetTestDataPath("Integration/xslt/v1/foreach-test.xslt"); + var stylesheetPath = GetTestDataPath("Integration/xslt/compiled/foreach-test.xslt"); var xmlPath = GetTestDataPath("Integration/xml/foreach-test.xml"); var fullStylesheetPath = Path.GetFullPath(stylesheetPath); var fullXmlPath = Path.GetFullPath(xmlPath); @@ -454,7 +454,7 @@ void OnOutput(string message) [Fact] public async Task CompiledEngine_ShouldLogForEachPositionWithSort() { - var stylesheetPath = GetTestDataPath("Integration/xslt/v1/foreach-test.xslt"); + var stylesheetPath = GetTestDataPath("Integration/xslt/compiled/foreach-test.xslt"); var xmlPath = GetTestDataPath("Integration/xml/foreach-test.xml"); var fullStylesheetPath = Path.GetFullPath(stylesheetPath); var fullXmlPath = Path.GetFullPath(xmlPath); @@ -517,7 +517,7 @@ void OnOutput(string message) [Fact] public async Task CompiledEngine_ShouldLogForEachInShipmentConfv1() { - var stylesheetPath = GetTestDataPath("Integration/xslt/v1/ShipmentConfv1.xslt"); + var stylesheetPath = GetTestDataPath("Integration/xslt/compiled/ShipmentConfv1.xslt"); var xmlPath = GetTestDataPath("Integration/xml/ShipmentConf-proper.xml"); var fullStylesheetPath = Path.GetFullPath(stylesheetPath); var fullXmlPath = Path.GetFullPath(xmlPath); diff --git a/XsltDebugger.Tests/SaxonEngineIntegrationTests.cs b/XsltDebugger.Tests/SaxonEngineIntegrationTests.cs index eb5750a..c9dcfbb 100644 --- a/XsltDebugger.Tests/SaxonEngineIntegrationTests.cs +++ b/XsltDebugger.Tests/SaxonEngineIntegrationTests.cs @@ -82,7 +82,7 @@ void OnOutput(string message) [Fact] public async Task SaxonEngine_ShouldCaptureVariablesAndHitBreakpoints() { - var stylesheetPath = GetTestDataPath("Integration/xslt/v2/VariableLoggingSample.xslt"); + var stylesheetPath = GetTestDataPath("Integration/xslt/saxon/VariableLoggingSample.xslt"); var xmlPath = GetTestDataPath("Integration/xml/ItemsSample.xml"); var fullStylesheetPath = Path.GetFullPath(stylesheetPath); var fullXmlPath = Path.GetFullPath(xmlPath); @@ -156,6 +156,80 @@ void OnStopped(string file, int line, DebugStopReason reason) } } + [Fact] + public async Task SaxonEngine_ShouldCaptureVariables_WhenRunningXslt1Stylesheet() + { + var stylesheetPath = GetTestDataPath("Integration/xslt/compiled/VariableLoggingSampleV1.xslt"); + var xmlPath = GetTestDataPath("Integration/xml/ItemsSample.xml"); + var fullStylesheetPath = Path.GetFullPath(stylesheetPath); + var fullXmlPath = Path.GetFullPath(xmlPath); + var breakpoints = new[] { (fullStylesheetPath, 8) }; + + var engine = new SaxonEngine(); + var outputLog = new List(); + var outputLock = new object(); + var breakpointHitSource = new TaskCompletionSource<(string file, int line, DebugStopReason reason)>(TaskCreationOptions.RunContinuationsAsynchronously); + var terminatedSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + void OnOutput(string message) + { + lock (outputLock) + { + outputLog.Add(message); + } + } + void OnStopped(string file, int line, DebugStopReason reason) + { + breakpointHitSource.TrySetResult((file, line, reason)); + _ = engine.ContinueAsync(); + } + void OnTerminated(int code) => terminatedSource.TrySetResult(code); + + XsltEngineManager.Reset(); + XsltEngineManager.SetDebugFlags(true, LogLevel.TraceAll); + XsltEngineManager.EngineOutput += OnOutput; + XsltEngineManager.EngineStopped += OnStopped; + XsltEngineManager.EngineTerminated += OnTerminated; + + try + { + engine.SetBreakpoints(breakpoints); + await engine.StartAsync(fullStylesheetPath, fullXmlPath, stopOnEntry: false); + + var breakpointHit = await breakpointHitSource.Task.WaitAsync(TimeSpan.FromSeconds(5)); + breakpointHit.file.Should().Be(fullStylesheetPath); + breakpointHit.line.Should().Be(8); + breakpointHit.reason.Should().Be(DebugStopReason.Breakpoint); + + var exitCode = await terminatedSource.Task.WaitAsync(TimeSpan.FromSeconds(5)); + exitCode.Should().Be(0, "successful transformation should terminate with 0 exit code"); + + XsltEngineManager.Variables.Should().ContainKey("count"); + XsltEngineManager.Variables["count"].Should().Be("2"); + + XsltEngineManager.Variables.Should().ContainKey("firstName"); + XsltEngineManager.Variables["firstName"].Should().Be("Alpha"); + + List snapshot; + lock (outputLock) + { + snapshot = outputLog.ToList(); + } + + snapshot.Should().Contain(message => message.Contains("[debug] Instrumenting 2 variable", StringComparison.OrdinalIgnoreCase)); + snapshot.Should().Contain(message => message.Contains("[debug] Instrumented variable: $count", StringComparison.OrdinalIgnoreCase)); + snapshot.Should().Contain(message => message.Contains("[debug] Instrumented variable: $firstName", StringComparison.OrdinalIgnoreCase)); + snapshot.Should().Contain(message => message.Contains("[DBG] count", StringComparison.OrdinalIgnoreCase)); + } + finally + { + XsltEngineManager.EngineOutput -= OnOutput; + XsltEngineManager.EngineStopped -= OnStopped; + XsltEngineManager.EngineTerminated -= OnTerminated; + XsltEngineManager.Reset(); + } + } + [Fact] public async Task SaxonEngine_ShouldReportCompilationErrors() { @@ -210,7 +284,7 @@ void OnOutput(string message) [Fact] public async Task SaxonEngine_ShouldTransformShipmentSample_WithTraceLogging() { - var stylesheetPath = GetTestDataPath("Integration/xslt/v3/ShipmentConf3.xslt"); + var stylesheetPath = GetTestDataPath("Integration/xslt/saxon/ShipmentConf3.xslt"); var xmlPath = GetTestDataPath("Integration/xml/ShipmentConf-proper.xml"); var fullStylesheetPath = Path.GetFullPath(stylesheetPath); var fullXmlPath = Path.GetFullPath(xmlPath); @@ -321,7 +395,7 @@ void OnOutput(string message) [Fact] public async Task SaxonEngine_ShouldTransformAdvancedXslt2_WithInstrumentation() { - var (log, outFile, exitCode) = await RunSaxonAsync("Integration/xslt/v2/AdvanceXslt2.xslt", "Integration/xml/AdvanceFile.xml"); + var (log, outFile, exitCode) = await RunSaxonAsync("Integration/xslt/saxon/AdvanceXslt2.xslt", "Integration/xml/AdvanceFile.xml"); try { exitCode.Should().Be(0); @@ -344,7 +418,7 @@ public async Task SaxonEngine_ShouldTransformAdvancedXslt2_WithInstrumentation() [Fact] public async Task SaxonEngine_ShouldTransformAdvancedXslt3_WithAccumulatorInstrumentation() { - var (log, outFile, exitCode) = await RunSaxonAsync("Integration/xslt/v3/AdvanceXslt3.xslt", "Integration/xml/AdvanceFile.xml"); + var (log, outFile, exitCode) = await RunSaxonAsync("Integration/xslt/saxon/AdvanceXslt3.xslt", "Integration/xml/AdvanceFile.xml"); try { exitCode.Should().Be(0); diff --git a/docs/EngineFeatureComparison.md b/docs/EngineFeatureComparison.md index e4cfe33..94fa6ee 100644 --- a/docs/EngineFeatureComparison.md +++ b/docs/EngineFeatureComparison.md @@ -11,6 +11,7 @@ - Builds a Saxon HE/PE processor, enables XSLT 3.0 features, and compiles stylesheets to a reusable executable for full 2.0/3.0 behavior. - Only instruments stylesheets when debugging is enabled, registering a custom Saxon extension function to surface probes. - Blocks inline C# by reusing engine validation logic and reports static compilation issues (line/column) via Saxon’s `ErrorList`. +- Switches between XSLT 1.0-safe probes (value-of/message) and XSLT 2.0/3.0 probes (`xsl:sequence` with tuple logging) based on the stylesheet `version` attribute. ## Shared Debug Surface - Breakpoints: both inject `dbg:break()` calls, mark template entry/exit, and honor user breakpoints through shared stepping state (`StepMode` with depth tracking). @@ -36,13 +37,13 @@ | Feature Area | Only in XsltCompiledEngine | Only in SaxonEngine | | --- | --- | --- | -| XSLT version focus | Inline C# compatible XSLT 1.0 runtime | Native XSLT 2.0/3.0 execution with Saxon | +| XSLT version focus | Inline C# compatible XSLT 1.0 runtime | XSLT 1.0 (no inline C#) plus native XSLT 2.0/3.0 execution | | Inline scripting | Compiles `msxsl:script` via Roslyn and instruments generated code | Explicitly blocked; prompts user to switch engines | | Instrumentation safety | Tailored to 1.0 structural rules and limited fragile contexts | Expansive guards for 2.0/3.0 constructs (accumulators, iterate/try, merge) with `dbg:probe` markers | | Variable capture | Text/value-of messages per template, respecting 1.0 ordering | Tuple/sequence messages with `string-join` across sequences | | Compilation diagnostics | Relies on transform exceptions | Surfaces Saxon static errors with module URI, line, and column | ## Selection Guidance -- Choose **XsltCompiledEngine** for XSLT 1.0 stylesheets, projects relying on inline C#, or environments tied to the .NET runtime. -- Choose **SaxonEngine** for XSLT 2.0/3.0 transformations, schema-aware features, or when richer compile-time diagnostics and sequence-aware variable logging are required. +- Choose **XsltCompiledEngine** for XSLT 1.0 stylesheets that require inline C# or when targeting the .NET runtime exclusively. +- Choose **SaxonEngine** for XSLT 1.0 stylesheets without `msxsl:script`, XSLT 2.0/3.0 transformations, schema-aware features, or when richer compile-time diagnostics and sequence-aware variable logging are required. - Maintain separate launch configurations so each stylesheet uses the engine aligned with its language level and extension requirements. diff --git a/package-lock.json b/package-lock.json index 791ebd3..25186b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "xsltdebugger", - "version": "0.0.1", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "xsltdebugger", - "version": "0.0.1", + "version": "1.0.0", "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "22.x", diff --git a/package.json b/package.json index 60c9378..865ed71 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "xsltdebugger-darwin", "displayName": "XSLT Debugger for macOS-arm64", "description": "A VS Code extension for debugging XSLT using a .NET Debug Adapter.", - "version": "0.7.0", + "version": "1.0.0", "publisher": "DanielJonathan", "repository": { "type": "git",