diff --git a/changelog.d/unreleased/2961.fixed.md b/changelog.d/unreleased/2961.fixed.md new file mode 100644 index 0000000000..74f24cc779 --- /dev/null +++ b/changelog.d/unreleased/2961.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 2961 +affected: + - src/CodeIndex/Indexer/References/Support/LanguageReferenceExtractionSupport.cs + - tests/CodeIndex.Tests/ReferenceExtractorTests.cs +--- + +## English + +- **C `va_arg` typedef references are now parsed from balanced arguments (#2961)** — `va_arg(select_args(primary, fallback), widget_t)` now records the requested typedef even when the `va_list` expression contains nested commas. + +## 日本語 + +- **C の `va_arg` typedef 参照を balanced argument から解析するようになりました (#2961)** — `va_arg(select_args(primary, fallback), widget_t)` のように `va_list` 式が入れ子のカンマを含む場合でも、要求された typedef を記録します。 diff --git a/src/CodeIndex/Indexer/References/Support/LanguageReferenceExtractionSupport.cs b/src/CodeIndex/Indexer/References/Support/LanguageReferenceExtractionSupport.cs index ce58e55643..1167ea1358 100644 --- a/src/CodeIndex/Indexer/References/Support/LanguageReferenceExtractionSupport.cs +++ b/src/CodeIndex/Indexer/References/Support/LanguageReferenceExtractionSupport.cs @@ -150,6 +150,11 @@ internal static class LanguageReferenceExtractionSupport private static readonly Regex CTaggedVaArgTypeRegex = new( @"\b(?:va_arg|__builtin_va_arg)\s*\(\s*[^,;{}]+,\s*(?(?:struct|enum|union)\s+[A-Za-z_]\w*)\s*(?:\*+\s*)?\)", RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly string[] CVaArgFunctionNames = + { + "va_arg", + "__builtin_va_arg", + }; private static readonly Regex CppTypeOperandOperatorRegex = new( @"\b(?:sizeof|alignof)\s*\(\s*(?(?:(?:const|volatile|typename|class|struct|enum)\s+)*(?:[A-Z_]\w*|[A-Za-z_]\w*\s*::\s*[A-Za-z_]\w*)(?:\s*<[^;{}]+>)?(?:\s*[*&])*)\s*\)", RegexOptions.Compiled | RegexOptions.CultureInvariant); @@ -1606,6 +1611,8 @@ private static void EmitCppTypeReferences( var group = match.Groups["type"]; ReferenceExtractor.AddTypeExpressionSegments(references, seen, fileId, group.Value, group.Index, context, lineNumber, resolveContainerForColumn(group.Index), language); } + + EmitCVaArgTypeOperandReferences(preparedLine, references, seen, fileId, context, lineNumber, resolveContainerForColumn, language); } foreach (Match match in CppTypeOperandOperatorRegex.Matches(preparedLine)) @@ -1773,6 +1780,160 @@ private static void EmitCppTypeReferences( } } + private static void EmitCVaArgTypeOperandReferences( + string line, + List references, + HashSet seen, + long fileId, + string context, + int lineNumber, + Func resolveContainerForColumn, + string language) + { + foreach (var functionName in CVaArgFunctionNames) + { + var searchStart = 0; + while (searchStart < line.Length) + { + var functionIndex = line.IndexOf(functionName, searchStart, StringComparison.Ordinal); + if (functionIndex < 0) + break; + + searchStart = functionIndex + functionName.Length; + if (!IsIdentifierAt(line, functionIndex, functionName)) + continue; + + var open = SkipWhitespace(line, functionIndex + functionName.Length); + if (open >= line.Length || line[open] != '(') + continue; + + var close = ReferenceExtractor.FindMatchingChar(line, open, '(', ')'); + if (close < 0) + continue; + + var argumentList = line.Substring(open + 1, close - open - 1); + var arguments = SplitTopLevelCArgumentSpans(argumentList); + if (arguments.Count < 2) + continue; + + var typeArgument = arguments[1]; + if (typeArgument.Length <= 0) + continue; + + var rawType = argumentList.Substring(typeArgument.Start, typeArgument.Length); + var expression = rawType.Trim(); + if (expression.Length == 0 || !LooksLikeCVaArgTypeOperand(expression)) + continue; + + var trimStart = rawType.IndexOf(expression, StringComparison.Ordinal); + var absoluteStart = open + 1 + typeArgument.Start + Math.Max(0, trimStart); + ReferenceExtractor.AddTypeExpressionSegments( + references, + seen, + fileId, + expression, + absoluteStart, + context, + lineNumber, + resolveContainerForColumn(absoluteStart), + language); + } + } + } + + private static List<(int Start, int Length)> SplitTopLevelCArgumentSpans(string text) + { + var spans = new List<(int Start, int Length)>(); + int parenDepth = 0; + int squareDepth = 0; + int braceDepth = 0; + int start = 0; + + for (int i = 0; i < text.Length; i++) + { + switch (text[i]) + { + case '(': + parenDepth++; + break; + case ')': + if (parenDepth > 0) + parenDepth--; + break; + case '[': + squareDepth++; + break; + case ']': + if (squareDepth > 0) + squareDepth--; + break; + case '{': + braceDepth++; + break; + case '}': + if (braceDepth > 0) + braceDepth--; + break; + case ',' when parenDepth == 0 && squareDepth == 0 && braceDepth == 0: + spans.Add((start, i - start)); + start = i + 1; + break; + } + } + + spans.Add((start, text.Length - start)); + return spans; + } + + private static bool LooksLikeCVaArgTypeOperand(string expression) + { + var cursor = SkipLeadingCTypeQualifiers(expression, 0); + if (cursor >= expression.Length) + return false; + + foreach (var keyword in new[] { "struct", "enum", "union" }) + { + if (StartsWithKeyword(expression, cursor, keyword)) + { + cursor = SkipWhitespace(expression, cursor + keyword.Length); + return cursor < expression.Length && IsIdentifierStart(expression[cursor]); + } + } + + if (!IsIdentifierStart(expression[cursor])) + return false; + + var nameStart = cursor; + cursor++; + while (cursor < expression.Length && IsSimpleIdentifierPart(expression[cursor])) + cursor++; + + return expression.AsSpan(nameStart, cursor - nameStart).EndsWith("_t", StringComparison.Ordinal); + } + + private static int SkipLeadingCTypeQualifiers(string expression, int cursor) + { + while (cursor < expression.Length) + { + cursor = SkipWhitespace(expression, cursor); + var next = cursor; + if (StartsWithKeyword(expression, cursor, "const")) + next += "const".Length; + else if (StartsWithKeyword(expression, cursor, "volatile")) + next += "volatile".Length; + else if (StartsWithKeyword(expression, cursor, "restrict")) + next += "restrict".Length; + else if (StartsWithKeyword(expression, cursor, "_Atomic")) + next += "_Atomic".Length; + else + return cursor; + + cursor = next; + } + + return cursor; + } + private static void EmitGoTypeReferences( string preparedLine, string originalLine, diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 260e3c7602..2910d6e4af 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -5114,6 +5114,36 @@ void configure(void) { Assert.Contains(references, r => r.SymbolName == "widget_t" && r.ReferenceKind == "type_reference"); } + [Fact] + public void Extract_CTypedefVaArgOperands_CapturesTypeWhenVaListExpressionContainsComma() + { + const string content = """ + void configure(void) { + consume(va_arg(select_args(primary, fallback), widget_t)); + } + """; + + var symbols = SymbolExtractor.Extract(1, "c", content); + var references = ReferenceExtractor.Extract(1, "c", content, symbols); + + Assert.Contains(references, r => r.SymbolName == "widget_t" && r.ReferenceKind == "type_reference"); + } + + [Fact] + public void Extract_CTypedefVaArgOperands_CapturesTypeWhenVaListExpressionContainsComparisonAndComma() + { + const string content = """ + void configure(void) { + consume(va_arg(select_args(primary < fallback, fallback), widget_t)); + } + """; + + var symbols = SymbolExtractor.Extract(1, "c", content); + var references = ReferenceExtractor.Extract(1, "c", content, symbols); + + Assert.Contains(references, r => r.SymbolName == "widget_t" && r.ReferenceKind == "type_reference"); + } + [Fact] public void Extract_CTaggedSizeofOperands_CapturesTagTypeReferences() {