diff --git a/changelog.d/unreleased/2058.fixed.md b/changelog.d/unreleased/2058.fixed.md new file mode 100644 index 0000000000..40a75fb4e6 --- /dev/null +++ b/changelog.d/unreleased/2058.fixed.md @@ -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 も記録します。 diff --git a/src/CodeIndex/Indexer/References/Languages/PythonReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/PythonReferenceExtractor.cs index a0ea58eac8..4057b21ae3 100644 --- a/src/CodeIndex/Indexer/References/Languages/PythonReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/PythonReferenceExtractor.cs @@ -55,7 +55,7 @@ internal static class PythonReferenceExtractor @"^\s*class\s+\w+\s*\(\s*(?(?:[_\p{L}]\w*\.)*[_\p{Lu}]\w*)\s*\)\s*:", RegexOptions.Compiled); private static readonly Regex MultipleClassBaseTypesRegex = new( - @"^\s*class\s+\w+\s*\((?[^=)]*,[^=)]*)\)\s*:", + @"^\s*class\s+\w+\s*\((?[^)]*,[^)]*)\)\s*:", RegexOptions.Compiled); private static readonly Regex ClassMetaclassTypeRegex = new( @"^\s*class\s+\w+\s*\([^)]*\bmetaclass\s*=\s*(?(?:[_\p{L}]\w*\.)*[_\p{Lu}]\w*)", @@ -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( @@ -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 references, diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs index 6e6acdcf73..e107307170 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs @@ -190,7 +190,7 @@ private static bool IsFunctionLikeSymbolKind(string kind) // Python contextual keywords / Python の文脈キーワード ["python"] = new HashSet(StringComparer.Ordinal) { - "raise", "yield", "from", + "raise", "yield", "from", "super", }, // Ruby contextual keywords / Ruby の文脈キーワード ["ruby"] = new HashSet(StringComparer.Ordinal) @@ -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); } diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index bd96c02ca1..4461c986f7 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -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() {