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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Xunit;

namespace Microsoft.AspNetCore.Razor.Language;

public class DefaultTagHelperResolutionPhaseTest
{
[Fact]
public void MergeSourceSpans_SameLine_ReturnsCorrectSpan()
{
// Arrange
var filePath = "test.razor";
var first = new SourceSpan(filePath, absoluteIndex: 10, lineIndex: 2, characterIndex: 5, length: 3, lineCount: 0, endCharacterIndex: 8);
var last = new SourceSpan(filePath, absoluteIndex: 15, lineIndex: 2, characterIndex: 10, length: 4, lineCount: 0, endCharacterIndex: 14);

// Act
var result = DefaultTagHelperResolutionPhase.MergeSourceSpans(first, last);

// Assert
Assert.Equal(filePath, result.FilePath);
Assert.Equal(10, result.AbsoluteIndex); // starts at first
Assert.Equal(2, result.LineIndex); // same line as first
Assert.Equal(5, result.CharacterIndex); // same column as first
Assert.Equal(9, result.Length); // (15 + 4) - 10 = 9
Assert.Equal(0, result.LineCount); // 2 + 0 - 2 = 0 (same line)
Assert.Equal(14, result.EndCharacterIndex); // taken from last
}

[Fact]
public void MergeSourceSpans_MultiLine_ReturnsCorrectSpan()
{
// Arrange
var filePath = "test.razor";
// first spans lines 1-2 (lineCount = 1 means it crosses into the next line)
var first = new SourceSpan(filePath, absoluteIndex: 0, lineIndex: 1, characterIndex: 0, length: 10, lineCount: 1, endCharacterIndex: 5);
// last is on line 3 (lineIndex = 3)
var last = new SourceSpan(filePath, absoluteIndex: 20, lineIndex: 3, characterIndex: 2, length: 5, lineCount: 0, endCharacterIndex: 7);

// Act
var result = DefaultTagHelperResolutionPhase.MergeSourceSpans(first, last);

// Assert
Assert.Equal(filePath, result.FilePath);
Assert.Equal(0, result.AbsoluteIndex); // starts at first
Assert.Equal(1, result.LineIndex); // line of first
Assert.Equal(0, result.CharacterIndex); // column of first
Assert.Equal(25, result.Length); // (20 + 5) - 0 = 25
Assert.Equal(2, result.LineCount); // (3 + 0) - 1 = 2
Assert.Equal(7, result.EndCharacterIndex); // end column from last
}

[Fact]
public void MergeSourceSpans_AdjacentSpans_ReturnsCorrectSpan()
{
// Arrange
var filePath = "test.razor";
var first = new SourceSpan(filePath, absoluteIndex: 5, lineIndex: 0, characterIndex: 5, length: 3, lineCount: 0, endCharacterIndex: 8);
// last starts right where first ends
var last = new SourceSpan(filePath, absoluteIndex: 8, lineIndex: 0, characterIndex: 8, length: 4, lineCount: 0, endCharacterIndex: 12);

// Act
var result = DefaultTagHelperResolutionPhase.MergeSourceSpans(first, last);

// Assert
Assert.Equal(5, result.AbsoluteIndex);
Assert.Equal(7, result.Length); // (8 + 4) - 5 = 7
Assert.Equal(0, result.LineCount);
Assert.Equal(12, result.EndCharacterIndex);
}

[Fact]
public void MergeSourceSpans_SameSpan_ReturnsEquivalentSpan()
{
// Arrange
var filePath = "test.razor";
var span = new SourceSpan(filePath, absoluteIndex: 10, lineIndex: 1, characterIndex: 3, length: 5, lineCount: 0, endCharacterIndex: 8);

// Act — first and last are the same span
var result = DefaultTagHelperResolutionPhase.MergeSourceSpans(span, span);

// Assert
Assert.Equal(10, result.AbsoluteIndex);
Assert.Equal(1, result.LineIndex);
Assert.Equal(3, result.CharacterIndex);
Assert.Equal(5, result.Length); // (10 + 5) - 10 = 5
Assert.Equal(0, result.LineCount); // 1 + 0 - 1 = 0
Assert.Equal(8, result.EndCharacterIndex);
}

[Fact]
public void MergeSourceSpans_NullFilePath_PreservesNullFilePath()
{
// Arrange — file path is null (e.g. for in-memory content)
var first = new SourceSpan(filePath: null, absoluteIndex: 0, lineIndex: 0, characterIndex: 0, length: 3, lineCount: 0, endCharacterIndex: 3);
var last = new SourceSpan(filePath: null, absoluteIndex: 5, lineIndex: 0, characterIndex: 5, length: 2, lineCount: 0, endCharacterIndex: 7);

// Act
var result = DefaultTagHelperResolutionPhase.MergeSourceSpans(first, last);

// Assert
Assert.Null(result.FilePath);
Assert.Equal(0, result.AbsoluteIndex);
Assert.Equal(7, result.Length); // (5 + 2) - 0 = 7
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -580,15 +580,7 @@ private static void MergeAdjacentCSharpTokens(IntermediateNode node)
SourceSpan? mergedSpan = null;
if (firstSpan is { } first && lastSpan is { } last)
{
var endAbsolute = last.AbsoluteIndex + last.Length;
mergedSpan = new SourceSpan(
first.FilePath,
first.AbsoluteIndex,
first.LineIndex,
first.CharacterIndex,
endAbsolute - first.AbsoluteIndex,
last.LineIndex - first.LineIndex + 1,
last.EndCharacterIndex);
mergedSpan = MergeSourceSpans(first, last);
}

var content = sb.ToString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -509,19 +509,14 @@ CSharpExpressionIntermediateNode or
targetNode.Children.Add(new CSharpIntermediateToken(content, source));
}
}
else if (isBoundStringProperty && hasDynamicContent)
{
// Bound string property with dynamic content (expressions/code blocks):
// Unwrap attribute value nodes to content nodes for BeginWriteTagHelperAttribute pattern.
UnwrapValueChildrenToTokens(targetNode, htmlAttr);
}
else if (!isBoundStringProperty && hasDynamicContent)
{
ConvertDynamicNonStringValueChildren(targetNode, htmlAttr, sourceDocument);
}
else
{
// Complex/dynamic value - unwrap attribute value nodes to content nodes.
// Bound string property with dynamic content, or complex/non-dynamic fallback:
// unwrap attribute value nodes to content nodes for BeginWriteTagHelperAttribute pattern.
UnwrapValueChildrenToTokens(targetNode, htmlAttr);
}

Expand Down Expand Up @@ -772,21 +767,12 @@ private static void ConvertPureCSharpExpressionValue(IntermediateNode targetNode
attrSource.FilePath, closeParenAbsIndex, attrSource.LineIndex, closeParenCharIndex,
1, 0, closeParenCharIndex + 1);
targetNode.Children.Add(new CSharpIntermediateToken(")", closeParenSource));
return;
}
else
{
UnwrapValueChildrenToTokens(targetNode, htmlAttr);
}
}
else
{
UnwrapValueChildrenToTokens(targetNode, htmlAttr);
}
}
else
{
UnwrapValueChildrenToTokens(targetNode, htmlAttr);
}

UnwrapValueChildrenToTokens(targetNode, htmlAttr);
}

private static void UnwrapValueChildrenToTokens(IntermediateNode targetNode, HtmlAttributeIntermediateNode htmlAttr)
Expand Down Expand Up @@ -1148,14 +1134,7 @@ private static void MergeAdjacentHtmlContent(IntermediateNode parent, int index,
current.Children.AddRange(next.Children);
if (current.Source is SourceSpan currentSource && next.Source is SourceSpan nextSource)
{
current.Source = new SourceSpan(
currentSource.FilePath,
currentSource.AbsoluteIndex,
currentSource.LineIndex,
currentSource.CharacterIndex,
(nextSource.AbsoluteIndex + nextSource.Length) - currentSource.AbsoluteIndex,
nextSource.LineCount,
nextSource.EndCharacterIndex);
current.Source = MergeSourceSpans(currentSource, nextSource);
}
else if (current.Source == null)
{
Expand Down Expand Up @@ -1383,8 +1362,7 @@ private static void LowerImplicitExpressionAttribute_Legacy(

var mergedContent = sb.ToString();
var tokenSpan = firstSpan is { } f && lastSpan is { } l
? new SourceSpan(f.FilePath, f.AbsoluteIndex, f.LineIndex, f.CharacterIndex,
(l.AbsoluteIndex + l.Length) - f.AbsoluteIndex, l.LineIndex - f.LineIndex, l.EndCharacterIndex)
? MergeSourceSpans(f, l)
: firstSpan;
expr.Children.Add(new CSharpIntermediateToken(
LazyContent.Create(mergedContent, static s => s), tokenSpan));
Expand Down Expand Up @@ -1606,8 +1584,7 @@ private static void FlushPendingLiterals(

if (pendingFirstSpan is { } f && pendingLastSpan is { } l)
{
htmlContent.Source = new SourceSpan(f.FilePath, f.AbsoluteIndex, f.LineIndex, f.CharacterIndex,
(l.AbsoluteIndex + l.Length) - f.AbsoluteIndex, l.LineIndex - f.LineIndex + 1, l.EndCharacterIndex);
htmlContent.Source = MergeSourceSpans(f, l);
}

target.Children.Add(htmlContent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Microsoft.AspNetCore.Razor.Language.Components;
Expand Down Expand Up @@ -392,10 +393,7 @@ private static void MergeAdjacentHtmlContent(IntermediateNode parent)
if (current.Source is SourceSpan cs && next.Source is SourceSpan ns)
{
// Adjacent nodes are sequential, so next always ends after current.
var end = ns.AbsoluteIndex + ns.Length;
var lineCount = (ns.LineIndex + ns.LineCount) - cs.LineIndex;
current.Source = new SourceSpan(cs.FilePath, cs.AbsoluteIndex, cs.LineIndex, cs.CharacterIndex,
end - cs.AbsoluteIndex, lineCount, ns.EndCharacterIndex);
current.Source = MergeSourceSpans(cs, ns);
}
else if (current.Source == null)
{
Expand Down Expand Up @@ -545,8 +543,7 @@ private static void ConvertUnresolvedValuesToBasicForm(HtmlAttributeIntermediate
var lastSrc = htmlContent.Children[^1].Source;
if (firstSrc is { } fs && lastSrc is { } ls)
{
htmlContent.Source = new SourceSpan(fs.FilePath, fs.AbsoluteIndex, fs.LineIndex, fs.CharacterIndex,
(ls.AbsoluteIndex + ls.Length) - fs.AbsoluteIndex, ls.LineIndex - fs.LineIndex, ls.EndCharacterIndex);
htmlContent.Source = MergeSourceSpans(fs, ls);
}
}

Expand Down Expand Up @@ -743,20 +740,11 @@ private static AttributeStructure InferAttributeStructure(HtmlAttributeIntermedi
SourceSpan? result = null;
foreach (var child in htmlAttr.Children)
{
// For HtmlAttributeValueIntermediateNode, use the inner token sources (not the wrapper).
if (child is HtmlAttributeValueIntermediateNode attrValue)
// For HtmlAttributeValueIntermediateNode and CSharpExpressionAttributeValueIntermediateNode,
// use the inner token sources (not the wrapper).
if (child is HtmlAttributeValueIntermediateNode or CSharpExpressionAttributeValueIntermediateNode)
{
foreach (var token in attrValue.Children)
{
if (token.Source is SourceSpan tokenSource)
{
result = result == null ? tokenSource : MergeSpans(result.Value, tokenSource);
}
}
}
else if (child is CSharpExpressionAttributeValueIntermediateNode csharpAttrValue)
{
foreach (var token in csharpAttrValue.Children)
foreach (var token in child.Children)
{
if (token.Source is SourceSpan tokenSource)
{
Expand All @@ -783,6 +771,24 @@ private static SourceSpan MergeSpans(SourceSpan a, SourceSpan b)
end - start, lineCount, last.EndCharacterIndex);
}

/// <summary>
/// Merges two already-ordered source spans into a single span covering both.
/// <paramref name="first"/> must start at or before <paramref name="last"/>.
/// </summary>
internal static SourceSpan MergeSourceSpans(SourceSpan first, SourceSpan last)
{
Debug.Assert(first.AbsoluteIndex <= last.AbsoluteIndex,
"first span must start at or before the last span");
return new SourceSpan(
first.FilePath,
first.AbsoluteIndex,
first.LineIndex,
first.CharacterIndex,
last.AbsoluteIndex + last.Length - first.AbsoluteIndex,
last.LineIndex + last.LineCount - first.LineIndex,
last.EndCharacterIndex);
}

/// <summary>
/// Splits an explicit expression <c>@(expr)</c> at the given source position into
/// structured <see cref="CSharpIntermediateToken"/> children: <c>@</c>, <c>(</c>,
Expand Down