Skip to content

Commit f9bb758

Browse files
authored
Merge pull request #4 from imdj360/foreachfeature
Foreachfeature
2 parents e26a3bd + 7a43e5b commit f9bb758

18 files changed

Lines changed: 296 additions & 36 deletions

.vscodeignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ vsc-extension-quickstart.md
88
package.json.backup
99
TestData/**
1010
docs/**
11+
BestPractices_Presentation.md
12+
TEST_README.md
1113
**/tsconfig.json
1214
**/eslint.config.mjs
1315
**/*.map
@@ -103,7 +105,6 @@ XsltDebugger.sln
103105
sample/**
104106
.claude/**
105107
tools/**
106-
changelog.md
107108
package-clean.sh
108109

109110
# Test projects

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@
22

33
All notable changes to the XSLT Debugger extension will be documented in this file.
44

5+
## [1.0.2] - 2025
6+
7+
### Added
8+
9+
- **XSLT 2.0/3.0 Function Parameter Debugging**: Added `SaxonInstrumentation.cs` helper to instrument `xsl:function` elements with parameter logging.
10+
- Function parameters are now logged during execution with format: `[function functionName] param1=value1, param2=value2, ...`.
11+
- For-each loop instrumentation now works correctly with both Saxon and Compiled engines.
12+
13+
### Changed
14+
15+
- Simplified function instrumentation to only log parameters (removed verbose return statement logging).
16+
- Unified instrumentation approach for XSLT 1.0 across both Saxon and Compiled engines using shared `Xslt1Instrumentation` helper.
17+
18+
### Improved
19+
20+
- Cleaner debug output with reduced verbosity for function calls.
21+
522
## [1.0.1] - 2025
623

724
### Added
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ShipmentConfirmation>
3+
<Reference>REF-12345</Reference>
4+
<TransportTypeCode>TRK</TransportTypeCode>
5+
<TransportTypeDescription>Truck</TransportTypeDescription>
6+
<TransportMode>Road</TransportMode>
7+
<Direction>Inbound</Direction>
8+
<LicensePlate>ABC-123</LicensePlate>
9+
<Status>Completed</Status>
10+
<Arrival>2025-10-15T08:00:00</Arrival>
11+
<Departure>2025-10-15T16:30:00</Departure>
12+
<Net>1500.50</Net>
13+
<Orders>
14+
<Number>ORD-001</Number>
15+
<Date>2025-10-14</Date>
16+
<CustomerCode>CUST-100</CustomerCode>
17+
<CustomerName>Acme Corporation</CustomerName>
18+
<OrderItems>
19+
<Sequence>1</Sequence>
20+
<OperationName>Loading</OperationName>
21+
<OperationCode>LOAD</OperationCode>
22+
<OperationReports>
23+
<ReportInfo>
24+
<OperationReportDate>2025-10-15T10:30:00</OperationReportDate>
25+
</ReportInfo>
26+
<ReportInfo>
27+
<OperationReportDate>2025-10-15T12:00:00</OperationReportDate>
28+
</ReportInfo>
29+
</OperationReports>
30+
</OrderItems>
31+
<OrderItems>
32+
<Sequence>2</Sequence>
33+
<OperationName>Unloading</OperationName>
34+
<OperationCode>UNLOAD</OperationCode>
35+
<OperationReports>
36+
<ReportInfo>
37+
<OperationReportDate>2025-10-15T14:00:00</OperationReportDate>
38+
</ReportInfo>
39+
</OperationReports>
40+
</OrderItems>
41+
</Orders>
42+
</ShipmentConfirmation>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<root>
3+
<value>42</value>
4+
</root>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Student xmlns="http://StudentEnrollment.Student">
3+
<FirstName>John</FirstName>
4+
<MiddleInitial>A</MiddleInitial>
5+
<LastName>Smith</LastName>
6+
<DateOfBirth>2000-05-15</DateOfBirth>
7+
<Address>
8+
<StreetAddress>123 Main Street</StreetAddress>
9+
<City>Springfield</City>
10+
<State>il</State>
11+
<Zip>62701</Zip>
12+
<ContactType>Mobile</ContactType>
13+
<Contact>555-1234</Contact>
14+
</Address>
15+
</Student>

TestData/Integration/xslt/saxon/AdvanceXslt3.xslt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
<!-- Lenient parse: try xs:dateTime($s); if that fails, try xs:date($s)||'T00:00:00' -->
2626
<xsl:function name="f:to-datetime" as="xs:dateTime?">
2727
<xsl:param name="s" as="xs:string?" />
28-
<xsl:variable name="res">
28+
<xsl:variable name="res" as="xs:dateTime?">
2929
<xsl:choose>
30-
<xsl:when test="empty($s)" />
30+
<xsl:when test="empty($s) or $s = ''" />
3131
<xsl:otherwise>
3232
<xsl:try>
3333
<xsl:sequence select="xs:dateTime($s)" />
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<xsl:stylesheet version="2.0"
3+
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
4+
xmlns:xs="http://www.w3.org/2001/XMLSchema"
5+
xmlns:my="urn:my-functions">
6+
7+
<xsl:output method="xml" indent="yes"/>
8+
9+
<!-- Simple function that doubles a number -->
10+
<xsl:function name="my:double" as="xs:integer">
11+
<xsl:param name="n" as="xs:integer"/>
12+
<xsl:sequence select="$n * 2"/>
13+
</xsl:function>
14+
15+
<!-- Function with multiple parameters -->
16+
<xsl:function name="my:add" as="xs:integer">
17+
<xsl:param name="a" as="xs:integer"/>
18+
<xsl:param name="b" as="xs:integer"/>
19+
<xsl:sequence select="$a + $b"/>
20+
</xsl:function>
21+
22+
<!-- Main template -->
23+
<xsl:template match="/">
24+
<result>
25+
<double>
26+
<xsl:value-of select="my:double(5)"/>
27+
</double>
28+
<add>
29+
<xsl:value-of select="my:add(10, 20)"/>
30+
</add>
31+
</result>
32+
</xsl:template>
33+
34+
</xsl:stylesheet>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<xsl:stylesheet version="3.0"
3+
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
4+
xmlns:xs="http://www.w3.org/2001/XMLSchema"
5+
xmlns:ef="http://azure.workflow.datamapper.extensions">
6+
7+
<xsl:output method="xml" indent="yes"/>
8+
9+
<!-- Function to calculate age from date of birth -->
10+
<xsl:function name="ef:age" as="xs:float">
11+
<xsl:param name="inputDate" as="xs:date" />
12+
<xsl:value-of select="round(days-from-duration(current-date() - xs:date($inputDate)) div 365.25, 1)" />
13+
</xsl:function>
14+
15+
<!-- Main template -->
16+
<xsl:template match="/">
17+
<result>
18+
<age>
19+
<xsl:value-of select="ef:age(xs:date('2000-05-15'))"/>
20+
</age>
21+
</result>
22+
</xsl:template>
23+
24+
</xsl:stylesheet>

XsltDebugger.DebugAdapter/SaxonEngine.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ await Task.Run(() =>
157157
{
158158
InstrumentStylesheet(xdoc);
159159
InstrumentVariables(xdoc);
160+
// XSLT 2.0/3.0 only: Instrument functions (not available in XSLT 1.0)
161+
SaxonInstrumentation.InstrumentFunctions(xdoc, DebugNamespace, addProbeAttribute: true);
160162
if (XsltEngineManager.IsLogEnabled)
161163
{
162164
XsltEngineManager.NotifyOutput("Debugging enabled for XSLT 2.0/3.0.");
@@ -746,6 +748,9 @@ private void EnsureDebugNamespace(XDocument doc)
746748
}
747749
}
748750

751+
// Elements that are fragile for breakpoint AND variable instrumentation
752+
// Note: "function" blocks variable/param capture because inserting xsl:message
753+
// between params and return value violates XSLT spec for functions
749754
private static readonly HashSet<string> FragileAnywhere = new(StringComparer.OrdinalIgnoreCase)
750755
{
751756
"function",
@@ -1177,9 +1182,11 @@ private static List<XElement> BuildProbesForElement(XElement element, int line,
11771182
{
11781183
var selectAttr = element.Attribute("select")?.Value ?? string.Empty;
11791184
var safeSelect = string.IsNullOrWhiteSpace(selectAttr) ? "(none)" : EscapeApostrophes(selectAttr.Trim());
1180-
var loopLabel = string.Equals(localName, "for-each-group", StringComparison.OrdinalIgnoreCase)
1185+
var loopBase = string.Equals(localName, "for-each-group", StringComparison.OrdinalIgnoreCase)
11811186
? "for-each-group"
11821187
: "for-each";
1188+
// Include line number in variable name to make each for-each unique (prevents nested loops from overwriting)
1189+
var loopLabel = $"{loopBase}-{line}";
11831190
var messageSelect =
11841191
$"('[DBG]', '{loopLabel}', concat('line={line} select={safeSelect} ', 'pos=', string(position())))";
11851192
var messageProbe = new XElement(
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using System.Linq;
2+
using System.Xml.Linq;
3+
4+
namespace XsltDebugger.DebugAdapter;
5+
6+
/// <summary>
7+
/// Instrumentation helpers specific to XSLT 2.0/3.0 features (Saxon engine)
8+
/// </summary>
9+
internal static class SaxonInstrumentation
10+
{
11+
/// <summary>
12+
/// Instruments xsl:function elements to log parameter values and return expressions.
13+
/// This is only applicable to XSLT 2.0/3.0 as xsl:function is not available in XSLT 1.0.
14+
/// </summary>
15+
public static void InstrumentFunctions(XDocument doc, XNamespace debugNamespace, bool addProbeAttribute)
16+
{
17+
if (doc.Root == null)
18+
{
19+
return;
20+
}
21+
22+
var xsltNamespace = doc.Root.Name.Namespace;
23+
24+
var functions = doc
25+
.Descendants()
26+
.Where(e => e.Name.Namespace == xsltNamespace && e.Name.LocalName == "function")
27+
.Where(e => e.Attribute("name") != null)
28+
.ToList();
29+
30+
if (XsltEngineManager.IsLogEnabled)
31+
{
32+
XsltEngineManager.NotifyOutput($"[debug] Instrumenting {functions.Count} function(s) for debugging");
33+
}
34+
35+
foreach (var function in functions)
36+
{
37+
var functionName = function.Attribute("name")?.Value ?? "unknown";
38+
39+
// Get all parameters
40+
var parameters = function.Elements()
41+
.Where(e => e.Name.Namespace == xsltNamespace && e.Name.LocalName == "param")
42+
.Where(e => e.Attribute("name") != null)
43+
.ToList();
44+
45+
if (parameters.Count == 0)
46+
{
47+
if (XsltEngineManager.IsLogEnabled)
48+
{
49+
XsltEngineManager.NotifyOutput($"[debug] Skipped function {functionName} - no parameters");
50+
}
51+
continue;
52+
}
53+
54+
// Find the last param element
55+
var lastParam = parameters.LastOrDefault();
56+
if (lastParam == null)
57+
{
58+
continue;
59+
}
60+
61+
// Build a single xsl:message with all parameters
62+
// Format: [function functionName] param1=$value1, param2=$value2, ...
63+
var debugMessage = new XElement(xsltNamespace + "message");
64+
65+
// Add opening text with function name
66+
debugMessage.Add(new XElement(xsltNamespace + "text", $"[function {functionName}] "));
67+
68+
// Add each parameter with its value
69+
for (int i = 0; i < parameters.Count; i++)
70+
{
71+
var param = parameters[i];
72+
var paramName = param.Attribute("name")?.Value;
73+
if (string.IsNullOrEmpty(paramName))
74+
{
75+
continue;
76+
}
77+
78+
// Add "param name="
79+
debugMessage.Add(new XElement(xsltNamespace + "text", $"{paramName}="));
80+
81+
// Add the parameter value
82+
debugMessage.Add(new XElement(xsltNamespace + "value-of", new XAttribute("select", $"${paramName}")));
83+
84+
// Add comma separator if not the last parameter
85+
if (i < parameters.Count - 1)
86+
{
87+
debugMessage.Add(new XElement(xsltNamespace + "text", ", "));
88+
}
89+
90+
if (XsltEngineManager.IsLogEnabled)
91+
{
92+
XsltEngineManager.NotifyOutput($"[debug] Instrumented function parameter: {functionName}::{paramName}");
93+
}
94+
}
95+
96+
if (addProbeAttribute)
97+
{
98+
debugMessage.SetAttributeValue(debugNamespace + "probe", "1");
99+
}
100+
101+
// Insert the combined message after the last parameter
102+
lastParam.AddAfterSelf(debugMessage);
103+
}
104+
}
105+
}

0 commit comments

Comments
 (0)