diff --git a/changelog.d/unreleased/1440.fixed.md b/changelog.d/unreleased/1440.fixed.md
new file mode 100644
index 0000000000..9489c71acc
--- /dev/null
+++ b/changelog.d/unreleased/1440.fixed.md
@@ -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 メソッドで型パラメータリストを値パラメータリストと誤認せず、制約・引数型・戻り値の参照をメソッドに紐づけます。
diff --git a/changelog.d/unreleased/1445.fixed.md b/changelog.d/unreleased/1445.fixed.md
new file mode 100644
index 0000000000..f8b7e33611
--- /dev/null
+++ b/changelog.d/unreleased/1445.fixed.md
@@ -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 配下で解決します。
diff --git a/changelog.d/unreleased/1479.fixed.md b/changelog.d/unreleased/1479.fixed.md
new file mode 100644
index 0000000000..82856d4dff
--- /dev/null
+++ b/changelog.d/unreleased/1479.fixed.md
@@ -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 `` emit a call reference to the fully qualified component name instead of dropping namespace segments.
+
+## 日本語
+
+- **Razor component tag 参照で dot 付き component 名を保持するようになりました (#1479)** - `` のようなタグで namespace 部分を落とさず、完全修飾 component 名への call 参照を出します。
diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs
index 2d6f6f6865..6e4f734718 100644
--- a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs
+++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs
@@ -952,7 +952,7 @@ private static StringComparer GetDefinitionNamesComparer(string language)
private static List BuildReferenceContainerCandidates(IReadOnlyList 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))
diff --git a/src/CodeIndex/Indexer/References/Support/LanguageReferenceExtractionSupport.cs b/src/CodeIndex/Indexer/References/Support/LanguageReferenceExtractionSupport.cs
index 2e93b8928a..6731446adf 100644
--- a/src/CodeIndex/Indexer/References/Support/LanguageReferenceExtractionSupport.cs
+++ b/src/CodeIndex/Indexer/References/Support/LanguageReferenceExtractionSupport.cs
@@ -535,7 +535,7 @@ internal static class LanguageReferenceExtractionSupport
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex RazorComponentTagRegex = new(
- @"<(?[A-Z][A-Za-z0-9_]*(?:\.[A-Za-z_]\w*)?)(?=[\s>/])",
+ @"<(?[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+(?[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)",
@@ -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,
@@ -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;
}
}
}
diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs
index 976d5a5c3f..acffdd257f 100644
--- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs
+++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs
@@ -645,6 +645,9 @@ private readonly record struct JavaScriptClassScanTarget(
private static readonly Regex JavaCompactConstructorRegex = new(
@"^\s*(?:(?public|private|protected)\s+)?(?\w+)\s*(?=\{|$)",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
+ private static readonly Regex PhpPropertyHookAccessorRegex = new(
+ @"^\s*(?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);
@@ -4060,6 +4063,8 @@ public static List 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")
@@ -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 symbols)
+ {
+ var existing = new HashSet(
+ 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,
diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs
index 5a9e33fcd8..294e40d863 100644
--- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs
+++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs
@@ -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()
{
@@ -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 = """
+ $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()
{
@@ -13640,6 +13711,7 @@ @inject Services.UserService UserService
+
@@ -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"
diff --git a/tests/CodeIndex.Tests/SymbolExtractorTests.cs b/tests/CodeIndex.Tests/SymbolExtractorTests.cs
index 2d8d779614..b95b8ad2e8 100644
--- a/tests/CodeIndex.Tests/SymbolExtractorTests.cs
+++ b/tests/CodeIndex.Tests/SymbolExtractorTests.cs
@@ -13741,6 +13741,33 @@ class User {
Assert.DoesNotContain(symbols, s => s.Kind == "property" && s.Name == "notAProperty");
}
+ [Fact]
+ public void Extract_PHP_DetectsPropertyHookAccessors()
+ {
+ var content = """
+ $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()
{