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

## English

- **Python metaclass and `__init_subclass__` references are now emitted (#2058)** — mixed class headers such as `class Derived(Base, Mixin, metaclass=Meta)` now emit references for both base classes and the metaclass, and `super().__init_subclass__()` now records a call edge to the lifecycle hook.

## 日本語

- **Python の metaclass と `__init_subclass__` 参照を出力するようになりました (#2058)** — `class Derived(Base, Mixin, metaclass=Meta)` のような混在 class header で base class と metaclass の両方を参照として出し、`super().__init_subclass__()` から lifecycle hook への call edge も記録します。
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ internal static class PythonReferenceExtractor
@"^\s*class\s+\w+\s*\(\s*(?<name>(?:[_\p{L}]\w*\.)*[_\p{Lu}]\w*)\s*\)\s*:",
RegexOptions.Compiled);
private static readonly Regex MultipleClassBaseTypesRegex = new(
@"^\s*class\s+\w+\s*\((?<types>[^=)]*,[^=)]*)\)\s*:",
@"^\s*class\s+\w+\s*\((?<types>[^)]*,[^)]*)\)\s*:",
RegexOptions.Compiled);
private static readonly Regex ClassMetaclassTypeRegex = new(
@"^\s*class\s+\w+\s*\([^)]*\bmetaclass\s*=\s*(?<name>(?:[_\p{L}]\w*\.)*[_\p{Lu}]\w*)",
Expand Down Expand Up @@ -514,6 +514,8 @@ public static void EmitClassBaseReferences(
var name = typeMatch.Groups["name"].Value;
if (isIgnoredName(name))
continue;
if (IsPythonClassHeaderKeywordArgument(typesGroup.Value, typeMatch.Groups["name"].Index))
continue;

var nameIndex = typesGroup.Index + typeMatch.Groups["name"].Index;
ReferenceExtractor.AddTypeReferenceSegments(
Expand Down Expand Up @@ -548,6 +550,31 @@ public static void EmitClassBaseReferences(
}
}

private static bool IsPythonClassHeaderKeywordArgument(string headerArguments, int nameIndex)
{
for (var i = nameIndex - 1; i >= 0; i--)
{
var ch = headerArguments[i];
if (char.IsWhiteSpace(ch))
continue;
if (ch == '=')
return true;
break;
}

for (var i = nameIndex; i < headerArguments.Length; i++)
{
var ch = headerArguments[i];
if (char.IsLetterOrDigit(ch) || ch == '_' || ch == '.')
continue;
if (char.IsWhiteSpace(ch))
continue;
return ch == '=';
}

return false;
}

public static void EmitFunctionReturnReferences(
string preparedLine,
List<ReferenceRecord> references,
Expand Down
3 changes: 1 addition & 2 deletions src/CodeIndex/Indexer/References/ReferenceExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ private static bool IsFunctionLikeSymbolKind(string kind)
// Python contextual keywords / Python の文脈キーワード
["python"] = new HashSet<string>(StringComparer.Ordinal)
{
"raise", "yield", "from",
"raise", "yield", "from", "super",
},
// Ruby contextual keywords / Ruby の文脈キーワード
["ruby"] = new HashSet<string>(StringComparer.Ordinal)
Expand Down Expand Up @@ -3149,7 +3149,6 @@ void AddGradleDslReference(string name, int callIndex)
lineNumber,
container,
name => IsIgnoredCallName(language, name));

if (pythonHeaderMap.HasValue)
RemapPythonLogicalHeaderReferences(references, pythonReferenceStart, pythonHeaderMap.Value, lines);
}
Expand Down
55 changes: 55 additions & 0 deletions tests/CodeIndex.Tests/ReferenceExtractorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1090,6 +1090,61 @@ def __init_subclass__(cls, plugin: Plugin) -> None:
&& reference.ContainerName == "__init_subclass__");
}

[Fact]
public void Extract_PythonMixedBasesAndMetaclass_EmitsBaseAndMetaclassReferences()
{
const string content = """
class Derived(Base, Mixin, metaclass=Meta):
pass
""";

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

Assert.Contains(references, reference =>
reference.SymbolName == "Base"
&& reference.ReferenceKind == "type_reference"
&& reference.ContainerName == "Derived");
Assert.Contains(references, reference =>
reference.SymbolName == "Mixin"
&& reference.ReferenceKind == "type_reference"
&& reference.ContainerName == "Derived");
Assert.Contains(references, reference =>
reference.SymbolName == "Meta"
&& reference.ReferenceKind == "type_reference"
&& reference.ContainerName == "Derived");
Assert.DoesNotContain(references, reference =>
reference.SymbolName == "metaclass"
&& reference.ReferenceKind == "type_reference");
}

[Fact]
public void Extract_PythonSuperInitSubclass_EmitsHookCallReference()
{
const string content = """
class Base:
def __init_subclass__(cls) -> None:
pass

class Child(Base):
def __init_subclass__(cls) -> None:
super().__init_subclass__()
""";

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

var hookCall = Assert.Single(references, reference =>
reference.SymbolName == "__init_subclass__"
&& reference.ReferenceKind == "call"
&& reference.ContainerName == "__init_subclass__"
&& reference.Line == 7);
Assert.Equal(17, hookCall.Column);
Assert.DoesNotContain(references, reference =>
reference.SymbolName == "super"
&& reference.ReferenceKind == "call");
}

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