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
16 changes: 16 additions & 0 deletions changelog.d/unreleased/1440.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 1440
affected:
- src/CodeIndex/Indexer/References/Support/LanguageReferenceExtractionSupport.cs
- tests/CodeIndex.Tests/ReferenceExtractorTests.cs
---

## English

- **Go generic methods now keep method type parameters in reference extraction (#1440)** - receiver methods such as `func (r *Repo) Get[T Constraint](input Input) T` no longer treat the type-parameter list as the value-parameter list, so constraints, parameter types, and returns stay attributed to the method.

## 日本語

- **Go のジェネリックメソッドで型パラメータを参照抽出に残すようになりました (#1440)** - `func (r *Repo) Get[T Constraint](input Input) T` のような receiver メソッドで型パラメータリストを値パラメータリストと誤認せず、制約・引数型・戻り値の参照をメソッドに紐づけます。
18 changes: 18 additions & 0 deletions changelog.d/unreleased/1445.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
category: fixed
issues:
- 1445
affected:
- src/CodeIndex/Indexer/References/ReferenceExtractor.cs
- src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs
- tests/CodeIndex.Tests/SymbolExtractorTests.cs
- tests/CodeIndex.Tests/ReferenceExtractorTests.cs
---

## English

- **PHP 8.4 property hooks now expose accessor scopes and hook-body references (#1445)** - `get` and `set` hook bodies are attached to the property, so references inside hook expressions resolve under `property.get` / `property.set` accessors.

## 日本語

- **PHP 8.4 property hook が accessor scope と hook 本文の参照を公開するようになりました (#1445)** - `get` / `set` hook 本文をプロパティに紐づけ、hook 式内の参照を `property.get` / `property.set` accessor 配下で解決します。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/1479.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 1479
affected:
- src/CodeIndex/Indexer/References/Support/LanguageReferenceExtractionSupport.cs
- tests/CodeIndex.Tests/ReferenceExtractorTests.cs
---

## English

- **Razor component tag references now preserve dotted component names (#1479)** - tags such as `<MyApp.Components.Forms.LoginButton />` emit a call reference to the fully qualified component name instead of dropping namespace segments.

## 日本語

- **Razor component tag 参照で dot 付き component 名を保持するようになりました (#1479)** - `<MyApp.Components.Forms.LoginButton />` のようなタグで namespace 部分を落とさず、完全修飾 component 名への call 参照を出します。
2 changes: 1 addition & 1 deletion src/CodeIndex/Indexer/References/ReferenceExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -952,7 +952,7 @@ private static StringComparer GetDefinitionNamesComparer(string language)
private static List<SymbolRecord> BuildReferenceContainerCandidates(IReadOnlyList<SymbolRecord> symbols)
=> symbols
.Where(symbol => symbol.BodyStartLine != null && symbol.BodyEndLine != null &&
(IsFunctionLikeSymbolKind(symbol.Kind) || symbol.Kind == "hook" || symbol.Kind == "class"
(IsFunctionLikeSymbolKind(symbol.Kind) || symbol.Kind == "hook" || symbol.Kind == "accessor" || symbol.Kind == "class"
|| symbol.Kind == "struct" || symbol.Kind == "namespace"
|| symbol.Kind == "object" || symbol.Kind == "property" || symbol.Kind == "class_hook"))
.OrderBy(symbol => (symbol.BodyEndLine ?? symbol.EndLine) - (symbol.BodyStartLine ?? symbol.StartLine))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ internal static class LanguageReferenceExtractionSupport
RegexOptions.Compiled | RegexOptions.CultureInvariant);

private static readonly Regex RazorComponentTagRegex = new(
@"<(?<name>[A-Z][A-Za-z0-9_]*(?:\.[A-Za-z_]\w*)?)(?=[\s>/])",
@"<(?<name>[A-Z][A-Za-z0-9_]*(?:\.[A-Za-z_]\w*)*)(?=[\s>/])",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex RazorDirectiveTypeRegex = new(
@"^\s*@(?:inherits|implements|model)\s+(?<type>[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)",
Expand Down Expand Up @@ -660,11 +660,10 @@ public static void EmitRazorReferences(
{
var group = match.Groups["name"];
var rawName = group.Value;
var name = LastQualifiedSegment(rawName);
var name = rawName;
if (definitionNames?.Contains(name) == true)
continue;
var nameOffset = rawName.LastIndexOf(name, StringComparison.Ordinal);
var nameIndex = group.Index + Math.Max(0, nameOffset);
var nameIndex = group.Index;

ReferenceExtractor.AddReference(
references,
Expand Down Expand Up @@ -4725,8 +4724,29 @@ private static void EmitGoFunctionSignatureTypes(
if (nextParen > afterReceiver)
{
EmitGoParameterListTypes(preparedLine, firstParen + 1, receiverClose, references, seen, fileId, context, lineNumber, resolveContainerForColumn);
parameterOpen = nextParen;
functionHeaderStart = afterReceiver;

var afterName = afterReceiver + 1;
while (afterName < preparedLine.Length && IsSimpleIdentifierPart(preparedLine[afterName]))
afterName++;
while (afterName < preparedLine.Length && char.IsWhiteSpace(preparedLine[afterName]))
afterName++;

if (afterName < preparedLine.Length && preparedLine[afterName] == '[')
{
var typeParameterClose = ReferenceExtractor.FindMatchingChar(preparedLine, afterName, '[', ']');
if (typeParameterClose > afterName)
{
EmitGoTypeParameterConstraints(preparedLine, afterName, typeParameterClose + 1, references, seen, fileId, context, lineNumber, resolveContainerForColumn);
var valueParameterOpen = preparedLine.IndexOf('(', typeParameterClose + 1);
if (valueParameterOpen < 0)
return;

nextParen = valueParameterOpen;
}
}

parameterOpen = nextParen;
}
}
}
Expand Down
82 changes: 82 additions & 0 deletions src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,9 @@ private readonly record struct JavaScriptClassScanTarget(
private static readonly Regex JavaCompactConstructorRegex = new(
@"^\s*(?:(?<visibility>public|private|protected)\s+)?(?<name>\w+)\s*(?=\{|$)",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex PhpPropertyHookAccessorRegex = new(
@"^\s*(?<name>get|set)\b",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex DartClassDeclarationRegex = new(
@"^\s*(?:(?:abstract|base|final|interface|sealed)\s+)*(?:mixin\s+)?class\s+\w+",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
Expand Down Expand Up @@ -4060,6 +4063,8 @@ public static List<SymbolRecord> Extract(long fileId, string? lang, string conte
ExtractPhpDocblockTypeAliasSymbols(fileId, lines, symbols);
if (lang == "php")
ExtractPhpDocblockImportTypeSymbols(fileId, lines, symbols);
if (lang == "php")
ExtractPhpPropertyHookSupplementalSymbols(fileId, lines, structuralLines, symbols);
if (lang == "swift")
ExtractSwiftPropertySupplementalSymbols(fileId, lines, structuralLines, symbols);
if (lang == "sql")
Expand Down Expand Up @@ -4856,6 +4861,83 @@ internal static bool IsJavaScriptTypeScriptReactHookName(string name)
&& IsJavaScriptTypeScriptIdentifierStart(name[3])
&& char.IsUpper(name[3]);

private static void ExtractPhpPropertyHookSupplementalSymbols(
long fileId,
string[] lines,
string[] structuralLines,
List<SymbolRecord> symbols)
{
var existing = new HashSet<string>(
symbols.Select(symbol => $"{symbol.Kind}:{symbol.Name}:{symbol.Line}"),
StringComparer.Ordinal);

foreach (var property in symbols
.Where(symbol => symbol.Kind == "property"
&& symbol.Line >= 1
&& symbol.Line <= lines.Length)
.ToArray())
{
var lineIndex = property.Line - 1;
var openBraceColumn = structuralLines[lineIndex].IndexOf('{', StringComparison.Ordinal);
if (openBraceColumn < 0)
continue;

var closeBraceLine = FindBraceRangeEndLine(structuralLines, lineIndex, openBraceColumn);
if (closeBraceLine <= lineIndex)
continue;

var sawAccessor = false;
for (var accessorLine = lineIndex + 1; accessorLine <= closeBraceLine; accessorLine++)
{
var accessorMatch = PhpPropertyHookAccessorRegex.Match(structuralLines[accessorLine]);
if (!accessorMatch.Success)
continue;

var accessorName = accessorMatch.Groups["name"].Value;
var symbolName = $"{property.Name}.{accessorName}";
var key = $"accessor:{symbolName}:{accessorLine + 1}";
if (!existing.Add(key))
continue;

var accessorBodyEndLine = accessorLine;
var accessorNameEnd = accessorMatch.Groups["name"].Index + accessorMatch.Groups["name"].Length;
var accessorOpenBraceColumn = structuralLines[accessorLine].IndexOf('{', accessorNameEnd);
if (accessorOpenBraceColumn >= 0)
{
var accessorCloseBraceLine = FindBraceRangeEndLine(structuralLines, accessorLine, accessorOpenBraceColumn);
if (accessorCloseBraceLine > accessorLine && accessorCloseBraceLine <= closeBraceLine)
accessorBodyEndLine = accessorCloseBraceLine;
}

sawAccessor = true;
symbols.Add(new SymbolRecord
{
FileId = fileId,
Kind = "accessor",
Name = symbolName,
Line = accessorLine + 1,
StartLine = accessorLine + 1,
StartColumn = accessorMatch.Groups["name"].Index,
EndLine = accessorBodyEndLine + 1,
BodyStartLine = accessorLine + 1,
BodyEndLine = accessorBodyEndLine + 1,
Signature = lines[accessorLine].Trim(),
ContainerKind = "property",
ContainerName = property.Name,
ContainerQualifiedName = property.ContainerQualifiedName,
});
}

if (sawAccessor)
{
property.SubKind = CombineSubKinds(property.SubKind, "php_property_hook");
property.EndLine = Math.Max(property.EndLine, closeBraceLine + 1);
property.BodyStartLine = lineIndex + 1;
property.BodyEndLine = closeBraceLine + 1;
}
}
}

private static void ExtractSwiftPropertySupplementalSymbols(
long fileId,
string[] lines,
Expand Down
81 changes: 79 additions & 2 deletions tests/CodeIndex.Tests/ReferenceExtractorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,40 @@ go worker()
&& reference.Line == 18);
}

[Fact]
public void Extract_Go_GenericMethodsEmitTypeParameterAndParameterReferences()
{
const string content = """
package demo

type Repo struct {}
type Constraint interface {}
type Input struct {}

func (r *Repo) Get[T Constraint](input Input) T {
var zero T
return zero
}
""";

var symbols = SymbolExtractor.Extract(1, "go", content);
var references = ReferenceExtractor.Extract(1, "go", content, symbols);

Assert.Contains(symbols, symbol => symbol.Kind == "function" && symbol.Name == "Get");
Assert.Contains(references, reference =>
reference.SymbolName == "Constraint"
&& reference.ReferenceKind == "type_reference"
&& reference.ContainerName == "Get");
Assert.Contains(references, reference =>
reference.SymbolName == "Input"
&& reference.ReferenceKind == "type_reference"
&& reference.ContainerName == "Get");
Assert.Contains(references, reference =>
reference.SymbolName == "T"
&& reference.ReferenceKind == "type_reference"
&& reference.ContainerName == "Get");
}

[Fact]
public void TryGetExtractor_RegisteredLanguage_ReturnsAddressableExtractor()
{
Expand Down Expand Up @@ -11480,6 +11514,43 @@ function inspect(User $user): void {
Assert.Contains(references, reference => reference.SymbolName == "greet" && reference.ReferenceKind == "call");
}

[Fact]
public void Extract_PhpPropertyHooks_EmitReferencesInsideHookBodies()
{
const string content = """
<?php
class User {
public string $displayName {
get => $this->firstName . ' ' . $this->lastName;
set {
$this->_displayName = strtoupper($value);
}
}
}
?>
""";

var symbols = SymbolExtractor.Extract(1, "php", content);
var references = ReferenceExtractor.Extract(1, "php", content, symbols);

Assert.Contains(references, reference =>
reference.SymbolName == "firstName"
&& reference.ReferenceKind == "reference"
&& reference.ContainerName == "displayName.get");
Assert.Contains(references, reference =>
reference.SymbolName == "lastName"
&& reference.ReferenceKind == "reference"
&& reference.ContainerName == "displayName.get");
Assert.Contains(references, reference =>
reference.SymbolName == "_displayName"
&& reference.ReferenceKind == "reference"
&& reference.ContainerName == "displayName.set");
Assert.Contains(references, reference =>
reference.SymbolName == "strtoupper"
&& reference.ReferenceKind == "call"
&& reference.ContainerName == "displayName.set");
}

[Fact]
public void Extract_PhpLanguageConstructCalls_AreIgnored()
{
Expand Down Expand Up @@ -13640,6 +13711,7 @@ @inject Services.UserService UserService

<UserCard User="CurrentUser" />
<Shared.DetailPanel />
<MyApp.Components.Forms.LoginButton OnClick="HandleClick" />
<button @onclick="HandleClick">Save</button>
<button @onclick="@HandleClick">Save explicit</button>
<button @onclick="InheritedClick">Inherited</button>
Expand Down Expand Up @@ -13670,14 +13742,19 @@ @inject Services.UserService UserService
var qualifiedComponentColumn = content
.Split('\n')
.Single(line => line.Contains("Shared.DetailPanel", StringComparison.Ordinal))
.IndexOf("DetailPanel", StringComparison.Ordinal) + 1;
.IndexOf("Shared.DetailPanel", StringComparison.Ordinal) + 1;
var nestedComponentColumn = content
.Split('\n')
.Single(line => line.Contains("MyApp.Components.Forms.LoginButton", StringComparison.Ordinal))
.IndexOf("MyApp.Components.Forms.LoginButton", StringComparison.Ordinal) + 1;

Assert.Contains(references, r => r.SymbolName == "BasePage" && r.ReferenceKind == "type_reference");
Assert.Contains(references, r => r.SymbolName == "IUserActions" && r.ReferenceKind == "type_reference");
Assert.Contains(references, r => r.SymbolName == "Authorize" && r.ReferenceKind == "type_reference");
Assert.Contains(references, r => r.SymbolName == "UserService" && r.ReferenceKind == "type_reference");
Assert.Contains(references, r => r.SymbolName == "UserCard" && r.ReferenceKind == "call");
Assert.Contains(references, r => r.SymbolName == "DetailPanel" && r.ReferenceKind == "call" && r.Column == qualifiedComponentColumn);
Assert.Contains(references, r => r.SymbolName == "Shared.DetailPanel" && r.ReferenceKind == "call" && r.Column == qualifiedComponentColumn);
Assert.Contains(references, r => r.SymbolName == "MyApp.Components.Forms.LoginButton" && r.ReferenceKind == "call" && r.Column == nestedComponentColumn);
Assert.Contains(references, r => r.SymbolName == "HandleClick" && r.ReferenceKind == "razor_event_binding");
Assert.DoesNotContain(references, r =>
r.SymbolName == "HandleClick"
Expand Down
27 changes: 27 additions & 0 deletions tests/CodeIndex.Tests/SymbolExtractorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13741,6 +13741,33 @@ class User {
Assert.DoesNotContain(symbols, s => s.Kind == "property" && s.Name == "notAProperty");
}

[Fact]
public void Extract_PHP_DetectsPropertyHookAccessors()
{
var content = """
<?php
class User {
public string $displayName {
get => $this->firstName . ' ' . $this->lastName;
set {
$this->_displayName = strtoupper($value);
}
}
}
""";

var symbols = SymbolExtractor.Extract(1, "php", content);

var property = Assert.Single(symbols, s => s.Kind == "property" && s.Name == "displayName");
Assert.Equal("php_property_hook", property.SubKind);
Assert.Equal(3, property.StartLine);
Assert.Equal(8, property.EndLine);
Assert.Equal(3, property.BodyStartLine);
Assert.Equal(8, property.BodyEndLine);
Assert.Contains(symbols, s => s.Kind == "accessor" && s.Name == "displayName.get" && s.ContainerKind == "property" && s.ContainerName == "displayName");
Assert.Contains(symbols, s => s.Kind == "accessor" && s.Name == "displayName.set" && s.ContainerKind == "property" && s.ContainerName == "displayName" && s.BodyEndLine == 7);
}

[Fact]
public void Extract_PHP_DetectsSameLinePromotedConstructorProperties()
{
Expand Down
Loading