From 3c28b78271624093615242dbfd12f56743563165 Mon Sep 17 00:00:00 2001 From: Dan Vu Date: Thu, 26 Mar 2026 00:44:49 +0100 Subject: [PATCH 01/10] Expanded UFCS functionality, to chain UFCS functions. --- README.md | 3 +- dsymbol/src/dsymbol/ufcs.d | 419 +++++++++++++++--- .../expected_completion_test.txt | 10 + .../file.d | 19 + .../run.sh | 5 + 5 files changed, 387 insertions(+), 69 deletions(-) create mode 100644 tests/tc_ufcs_function_chaining_completion/expected_completion_test.txt create mode 100644 tests/tc_ufcs_function_chaining_completion/file.d create mode 100755 tests/tc_ufcs_function_chaining_completion/run.sh diff --git a/README.md b/README.md index 1bc4397f..b12e6b8f 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,9 @@ the issue.) * *auto* declarations (Mostly) * *with* statements * Simple UFCS suggestions for concrete types and fundamental types. + * Dot chaining with other UFCS functions * Not working: - * UFCS completion for templates, literals, aliased types, UFCS function arguments, and '.' chaining with other UFCS functions. + * UFCS completion for templates, literals, aliased types, UFCS function arguments. * UFCS calltips * Autocompletion of declarations with template arguments (This will work to some extent, but it won't do things like replace T with int) * Determining the type of an enum member when no base type is specified, but the first member has an initializer diff --git a/dsymbol/src/dsymbol/ufcs.d b/dsymbol/src/dsymbol/ufcs.d index 618c5a8f..33f3a2a0 100644 --- a/dsymbol/src/dsymbol/ufcs.d +++ b/dsymbol/src/dsymbol/ufcs.d @@ -4,9 +4,11 @@ import dsymbol.symbol; import dsymbol.scope_; import dsymbol.builtin.names; import dsymbol.utils; -import dparse.lexer : tok, Token; +import dparse.lexer : tok, Token, isStringLiteral, isNumberLiteral; +import dparse.strings; import std.functional : unaryFun; import std.algorithm; +import std.algorithm.searching : countUntil; import std.array; import std.range; import std.string; @@ -14,6 +16,8 @@ import std.regex; import containers.hashset : HashSet; import std.experimental.logger; +alias SortedTokens = SortedRange!(const(Token)[], "a < b"); + enum CompletionContext { UnknownCompletion, @@ -21,48 +25,215 @@ enum CompletionContext ParenCompletion, } - struct TokenCursorResult { CompletionContext completionContext; istring functionName; - Token significantToken; + const(Token)[] expressionTokens; string partialIdentifier; } // https://dlang.org/spec/type.html#implicit-conversions enum string[string] INTEGER_PROMOTIONS = [ - "bool": "byte", - "byte": "int", - "ubyte": "int", - "short": "int", - "ushort": "int", - "char": "int", - "wchar": "int", - "dchar": "uint", - - // found in test case extra/tc_ufcs_all_kinds: - "int": "float", - "uint": "float", - "long": "float", - "ulong": "float", - - "float": "double", - "double": "real", - ]; + "bool": "byte", + "byte": "int", + "ubyte": "int", + "short": "int", + "ushort": "int", + "char": "int", + "wchar": "int", + "dchar": "uint", + + // found in test case extra/tc_ufcs_all_kinds: + "int": "float", + "uint": "float", + "long": "float", + "ulong": "float", + + "float": "double", + "double": "real", +]; enum MAX_NUMBER_OF_MATCHING_RUNS = 50; -private const(DSymbol)* deduceSymbolTypeByToken(Scope* completionScope, scope ref const(Token) significantToken, size_t cursorPosition) +private const(Token)* findUFCSBaseToken( const(Token)[] tokens) +{ + if (tokens.empty) + return null; + + int depth = 0; + + // Walk backwards to skip nested parentheses/brackets/braces + for (long i = tokens.length - 1; i >= 0; i--) + { + auto t = tokens[i].type; + + // Handle closing of nested scopes + if (t is tok!")" || t is tok!"]" || t is tok!"}") + { + depth++; + continue; + } + + // Handle opening of nested scopes + if (t is tok!"(" || t is tok!"[" || t is tok!"{") + { + depth--; + continue; + } + + if (depth != 0) + continue; + + // If we hit a literal or identifier, that's likely our base + if (t is tok!"identifier" || isStringLiteral(t) || t is tok!"intLiteral" || t is tok!"floatLiteral") + { + return &tokens[i]; + } + + // Stop at anything else that breaks the expression (operators, keywords, etc.) + return &tokens[i + 1]; + } + + // If we never returned inside the loop, the first token is the base + return &tokens[0]; +} + +private const(Token)* findExpressionBase(const(Token)[] tokens) { - //Literal type deduction - if (significantToken.type is tok!"stringLiteral"){ - return completionScope.getFirstSymbolByNameAndCursor(symbolNameToTypeName(STRING_LITERAL_SYMBOL_NAME), cursorPosition); + foreach (i, t; tokens) + { + // literals are always a base + if (isStringLiteral(t.type) || isNumberLiteral(t.type) || t.type is tok!"identifier") + return &tokens[i]; + } + return tokens.ptr; // fallback +} + +private const(DSymbol)* deduceExpressionType( + Scope* completionScope, + const(Token)[] exprTokens, + size_t cursorPosition) +{ + + if (exprTokens.empty) + { + return null; + } + + auto firstToken = findUFCSBaseToken(exprTokens); + if (isStringLiteral(firstToken.type)) + { + return completionScope.getFirstSymbolByNameAndCursor( + symbolNameToTypeName(STRING_LITERAL_SYMBOL_NAME), cursorPosition); } - const(DSymbol)* symbol = completionScope.getFirstSymbolByNameAndCursor(istring(significantToken.text), cursorPosition); + auto currentType = + deduceSymbolTypeByToken(completionScope, *firstToken, cursorPosition); - if (symbol is null) { + if (currentType is null) + return null; + + // 2. Walk through the expression left → right + for (size_t i = 1; i < exprTokens.length; i++) + { + auto t = exprTokens[i].type; + + // ---- Handle function call: foo() ---- + if (t is tok!"(") + { + // Skip to matching ')' + int depth = 1; + size_t j = i + 1; + + while (j < exprTokens.length && depth > 0) + { + if (exprTokens[j].type is tok!"(") + depth++; + else if (exprTokens[j].type is tok!")") + depth--; + + j++; + } + + // Function call → move to return type + if (currentType !is null && currentType.type !is null) + { + currentType = currentType.type; + } + + i = j - 1; + continue; + } + + // ---- Handle dot call + if (t is tok!"." && + i + 1 < exprTokens.length && + exprTokens[i + 1].type is tok!"identifier") + { + auto name = istring(exprTokens[i + 1].text); + + // Get UFCS candidates for current type + auto candidates = getUFCSSymbolsForDotCompletion( + currentType, + completionScope, + cursorPosition, + "" + ); + + const(DSymbol)* match = null; + + foreach (c; candidates) + { + if (c.name == name) + { + match = c; + break; + } + } + + // Invalid UFCS chain + if (match is null) { + return null; + } + + // Update current type (this is the chain step) + currentType = match.type; + + i++; // skip identifier + continue; + } + } + return currentType; +} + +void printTokenType(const(Token)* token) +{ + if (token is null) + { + error("HMMM token is null why?"); + return; + } + error("token type: ", token.type); + switch (token.type) + { + case tok!"": + + break; + + default: + break; + } +} + +private const(DSymbol)* deduceSymbolTypeByToken(Scope* completionScope, scope ref const(Token) significantToken, size_t cursorPosition) +{ + + const(DSymbol)* symbol = completionScope.getFirstSymbolByNameAndCursor( + istring(significantToken.text), cursorPosition); + + if (symbol is null) + { return null; } @@ -82,7 +253,6 @@ private const(DSymbol)* deduceSymbolTypeByToken(Scope* completionScope, scope re symbolType = symbolType.type; } - return symbolType; } @@ -96,36 +266,80 @@ private bool isInvalidForUFCSCompletion(const(DSymbol)* beforeDotSymbol) tok!"void")); } -private TokenCursorResult getCursorToken(const(Token)[] tokens, size_t cursorPosition) +const(Token)* findUFCSExpressionStart(SortedTokens tokens) +{ + int depth = 0; + + for (long i = tokens.length - 1; i >= 0; i--) + { + auto t = tokens[i].type; + + // Handle nesting + if (t is tok!")" || t is tok!"]" || t is tok!"}") + { + depth++; + continue; + } + + if (t is tok!"(" || t is tok!"[" || t is tok!"{") + { + depth--; + continue; + } + + if (depth != 0) + continue; + + // Allow chaining: f.papa().x + if (t is tok!"." || + t is tok!"identifier" || + t is tok!"stringLiteral") + { + continue; + } + + // Stop when hitting something that breaks expression + return &tokens[i + 1]; + } + + // Entire range is the expression + return &tokens[0]; +} + +private TokenCursorResult getCursorToken(Scope* completionScope, const(Token)[] tokens, size_t cursorPosition) { - auto sortedTokens = assumeSorted(tokens); - auto sortedBeforeTokens = sortedTokens.lowerBound(cursorPosition); + SortedTokens sortedTokens = assumeSorted(tokens); + SortedTokens sortedBeforeTokens = sortedTokens.lowerBound(cursorPosition); TokenCursorResult tokenCursorResult; - if (sortedBeforeTokens.empty) { + if (sortedBeforeTokens.empty) + { return tokenCursorResult; } - // move before identifier for + // Handle partially completed if (sortedBeforeTokens[$ - 1].type is tok!"identifier") { tokenCursorResult.partialIdentifier = sortedBeforeTokens[$ - 1].text; sortedBeforeTokens = sortedBeforeTokens[0 .. $ - 1]; } - if (sortedBeforeTokens.length >= 2 - && sortedBeforeTokens[$ - 1].type is tok!"." - && (sortedBeforeTokens[$ - 2].type is tok!"identifier" || sortedBeforeTokens[$ - 2].type is tok!"stringLiteral")) + // Handle dot completion + if (!sortedBeforeTokens.empty && + sortedBeforeTokens[$ - 1].type is tok!".") { - // Check if it's UFCS dot completion - auto expressionTokens = getExpression(sortedBeforeTokens); - if(expressionTokens[0] !is sortedBeforeTokens[$ - 2]){ - // If the expression is invalid as a dot token we return + const(Token)* exprStart = findUFCSExpressionStart(sortedBeforeTokens); + + if (exprStart is null) return tokenCursorResult; - } - tokenCursorResult.significantToken = sortedBeforeTokens[$ - 2]; + size_t start = exprStart - tokens.ptr; + size_t end = (&sortedBeforeTokens[$ - 1]) - tokens.ptr; + + auto exprTokens = tokens[start .. end]; + + tokenCursorResult.expressionTokens = exprTokens; tokenCursorResult.completionContext = CompletionContext.DotCompletion; return tokenCursorResult; } @@ -146,8 +360,8 @@ private TokenCursorResult getCursorToken(const(Token)[] tokens, size_t cursorPos && slicedAtParen[$ - 2].type is tok!"identifier" && slicedAtParen[$ - 1].type is tok!"(") { + tokenCursorResult.expressionTokens = slicedAtParen[0 .. $ - 3].array; tokenCursorResult.completionContext = CompletionContext.ParenCompletion; - tokenCursorResult.significantToken = slicedAtParen[$ - 4]; tokenCursorResult.functionName = istring(slicedAtParen[$ - 2].text); return tokenCursorResult; } @@ -207,9 +421,46 @@ private void getUFCSSymbols(T, Y)(scope ref T localAppender, scope ref Y globalA } } +void printToken(const(Token)* token) +{ + if (token is null) + { + error("HMMM token is null why?"); + return; + } + error("token text: ", token.text); + error("token type: ", token.type); +} + +void printDsymbol(const(DSymbol)* sym) +{ + if (sym is null) + { + error("HMMM is null why?"); + return; + } + error("name: ", sym.name); + error("kind: ", sym.kind); + error("protection: ", sym.protection); + error("qualifier : ", sym.qualifier); + error("type: ", sym.type); +} + +void printTokenCursorResult(TokenCursorResult result) +{ + error("completionContext: ", result.completionContext); + error("functionName: ", result.functionName); + error("partialIdentifier: ", result.partialIdentifier); + error("expressionTokens: "); + foreach (t; result.expressionTokens) + { + printToken(&t); + } +} + DSymbol*[] getUFCSSymbolsForCursor(Scope* completionScope, scope ref const(Token)[] tokens, size_t cursorPosition) { - TokenCursorResult tokenCursorResult = getCursorToken(tokens, cursorPosition); + TokenCursorResult tokenCursorResult = getCursorToken(completionScope, tokens, cursorPosition); if (tokenCursorResult.completionContext is CompletionContext.UnknownCompletion) { @@ -217,7 +468,7 @@ DSymbol*[] getUFCSSymbolsForCursor(Scope* completionScope, scope ref const(Token return []; } - const(DSymbol)* deducedSymbolType = deduceSymbolTypeByToken(completionScope, tokenCursorResult.significantToken, cursorPosition); + const(DSymbol)* deducedSymbolType = deduceExpressionType(completionScope, tokenCursorResult.expressionTokens, cursorPosition); if (deducedSymbolType is null) { @@ -236,7 +487,8 @@ DSymbol*[] getUFCSSymbolsForCursor(Scope* completionScope, scope ref const(Token } else { - return getUFCSSymbolsForDotCompletion(deducedSymbolType, completionScope, cursorPosition, tokenCursorResult.partialIdentifier); + return getUFCSSymbolsForDotCompletion(deducedSymbolType, completionScope, cursorPosition, tokenCursorResult + .partialIdentifier); } } @@ -272,7 +524,8 @@ private DSymbol*[] getUFCSSymbolsForParenCompletion(const(DSymbol)* symbolType, } -private bool willImplicitBeUpcasted(scope ref const(DSymbol) incomingSymbolType, scope ref const(DSymbol) significantSymbolType) +private bool willImplicitBeUpcasted(scope ref const(DSymbol) incomingSymbolType, scope ref const( + DSymbol) significantSymbolType) { string fromTypeName = significantSymbolType.name.data; string toTypeName = incomingSymbolType.name.data; @@ -311,16 +564,24 @@ private int getIntegerTypeSize(string type) { switch (type) { - // ordered by subjective frequency of use, since the compiler may use that - // for optimization. - case "int", "uint": return 4; - case "long", "ulong": return 8; - case "byte", "ubyte": return 1; - case "short", "ushort": return 2; - case "dchar": return 4; - case "wchar": return 2; - case "char": return 1; - default: return 0; + // ordered by subjective frequency of use, since the compiler may use that + // for optimization. + case "int", "uint": + return 4; + case "long", "ulong": + return 8; + case "byte", "ubyte": + return 1; + case "short", "ushort": + return 2; + case "dchar": + return 4; + case "wchar": + return 2; + case "char": + return 1; + default: + return 0; } } @@ -342,14 +603,16 @@ bool isNonConstrainedTemplate(scope ref const(DSymbol) symbolType) return symbolType.kind is CompletionKind.typeTmpParam; } -private bool matchesWithTypeOfPointer(scope ref const(DSymbol) incomingSymbolType, scope ref const(DSymbol) significantSymbolType) +private bool matchesWithTypeOfPointer(scope ref const(DSymbol) incomingSymbolType, scope ref const( + DSymbol) significantSymbolType) { return incomingSymbolType.qualifier == SymbolQualifier.pointer && significantSymbolType.qualifier == SymbolQualifier.pointer && incomingSymbolType.type is significantSymbolType.type; } -private bool matchesWithTypeOfArray(scope ref const(DSymbol) incomingSymbolType, scope ref const(DSymbol) cursorSymbolType) +private bool matchesWithTypeOfArray(scope ref const(DSymbol) incomingSymbolType, scope ref const( + DSymbol) cursorSymbolType) { return incomingSymbolType.qualifier == SymbolQualifier.array && cursorSymbolType.qualifier == SymbolQualifier.array @@ -357,21 +620,38 @@ private bool matchesWithTypeOfArray(scope ref const(DSymbol) incomingSymbolType, } -private bool typeMatchesWith(scope ref const(DSymbol) incomingSymbolType, scope ref const(DSymbol) significantSymbolType) { +private bool matchStringLikeTypes(scope ref const(DSymbol) incomingSymbolType, scope ref const(DSymbol) significantSymbolType) +{ + if ((incomingSymbolType.name.data == "string" || incomingSymbolType.name.data == "wstring" + || incomingSymbolType.name.data == "dstring") && (significantSymbolType.name.data == "string" || significantSymbolType.name.data == "wstring" + || significantSymbolType.name.data == "dstring")) + { + return true; + } + return false; +} + +private bool typeMatchesWith(scope ref const(DSymbol) incomingSymbolType, scope ref const(DSymbol) significantSymbolType) +{ return incomingSymbolType is significantSymbolType - || isNonConstrainedTemplate(incomingSymbolType) + || isNonConstrainedTemplate( + incomingSymbolType) || matchesWithTypeOfArray(incomingSymbolType, significantSymbolType) - || matchesWithTypeOfPointer(incomingSymbolType, significantSymbolType); + || matchesWithTypeOfPointer(incomingSymbolType, significantSymbolType) + || matchStringLikeTypes(incomingSymbolType, significantSymbolType); + } -private bool matchSymbolType(const(DSymbol)* firstParameter, const(DSymbol)* significantSymbolType) { +private bool matchSymbolType(const(DSymbol)* firstParameter, const(DSymbol)* significantSymbolType) +{ auto currentSignificantSymbolType = significantSymbolType; uint numberOfRetries = 0; do { - if (typeMatchesWith(*firstParameter.type, *currentSignificantSymbolType)) { + if (typeMatchesWith(*firstParameter.type, *currentSignificantSymbolType)) + { return true; } @@ -379,7 +659,9 @@ private bool matchSymbolType(const(DSymbol)* firstParameter, const(DSymbol)* sig && willImplicitBeUpcasted(*firstParameter.type, *currentSignificantSymbolType)) return true; - if (currentSignificantSymbolType.aliasThisSymbols.empty || currentSignificantSymbolType is currentSignificantSymbolType.aliasThisSymbols.front){ + if (currentSignificantSymbolType.aliasThisSymbols.empty || currentSignificantSymbolType is currentSignificantSymbolType + .aliasThisSymbols.front) + { return false; } @@ -388,7 +670,7 @@ private bool matchSymbolType(const(DSymbol)* firstParameter, const(DSymbol)* sig // when multiple alias this are supported, we can rethink another solution currentSignificantSymbolType = currentSignificantSymbolType.aliasThisSymbols.front.type; } - while(numberOfRetries <= MAX_NUMBER_OF_MATCHING_RUNS); + while (numberOfRetries <= MAX_NUMBER_OF_MATCHING_RUNS); return false; } @@ -406,11 +688,12 @@ bool isCallableWithArg(const(DSymbol)* incomingSymbol, const(DSymbol)* beforeDot if (incomingSymbol is null || beforeDotType is null || isGlobalScope && incomingSymbol.protection is tok!"private") // don't show private functions if we are in global scope - { + { return false; } - if (incomingSymbol.kind is CompletionKind.functionName && !incomingSymbol.functionParameters.empty && incomingSymbol.functionParameters.front.type) + if (incomingSymbol.kind is CompletionKind.functionName && !incomingSymbol.functionParameters.empty && incomingSymbol + .functionParameters.front.type) { return matchSymbolType(incomingSymbol.functionParameters.front, beforeDotType); } diff --git a/tests/tc_ufcs_function_chaining_completion/expected_completion_test.txt b/tests/tc_ufcs_function_chaining_completion/expected_completion_test.txt new file mode 100644 index 00000000..1a215600 --- /dev/null +++ b/tests/tc_ufcs_function_chaining_completion/expected_completion_test.txt @@ -0,0 +1,10 @@ +identifiers +alignof k +bar F +baz F +foo F +init k +mangleof k +sizeof k +stringof k +tupleof k diff --git a/tests/tc_ufcs_function_chaining_completion/file.d b/tests/tc_ufcs_function_chaining_completion/file.d new file mode 100644 index 00000000..626291ce --- /dev/null +++ b/tests/tc_ufcs_function_chaining_completion/file.d @@ -0,0 +1,19 @@ +struct Foo {} +Foo foo(Foo f){ + return f; +} + +Foo bar(Foo f){ + return f; +} + +Foo baz(Foo f) { + return f; +} + + +void main() +{ + Foo f; + Foo foo = baz(f.foo().bar()). +} \ No newline at end of file diff --git a/tests/tc_ufcs_function_chaining_completion/run.sh b/tests/tc_ufcs_function_chaining_completion/run.sh new file mode 100755 index 00000000..acc380b5 --- /dev/null +++ b/tests/tc_ufcs_function_chaining_completion/run.sh @@ -0,0 +1,5 @@ +set -e +set -u + +../../bin/dcd-client $1 -c158 file.d > actual_completion_test.txt +diff actual_completion_test.txt expected_completion_test.txt \ No newline at end of file From 57c52996c3e4d6f69e7f17f7b48e4cb50a97eefc Mon Sep 17 00:00:00 2001 From: Dan Vu Date: Thu, 26 Mar 2026 00:52:46 +0100 Subject: [PATCH 02/10] removed some prints. --- dsymbol/src/dsymbol/ufcs.d | 39 -------------------------------------- 1 file changed, 39 deletions(-) diff --git a/dsymbol/src/dsymbol/ufcs.d b/dsymbol/src/dsymbol/ufcs.d index 33f3a2a0..91f9747e 100644 --- a/dsymbol/src/dsymbol/ufcs.d +++ b/dsymbol/src/dsymbol/ufcs.d @@ -211,10 +211,8 @@ void printTokenType(const(Token)* token) { if (token is null) { - error("HMMM token is null why?"); return; } - error("token type: ", token.type); switch (token.type) { case tok!"": @@ -421,43 +419,6 @@ private void getUFCSSymbols(T, Y)(scope ref T localAppender, scope ref Y globalA } } -void printToken(const(Token)* token) -{ - if (token is null) - { - error("HMMM token is null why?"); - return; - } - error("token text: ", token.text); - error("token type: ", token.type); -} - -void printDsymbol(const(DSymbol)* sym) -{ - if (sym is null) - { - error("HMMM is null why?"); - return; - } - error("name: ", sym.name); - error("kind: ", sym.kind); - error("protection: ", sym.protection); - error("qualifier : ", sym.qualifier); - error("type: ", sym.type); -} - -void printTokenCursorResult(TokenCursorResult result) -{ - error("completionContext: ", result.completionContext); - error("functionName: ", result.functionName); - error("partialIdentifier: ", result.partialIdentifier); - error("expressionTokens: "); - foreach (t; result.expressionTokens) - { - printToken(&t); - } -} - DSymbol*[] getUFCSSymbolsForCursor(Scope* completionScope, scope ref const(Token)[] tokens, size_t cursorPosition) { TokenCursorResult tokenCursorResult = getCursorToken(completionScope, tokens, cursorPosition); From a3955b08453fe227765f9589e0075cde4fc136cd Mon Sep 17 00:00:00 2001 From: Dan Vu Date: Fri, 27 Mar 2026 13:00:37 +0100 Subject: [PATCH 03/10] Updated tests added return info update --- dsymbol/src/dsymbol/conversion/first.d | 13 ++++ dsymbol/src/dsymbol/symbol.d | 5 +- dsymbol/src/dsymbol/ufcs.d | 60 ++++++++++--------- .../expected_completion_test.txt | 3 + .../expected_completion_test2.txt | 7 +++ .../expected_completion_test3.txt | 7 +++ .../expected_completion_test4.txt | 7 +++ .../expected_completion_test5.txt | 13 ++++ .../file.d | 34 ++++++++++- .../run.sh | 16 ++++- 10 files changed, 132 insertions(+), 33 deletions(-) create mode 100644 tests/tc_ufcs_function_chaining_completion/expected_completion_test2.txt create mode 100644 tests/tc_ufcs_function_chaining_completion/expected_completion_test3.txt create mode 100644 tests/tc_ufcs_function_chaining_completion/expected_completion_test4.txt create mode 100644 tests/tc_ufcs_function_chaining_completion/expected_completion_test5.txt diff --git a/dsymbol/src/dsymbol/conversion/first.d b/dsymbol/src/dsymbol/conversion/first.d index 6cad7ff8..6ed49e12 100644 --- a/dsymbol/src/dsymbol/conversion/first.d +++ b/dsymbol/src/dsymbol/conversion/first.d @@ -132,6 +132,14 @@ final class FirstPass : ASTVisitor currentSymbol.acSymbol.protection = protection.current; currentSymbol.acSymbol.doc = makeDocumentation(dec.comment); currentSymbol.acSymbol.qualifier = SymbolQualifier.func; + foreach (sc; dec.storageClasses) + { + if (sc.token.type == tok!"ref") + { + currentSymbol.acSymbol.returnIsRef = true; + break; + } + } istring lastComment = this.lastComment; this.lastComment = istring.init; @@ -172,6 +180,11 @@ final class FirstPass : ASTVisitor block.startLocation, null); scope(exit) popSymbol(); + if (exp.returnRefType == ReturnRefType.ref_) + { + currentSymbol.acSymbol.returnIsRef = true; + } + pushScope(block.startLocation, block.endLocation); scope (exit) popScope(); processParameters(currentSymbol, exp.returnType, diff --git a/dsymbol/src/dsymbol/symbol.d b/dsymbol/src/dsymbol/symbol.d index 6e2f79ec..cf81a989 100644 --- a/dsymbol/src/dsymbol/symbol.d +++ b/dsymbol/src/dsymbol/symbol.d @@ -443,7 +443,8 @@ struct DSymbol bool, "_flag8", 1, bool, "_flag9", 1, bool, "_flag10", 1, - uint, "", 3, + bool, "_flag11", 1, + uint, "", 2, )); // dfmt on @@ -463,6 +464,8 @@ struct DSymbol alias parameterIsOut = _flag9; /// Only valid for parameters: the parameter has storage class `in` alias parameterIsIn = _flag10; + /// If the return type is a ref + alias returnIsRef = _flag11; deprecated bool isPointer() { diff --git a/dsymbol/src/dsymbol/ufcs.d b/dsymbol/src/dsymbol/ufcs.d index 91f9747e..159886ec 100644 --- a/dsymbol/src/dsymbol/ufcs.d +++ b/dsymbol/src/dsymbol/ufcs.d @@ -15,9 +15,16 @@ import std.string; import std.regex; import containers.hashset : HashSet; import std.experimental.logger; - alias SortedTokens = SortedRange!(const(Token)[], "a < b"); +struct ExpressionInfo +{ + const(DSymbol)* type; + bool assumingLvalue; // We only assume else we need to do life time analysis. + bool isFromFunction; + string name; +} + enum CompletionContext { UnknownCompletion, @@ -128,8 +135,8 @@ private const(DSymbol)* deduceExpressionType( symbolNameToTypeName(STRING_LITERAL_SYMBOL_NAME), cursorPosition); } - auto currentType = - deduceSymbolTypeByToken(completionScope, *firstToken, cursorPosition); + auto currentType = deduceSymbolTypeByToken(completionScope, *firstToken, cursorPosition); + if (currentType is null) return null; @@ -174,8 +181,18 @@ private const(DSymbol)* deduceExpressionType( auto name = istring(exprTokens[i + 1].text); // Get UFCS candidates for current type + ExpressionInfo beforeDotType; + beforeDotType.type = currentType; + beforeDotType.assumingLvalue = false; + // check if there it's from a function + auto functionSymbol = completionScope.getFirstSymbolByNameAndCursor(istring(firstToken.text), cursorPosition); + if (functionSymbol) { + beforeDotType.isFromFunction = functionSymbol.qualifier == SymbolQualifier.func; + beforeDotType.name = functionSymbol.name; + } + auto candidates = getUFCSSymbolsForDotCompletion( - currentType, + beforeDotType, completionScope, cursorPosition, "" @@ -207,23 +224,6 @@ private const(DSymbol)* deduceExpressionType( return currentType; } -void printTokenType(const(Token)* token) -{ - if (token is null) - { - return; - } - switch (token.type) - { - case tok!"": - - break; - - default: - break; - } -} - private const(DSymbol)* deduceSymbolTypeByToken(Scope* completionScope, scope ref const(Token) significantToken, size_t cursorPosition) { @@ -429,14 +429,15 @@ DSymbol*[] getUFCSSymbolsForCursor(Scope* completionScope, scope ref const(Token return []; } - const(DSymbol)* deducedSymbolType = deduceExpressionType(completionScope, tokenCursorResult.expressionTokens, cursorPosition); + ExpressionInfo deducedSymbolType; + deducedSymbolType.type = deduceExpressionType(completionScope, tokenCursorResult.expressionTokens, cursorPosition); - if (deducedSymbolType is null) + if (deducedSymbolType.type is null) { return []; } - if (deducedSymbolType.isInvalidForUFCSCompletion) + if (deducedSymbolType.type.isInvalidForUFCSCompletion) { trace("CursorSymbolType isn't valid for UFCS completion"); return []; @@ -454,7 +455,7 @@ DSymbol*[] getUFCSSymbolsForCursor(Scope* completionScope, scope ref const(Token } -private DSymbol*[] getUFCSSymbolsForDotCompletion(const(DSymbol)* symbolType, Scope* completionScope, size_t cursorPosition, string partial) +private DSymbol*[] getUFCSSymbolsForDotCompletion(ExpressionInfo symbolType, Scope* completionScope, size_t cursorPosition, string partial) { // local appender FilteredAppender!((DSymbol* a) => @@ -472,7 +473,7 @@ private DSymbol*[] getUFCSSymbolsForDotCompletion(const(DSymbol)* symbolType, Sc return localAppender.data ~ globalAppender.data; } -private DSymbol*[] getUFCSSymbolsForParenCompletion(const(DSymbol)* symbolType, Scope* completionScope, istring searchWord, size_t cursorPosition) +private DSymbol*[] getUFCSSymbolsForParenCompletion(ExpressionInfo symbolType, Scope* completionScope, istring searchWord, size_t cursorPosition) { // local appender FilteredAppender!(a => a.isCallableWithArg(symbolType) && a.name.among(searchWord), DSymbol*[]) localAppender; @@ -644,10 +645,10 @@ private bool matchSymbolType(const(DSymbol)* firstParameter, const(DSymbol)* sig * `true` if `incomingSymbols`' first parameter matches `beforeDotType` * `false` otherwise */ -bool isCallableWithArg(const(DSymbol)* incomingSymbol, const(DSymbol)* beforeDotType, bool isGlobalScope = false) +bool isCallableWithArg(const(DSymbol)* incomingSymbol, ExpressionInfo beforeDotType, bool isGlobalScope = false) { if (incomingSymbol is null - || beforeDotType is null + || beforeDotType.type is null || isGlobalScope && incomingSymbol.protection is tok!"private") // don't show private functions if we are in global scope { return false; @@ -656,7 +657,8 @@ bool isCallableWithArg(const(DSymbol)* incomingSymbol, const(DSymbol)* beforeDot if (incomingSymbol.kind is CompletionKind.functionName && !incomingSymbol.functionParameters.empty && incomingSymbol .functionParameters.front.type) { - return matchSymbolType(incomingSymbol.functionParameters.front, beforeDotType); + auto firstParam = incomingSymbol.functionParameters.front; + return matchSymbolType(firstParam, beforeDotType.type); } return false; } diff --git a/tests/tc_ufcs_function_chaining_completion/expected_completion_test.txt b/tests/tc_ufcs_function_chaining_completion/expected_completion_test.txt index 1a215600..9271886d 100644 --- a/tests/tc_ufcs_function_chaining_completion/expected_completion_test.txt +++ b/tests/tc_ufcs_function_chaining_completion/expected_completion_test.txt @@ -5,6 +5,9 @@ baz F foo F init k mangleof k +qux F +refFoo F +refFoo2 F sizeof k stringof k tupleof k diff --git a/tests/tc_ufcs_function_chaining_completion/expected_completion_test2.txt b/tests/tc_ufcs_function_chaining_completion/expected_completion_test2.txt new file mode 100644 index 00000000..e38cf1dd --- /dev/null +++ b/tests/tc_ufcs_function_chaining_completion/expected_completion_test2.txt @@ -0,0 +1,7 @@ +identifiers +bar F +baz F +foo F +qux F +refFoo F +refFoo2 F diff --git a/tests/tc_ufcs_function_chaining_completion/expected_completion_test3.txt b/tests/tc_ufcs_function_chaining_completion/expected_completion_test3.txt new file mode 100644 index 00000000..e38cf1dd --- /dev/null +++ b/tests/tc_ufcs_function_chaining_completion/expected_completion_test3.txt @@ -0,0 +1,7 @@ +identifiers +bar F +baz F +foo F +qux F +refFoo F +refFoo2 F diff --git a/tests/tc_ufcs_function_chaining_completion/expected_completion_test4.txt b/tests/tc_ufcs_function_chaining_completion/expected_completion_test4.txt new file mode 100644 index 00000000..e38cf1dd --- /dev/null +++ b/tests/tc_ufcs_function_chaining_completion/expected_completion_test4.txt @@ -0,0 +1,7 @@ +identifiers +bar F +baz F +foo F +qux F +refFoo F +refFoo2 F diff --git a/tests/tc_ufcs_function_chaining_completion/expected_completion_test5.txt b/tests/tc_ufcs_function_chaining_completion/expected_completion_test5.txt new file mode 100644 index 00000000..9271886d --- /dev/null +++ b/tests/tc_ufcs_function_chaining_completion/expected_completion_test5.txt @@ -0,0 +1,13 @@ +identifiers +alignof k +bar F +baz F +foo F +init k +mangleof k +qux F +refFoo F +refFoo2 F +sizeof k +stringof k +tupleof k diff --git a/tests/tc_ufcs_function_chaining_completion/file.d b/tests/tc_ufcs_function_chaining_completion/file.d index 626291ce..095bcd31 100644 --- a/tests/tc_ufcs_function_chaining_completion/file.d +++ b/tests/tc_ufcs_function_chaining_completion/file.d @@ -11,9 +11,41 @@ Foo baz(Foo f) { return f; } +Foo qux(Foo f) { + return f; +} + +ref refFoo(ref Foo f) { + return f; +} + +ref refFoo2(ref Foo f) { + return f; +} void main() { Foo f; Foo foo = baz(f.foo().bar()). -} \ No newline at end of file +} + +void another() { + Foo f; + Foo foo = f.foo().bar().baz(). +} + +void yetAnother() { + Foo f; + Foo foo = f.foo.bar().baz.qux. +} + +void justAnother() { + Foo f; + Foo foo = f.foo().baz().qux() + . +} + +void refTest() { + Foo f; + f. +} diff --git a/tests/tc_ufcs_function_chaining_completion/run.sh b/tests/tc_ufcs_function_chaining_completion/run.sh index acc380b5..89747ad5 100755 --- a/tests/tc_ufcs_function_chaining_completion/run.sh +++ b/tests/tc_ufcs_function_chaining_completion/run.sh @@ -1,5 +1,17 @@ set -e set -u -../../bin/dcd-client $1 -c158 file.d > actual_completion_test.txt -diff actual_completion_test.txt expected_completion_test.txt \ No newline at end of file +../../bin/dcd-client $1 -c265 file.d > actual_completion_test.txt +diff actual_completion_test.txt expected_completion_test.txt --strip-trailing-cr + +../../bin/dcd-client $1 -c325 file.d > actual_completion_test2.txt +diff actual_completion_test2.txt expected_completion_test2.txt --strip-trailing-cr + +../../bin/dcd-client $1 -c388 file.d > actual_completion_test3.txt +diff actual_completion_test3.txt expected_completion_test3.txt --strip-trailing-cr + +../../bin/dcd-client $1 -c454 file.d > actual_completion_test4.txt +diff actual_completion_test4.txt expected_completion_test4.txt --strip-trailing-cr + +../../bin/dcd-client $1 -c486 file.d > actual_completion_test5.txt +diff actual_completion_test5.txt expected_completion_test5.txt --strip-trailing-cr \ No newline at end of file From 5f599be038e128a8e6a7ef2fc16745da50b45e66 Mon Sep 17 00:00:00 2001 From: Dan Vu Date: Sat, 28 Mar 2026 22:38:37 +0100 Subject: [PATCH 04/10] Fixing windows error --- dsymbol/src/dsymbol/ufcs.d | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dsymbol/src/dsymbol/ufcs.d b/dsymbol/src/dsymbol/ufcs.d index 159886ec..495285ff 100644 --- a/dsymbol/src/dsymbol/ufcs.d +++ b/dsymbol/src/dsymbol/ufcs.d @@ -71,7 +71,7 @@ private const(Token)* findUFCSBaseToken( const(Token)[] tokens) int depth = 0; // Walk backwards to skip nested parentheses/brackets/braces - for (long i = tokens.length - 1; i >= 0; i--) + for (size_t i = tokens.length; i-- > 0;) { auto t = tokens[i].type; @@ -268,7 +268,7 @@ const(Token)* findUFCSExpressionStart(SortedTokens tokens) { int depth = 0; - for (long i = tokens.length - 1; i >= 0; i--) + for (size_t i = tokens.length; i-- > 0;) { auto t = tokens[i].type; From aaa4949cd2c530f1e4010fae14e0eb081d87a9bf Mon Sep 17 00:00:00 2001 From: Dan Vu Date: Sun, 29 Mar 2026 09:11:11 +0200 Subject: [PATCH 05/10] added calltip ufcs function chaining. --- dsymbol/src/dsymbol/ufcs.d | 5 +++-- .../expected.txt | 2 ++ .../file.d | 12 ++++++++++++ .../run.sh | 5 +++++ 4 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 tests/tc_ufcs_function_chaining_calltip_completion/expected.txt create mode 100644 tests/tc_ufcs_function_chaining_calltip_completion/file.d create mode 100755 tests/tc_ufcs_function_chaining_calltip_completion/run.sh diff --git a/dsymbol/src/dsymbol/ufcs.d b/dsymbol/src/dsymbol/ufcs.d index 495285ff..09efa37a 100644 --- a/dsymbol/src/dsymbol/ufcs.d +++ b/dsymbol/src/dsymbol/ufcs.d @@ -352,8 +352,9 @@ private TokenCursorResult getCursorToken(Scope* completionScope, const(Token)[] } auto slicedAtParen = sortedBeforeTokens[0 .. index]; - if (slicedAtParen.length >= 4 - && slicedAtParen[$ - 4].type is tok!"identifier" + + // Also allowing ) for ufcs function chaining + if (slicedAtParen.length >= 3 && slicedAtParen[$ - 3].type is tok!"." && slicedAtParen[$ - 2].type is tok!"identifier" && slicedAtParen[$ - 1].type is tok!"(") diff --git a/tests/tc_ufcs_function_chaining_calltip_completion/expected.txt b/tests/tc_ufcs_function_chaining_calltip_completion/expected.txt new file mode 100644 index 00000000..661186b9 --- /dev/null +++ b/tests/tc_ufcs_function_chaining_calltip_completion/expected.txt @@ -0,0 +1,2 @@ +calltips +int subtract(int x, int y) diff --git a/tests/tc_ufcs_function_chaining_calltip_completion/file.d b/tests/tc_ufcs_function_chaining_calltip_completion/file.d new file mode 100644 index 00000000..921da512 --- /dev/null +++ b/tests/tc_ufcs_function_chaining_calltip_completion/file.d @@ -0,0 +1,12 @@ +int add(int x, int y) { + return x + y; +} + +int subtract(int x, int y) { + return x - y; +} + +void main(){ + int x = 5; + x.add(5).subtract( +} diff --git a/tests/tc_ufcs_function_chaining_calltip_completion/run.sh b/tests/tc_ufcs_function_chaining_calltip_completion/run.sh new file mode 100755 index 00000000..7525e0dc --- /dev/null +++ b/tests/tc_ufcs_function_chaining_calltip_completion/run.sh @@ -0,0 +1,5 @@ +set -e +set -u + +../../bin/dcd-client $1 -c133 file.d > actual.txt +diff actual.txt expected.txt --strip-trailing-cr From 7f7392c47438eceeb1141f38e4b9b5cbb3fecbfe Mon Sep 17 00:00:00 2001 From: Dan Vu Date: Tue, 31 Mar 2026 08:49:07 +0200 Subject: [PATCH 06/10] added more advance test case for ufcs function chaining --- dsymbol/src/dsymbol/ufcs.d | 58 ++++++++++++++----- .../expected_completion_test6.txt | 2 + .../file.d | 25 ++++++++ .../run.sh | 5 +- 4 files changed, 73 insertions(+), 17 deletions(-) create mode 100644 tests/tc_ufcs_function_chaining_completion/expected_completion_test6.txt diff --git a/dsymbol/src/dsymbol/ufcs.d b/dsymbol/src/dsymbol/ufcs.d index 09efa37a..875dd139 100644 --- a/dsymbol/src/dsymbol/ufcs.d +++ b/dsymbol/src/dsymbol/ufcs.d @@ -117,6 +117,43 @@ private const(Token)* findExpressionBase(const(Token)[] tokens) return tokens.ptr; // fallback } +/// Resolves a symbol in a UFCS chain during type deduction. +/// This is intentionally permissive (name-based lookup) +/// Used only for type deduction, completion filtering will be done in a later step +private const(DSymbol)* resolveUFCSChainSymbol( + Scope* completionScope, + ExpressionInfo beforeDotType, + istring name, + size_t cursorPosition) +{ + Appender!(DSymbol*[]) local; + Appender!(DSymbol*[]) global; + + getUFCSSymbols(local, global, completionScope, cursorPosition); + + auto allSymbols = local.data ~ global.data; + + const(DSymbol)* fallback = null; + + foreach (sym; allSymbols) + { + if (sym.name != name) + continue; + + // Prefer a symbol that actually matches the type + if (sym.isCallableWithArg(beforeDotType)) + { + return sym; + } + + // Otherwise remember a fallback (loose match) + if (fallback is null) + fallback = sym; + } + + return fallback; +} + private const(DSymbol)* deduceExpressionType( Scope* completionScope, const(Token)[] exprTokens, @@ -191,26 +228,15 @@ private const(DSymbol)* deduceExpressionType( beforeDotType.name = functionSymbol.name; } - auto candidates = getUFCSSymbolsForDotCompletion( - beforeDotType, + auto match = resolveUFCSChainSymbol( completionScope, - cursorPosition, - "" + beforeDotType, + name, + cursorPosition ); - const(DSymbol)* match = null; - - foreach (c; candidates) + if (match is null) { - if (c.name == name) - { - match = c; - break; - } - } - - // Invalid UFCS chain - if (match is null) { return null; } diff --git a/tests/tc_ufcs_function_chaining_completion/expected_completion_test6.txt b/tests/tc_ufcs_function_chaining_completion/expected_completion_test6.txt new file mode 100644 index 00000000..d84069cc --- /dev/null +++ b/tests/tc_ufcs_function_chaining_completion/expected_completion_test6.txt @@ -0,0 +1,2 @@ +identifiers +papaOnly F diff --git a/tests/tc_ufcs_function_chaining_completion/file.d b/tests/tc_ufcs_function_chaining_completion/file.d index 095bcd31..9902f387 100644 --- a/tests/tc_ufcs_function_chaining_completion/file.d +++ b/tests/tc_ufcs_function_chaining_completion/file.d @@ -49,3 +49,28 @@ void refTest() { Foo f; f. } + +Mama mamaFoo(Mama m) { + return m; +} + +Mama mamaBar(Mama m, int y) { + return m; +} + +Papa mamaToPapa(Mama m) { + return Papa.init; +} + +Papa papaOnly(Papa p) { + return p; +} + +struct Mama { +} +struct Papa {} + +void moreTesting() { + Mama m; + auto ms = m.mamaFoo.mamaToPapa. +} \ No newline at end of file diff --git a/tests/tc_ufcs_function_chaining_completion/run.sh b/tests/tc_ufcs_function_chaining_completion/run.sh index 89747ad5..09714935 100755 --- a/tests/tc_ufcs_function_chaining_completion/run.sh +++ b/tests/tc_ufcs_function_chaining_completion/run.sh @@ -14,4 +14,7 @@ diff actual_completion_test3.txt expected_completion_test3.txt --strip-trailing- diff actual_completion_test4.txt expected_completion_test4.txt --strip-trailing-cr ../../bin/dcd-client $1 -c486 file.d > actual_completion_test5.txt -diff actual_completion_test5.txt expected_completion_test5.txt --strip-trailing-cr \ No newline at end of file +diff actual_completion_test5.txt expected_completion_test5.txt --strip-trailing-cr + +../../bin/dcd-client $1 -c751 file.d > actual_completion_test6.txt +diff actual_completion_test6.txt expected_completion_test6.txt --strip-trailing-cr \ No newline at end of file From 385843d424633e8f545fa211c44333a86c1babb1 Mon Sep 17 00:00:00 2001 From: Dan Vu Date: Mon, 6 Apr 2026 14:46:37 +0200 Subject: [PATCH 07/10] Added function overload handling for UFCS --- dsymbol/src/dsymbol/ufcs.d | 253 ++++++++++++++---- .../expected_completion_test.txt | 5 + .../expected_completion_test2.txt | 4 + .../file.d | 29 ++ .../run.sh | 8 + 5 files changed, 252 insertions(+), 47 deletions(-) create mode 100644 tests/tc_ufcs_overloaded_function_chaining_completion/expected_completion_test.txt create mode 100644 tests/tc_ufcs_overloaded_function_chaining_completion/expected_completion_test2.txt create mode 100644 tests/tc_ufcs_overloaded_function_chaining_completion/file.d create mode 100755 tests/tc_ufcs_overloaded_function_chaining_completion/run.sh diff --git a/dsymbol/src/dsymbol/ufcs.d b/dsymbol/src/dsymbol/ufcs.d index 875dd139..cde93033 100644 --- a/dsymbol/src/dsymbol/ufcs.d +++ b/dsymbol/src/dsymbol/ufcs.d @@ -14,14 +14,112 @@ import std.range; import std.string; import std.regex; import containers.hashset : HashSet; -import std.experimental.logger; +import std.logger.core; +import std.typecons : nullable, Nullable; + alias SortedTokens = SortedRange!(const(Token)[], "a < b"); +struct ScopeLookupContext +{ + Scope* completionScope; + const(Token)[] exprTokens; + size_t cursorPosition; + const(DSymbol)* getSymbolByName(string name) + { + return completionScope.getFirstSymbolByNameAndCursor(istring(name), cursorPosition); + } +} + +bool compatibleType(DSymbol* sym, ref const(Token) argToken, ScopeLookupContext scopeComplentionContext) +{ + if (!sym.type) + { + return false; + } + + switch (argToken.type) + { + mixin(STRING_LITERAL_CASES); + return sym.type.name == "string"; + case tok!"true": + case tok!"false": + return sym.type.name is getBuiltinTypeName(tok!"bool"); + + case tok!"intLiteral": + case tok!"longLiteral": + case tok!"uintLiteral": + case tok!"ulongLiteral": + // integer literals + return + // integers + sym.type.name is getBuiltinTypeName(tok!"int") || + sym.type.name is getBuiltinTypeName(tok!"uint") || + sym.type.name is getBuiltinTypeName(tok!"long") || + sym.type.name is getBuiltinTypeName(tok!"ulong") || + sym.type.name is getBuiltinTypeName(tok!"byte") || + sym.type.name is getBuiltinTypeName(tok!"ubyte") || + sym.type.name is getBuiltinTypeName(tok!"short") || + sym.type.name is getBuiltinTypeName(tok!"ushort") || + + // floats + sym.type.name is getBuiltinTypeName(tok!"float") || + sym.type.name is getBuiltinTypeName(tok!"double") || + sym.type.name is getBuiltinTypeName(tok!"real") || + + // complex + sym.type.name is getBuiltinTypeName(tok!"cfloat") || + sym.type.name is getBuiltinTypeName(tok!"cdouble") || + sym.type.name is getBuiltinTypeName(tok!"creal"); + + case tok!"floatLiteral": + case tok!"doubleLiteral": + case tok!"realLiteral": + return + // floats + sym.type.name is getBuiltinTypeName(tok!"float") || + sym.type.name is getBuiltinTypeName(tok!"double") || + sym.type.name is getBuiltinTypeName(tok!"real") || + + sym.type.name is getBuiltinTypeName(tok!"cfloat") || + sym.type.name is getBuiltinTypeName(tok!"cdouble") || + sym.type.name is getBuiltinTypeName(tok!"creal"); + + case tok!"ifloatLiteral": + case tok!"idoubleLiteral": + case tok!"irealLiteral": + // imaginary literals + return + // imaginary + sym.type.name is getBuiltinTypeName(tok!"ifloat") || + sym.type.name is getBuiltinTypeName(tok!"idouble") || + sym.type.name is getBuiltinTypeName(tok!"ireal") || + + // complex + sym.type.name is getBuiltinTypeName(tok!"cfloat") || + sym.type.name is getBuiltinTypeName(tok!"cdouble") || + sym.type.name is getBuiltinTypeName(tok!"creal"); + + default: + // Not a primitive type eg. Identifier + // Doing type looking up + if (argToken.text) + { + auto found = scopeComplentionContext.getSymbolByName(argToken.text); + if (found) + { + return sym.type is found.type; + } + } + return false; + } +} struct ExpressionInfo { const(DSymbol)* type; + const(Token)* significantToken; bool assumingLvalue; // We only assume else we need to do life time analysis. bool isFromFunction; + const(Token)[] arguments; string name; } @@ -63,7 +161,7 @@ enum string[string] INTEGER_PROMOTIONS = [ enum MAX_NUMBER_OF_MATCHING_RUNS = 50; -private const(Token)* findUFCSBaseToken( const(Token)[] tokens) +private const(Token)* findUFCSBaseToken(const(Token)[] tokens, out const(Token)[] arguments) { if (tokens.empty) return null; @@ -93,8 +191,25 @@ private const(Token)* findUFCSBaseToken( const(Token)[] tokens) continue; // If we hit a literal or identifier, that's likely our base - if (t is tok!"identifier" || isStringLiteral(t) || t is tok!"intLiteral" || t is tok!"floatLiteral") + if (isStringLiteral(t) || t is tok!"intLiteral" || t is tok!"floatLiteral") + { + return &tokens[i]; + } + if (t is tok!"identifier") { + // If this is a function call, then extract the arguments + if (tokens[i + 1] == tok!"(" && tokens[$ - 1] == tok!")") + { + auto argTokens = tokens[i + 2 .. $ - 1]; // get whats inside ( ... ) + foreach (argToken; argTokens) + { + if (argToken == tok!",") + { + continue; + } + arguments ~= argToken; + } + } return &tokens[i]; } @@ -138,8 +253,9 @@ private const(DSymbol)* resolveUFCSChainSymbol( foreach (sym; allSymbols) { if (sym.name != name) + { continue; - + } // Prefer a symbol that actually matches the type if (sym.isCallableWithArg(beforeDotType)) { @@ -148,35 +264,41 @@ private const(DSymbol)* resolveUFCSChainSymbol( // Otherwise remember a fallback (loose match) if (fallback is null) + { fallback = sym; + } } return fallback; } -private const(DSymbol)* deduceExpressionType( +private Nullable!ExpressionInfo deduceExpressionType( Scope* completionScope, const(Token)[] exprTokens, size_t cursorPosition) { + ExpressionInfo info; if (exprTokens.empty) { - return null; + return Nullable!ExpressionInfo.init; } - auto firstToken = findUFCSBaseToken(exprTokens); - if (isStringLiteral(firstToken.type)) + info.significantToken = findUFCSBaseToken(exprTokens, info.arguments); + if (isStringLiteral(info.significantToken.type)) { - return completionScope.getFirstSymbolByNameAndCursor( + info.type = completionScope.getFirstSymbolByNameAndCursor( symbolNameToTypeName(STRING_LITERAL_SYMBOL_NAME), cursorPosition); + return nullable(info); } - auto currentType = deduceSymbolTypeByToken(completionScope, *firstToken, cursorPosition); - + auto scopeLookupContext = ScopeLookupContext(completionScope, exprTokens, cursorPosition); + info.type = deduceSymbolTypeByToken(info, scopeLookupContext); - if (currentType is null) - return null; + if (info.type is null) + { + return Nullable!ExpressionInfo.init; + } // 2. Walk through the expression left → right for (size_t i = 1; i < exprTokens.length; i++) @@ -201,9 +323,9 @@ private const(DSymbol)* deduceExpressionType( } // Function call → move to return type - if (currentType !is null && currentType.type !is null) + if (info.type !is null && info.type.type !is null) { - currentType = currentType.type; + info.type = info.type.type; } i = j - 1; @@ -217,44 +339,79 @@ private const(DSymbol)* deduceExpressionType( { auto name = istring(exprTokens[i + 1].text); - // Get UFCS candidates for current type - ExpressionInfo beforeDotType; - beforeDotType.type = currentType; - beforeDotType.assumingLvalue = false; - // check if there it's from a function - auto functionSymbol = completionScope.getFirstSymbolByNameAndCursor(istring(firstToken.text), cursorPosition); - if (functionSymbol) { - beforeDotType.isFromFunction = functionSymbol.qualifier == SymbolQualifier.func; - beforeDotType.name = functionSymbol.name; - } - auto match = resolveUFCSChainSymbol( completionScope, - beforeDotType, + info, name, cursorPosition ); if (match is null) { - return null; + return Nullable!ExpressionInfo.init; } - // Update current type (this is the chain step) - currentType = match.type; - i++; // skip identifier continue; } } - return currentType; + return nullable(info); } -private const(DSymbol)* deduceSymbolTypeByToken(Scope* completionScope, scope ref const(Token) significantToken, size_t cursorPosition) +private const(DSymbol)* deduceSymbolTypeByToken(ref ExpressionInfo info, ScopeLookupContext scopeCompletionContext) { + const(DSymbol)* symbol = null; + auto found = scopeCompletionContext.completionScope.getSymbolsByNameAndCursor( + istring(info.significantToken.text), scopeCompletionContext.cursorPosition); + + if (found.empty) + { + return null; + } - const(DSymbol)* symbol = completionScope.getFirstSymbolByNameAndCursor( - istring(significantToken.text), cursorPosition); + if (found.length == 1) + { + symbol = found.front; + } + else if (found.length > 1) + { + // If we have more functions then we must have overloaded function + if (info.arguments.length > 0) + { + // we need to match with the arguments accordingly if any + // we assume that the first param matches since it's a UFCS call, hence why we - 1. + auto filtered = found.find!((i => max(i.functionParameters.length - 1, 0) == info + .arguments.length)); + if (filtered.length == 1) + { + // There is only 1 solution + symbol = filtered.front; + } + else if (filtered.length > 1) + { + bool allMatch = false; + foreach (DSymbol* sym; filtered) + { + allMatch = false; + foreach (idx, p; sym.functionParameters[1 .. $]) // we assume that the first param matches since it's a UFCS call, hence why we start with 1. + { + allMatch = compatibleType(p, info.arguments[idx], scopeCompletionContext); + if (!allMatch) + { + trace(sym.name," doesn't match with the arguments"); + break; + } + } + if (allMatch) + { + symbol = sym; + trace("Found the right overloaded function ", sym.type.name); + return sym.type; + } + } + } + } + } if (symbol is null) { @@ -268,9 +425,8 @@ private const(DSymbol)* deduceSymbolTypeByToken(Scope* completionScope, scope re || symbolType.kind == CompletionKind.aliasName)) { if (symbolType.type is null - || symbolType.type is symbolType - || symbolType.name.data == "string") // special case for string - { + || symbolType.type is symbolType) // special case for string + { break; } //look at next type to deduce @@ -378,7 +534,7 @@ private TokenCursorResult getCursorToken(Scope* completionScope, const(Token)[] } auto slicedAtParen = sortedBeforeTokens[0 .. index]; - + // Also allowing ) for ufcs function chaining if (slicedAtParen.length >= 3 && slicedAtParen[$ - 3].type is tok!"." @@ -456,15 +612,15 @@ DSymbol*[] getUFCSSymbolsForCursor(Scope* completionScope, scope ref const(Token return []; } - ExpressionInfo deducedSymbolType; - deducedSymbolType.type = deduceExpressionType(completionScope, tokenCursorResult.expressionTokens, cursorPosition); + Nullable!ExpressionInfo deducedSymbolType = deduceExpressionType(completionScope, tokenCursorResult + .expressionTokens, cursorPosition); - if (deducedSymbolType.type is null) + if (deducedSymbolType.isNull) { return []; } - if (deducedSymbolType.type.isInvalidForUFCSCompletion) + if (deducedSymbolType.get().type.isInvalidForUFCSCompletion) { trace("CursorSymbolType isn't valid for UFCS completion"); return []; @@ -472,11 +628,12 @@ DSymbol*[] getUFCSSymbolsForCursor(Scope* completionScope, scope ref const(Token if (tokenCursorResult.completionContext == CompletionContext.ParenCompletion) { - return getUFCSSymbolsForParenCompletion(deducedSymbolType, completionScope, tokenCursorResult.functionName, cursorPosition); + return getUFCSSymbolsForParenCompletion(deducedSymbolType.get(), completionScope, tokenCursorResult + .functionName, cursorPosition); } else { - return getUFCSSymbolsForDotCompletion(deducedSymbolType, completionScope, cursorPosition, tokenCursorResult + return getUFCSSymbolsForDotCompletion(deducedSymbolType.get(), completionScope, cursorPosition, tokenCursorResult .partialIdentifier); } @@ -609,10 +766,12 @@ private bool matchesWithTypeOfArray(scope ref const(DSymbol) incomingSymbolType, } -private bool matchStringLikeTypes(scope ref const(DSymbol) incomingSymbolType, scope ref const(DSymbol) significantSymbolType) +private bool matchStringLikeTypes(scope ref const(DSymbol) incomingSymbolType, scope ref const( + DSymbol) significantSymbolType) { if ((incomingSymbolType.name.data == "string" || incomingSymbolType.name.data == "wstring" - || incomingSymbolType.name.data == "dstring") && (significantSymbolType.name.data == "string" || significantSymbolType.name.data == "wstring" + || incomingSymbolType.name.data == "dstring") && (significantSymbolType.name.data == "string" || significantSymbolType + .name.data == "wstring" || significantSymbolType.name.data == "dstring")) { return true; diff --git a/tests/tc_ufcs_overloaded_function_chaining_completion/expected_completion_test.txt b/tests/tc_ufcs_overloaded_function_chaining_completion/expected_completion_test.txt new file mode 100644 index 00000000..7ee43086 --- /dev/null +++ b/tests/tc_ufcs_overloaded_function_chaining_completion/expected_completion_test.txt @@ -0,0 +1,5 @@ +identifiers +showSomething F int showSomething(int x, string message, bool ok, bool lol) stdin 58 int +showSomething F int showSomething(int x, string message, bool ok, float percentage) stdin 130 int +showSomething F void showSomething(int x, string message, bool ok) stdin 5 void +test F int test(int x) stdin 211 int diff --git a/tests/tc_ufcs_overloaded_function_chaining_completion/expected_completion_test2.txt b/tests/tc_ufcs_overloaded_function_chaining_completion/expected_completion_test2.txt new file mode 100644 index 00000000..cb2aba01 --- /dev/null +++ b/tests/tc_ufcs_overloaded_function_chaining_completion/expected_completion_test2.txt @@ -0,0 +1,4 @@ +identifiers +boo F Foo boo(Foo f, ulong id) stdin 325 Foo +boo F Foo boo(Foo f, ulong id, Bar b) stdin 414 Foo +boo F void boo(Foo f, ulong id, Bar b, float num) stdin 367 void diff --git a/tests/tc_ufcs_overloaded_function_chaining_completion/file.d b/tests/tc_ufcs_overloaded_function_chaining_completion/file.d new file mode 100644 index 00000000..a2687884 --- /dev/null +++ b/tests/tc_ufcs_overloaded_function_chaining_completion/file.d @@ -0,0 +1,29 @@ +void showSomething(int x, string message, bool ok) {} +int showSomething(int x, string message, bool ok, bool lol) {return 2;} +int showSomething(int x, string message, bool ok, float percentage) { return 1;} +int test(int x); + +void main() { + int x; + x.showSomething( "my message", true, 2). +} + +struct Foo {} +struct Bar {} + +Foo boo(Foo f, ulong id) { + return f; +} + +void boo(Foo f, ulong id, Bar b, float num) {} + +Foo boo(Foo f, ulong id, Bar b) { + return f; +} + +void doStuff() { + Foo f; + Bar b; + f.boo(2).boo(4, b). +} + diff --git a/tests/tc_ufcs_overloaded_function_chaining_completion/run.sh b/tests/tc_ufcs_overloaded_function_chaining_completion/run.sh new file mode 100755 index 00000000..63ceb5c0 --- /dev/null +++ b/tests/tc_ufcs_overloaded_function_chaining_completion/run.sh @@ -0,0 +1,8 @@ +set -e +set -u + +../../bin/dcd-client $1 --extended -c288 file.d > actual_completion_test.txt +diff actual_completion_test.txt expected_completion_test.txt --strip-trailing-cr + +../../bin/dcd-client $1 --extended -c511 file.d > actual_completion_test2.txt +diff actual_completion_test2.txt expected_completion_test2.txt --strip-trailing-cr \ No newline at end of file From 0db0bf740296655ba69f5297ed74b6a5ea50ad34 Mon Sep 17 00:00:00 2001 From: Dan Vu Date: Mon, 6 Apr 2026 15:03:25 +0200 Subject: [PATCH 08/10] adding guard --- dsymbol/src/dsymbol/ufcs.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsymbol/src/dsymbol/ufcs.d b/dsymbol/src/dsymbol/ufcs.d index cde93033..876ed5d0 100644 --- a/dsymbol/src/dsymbol/ufcs.d +++ b/dsymbol/src/dsymbol/ufcs.d @@ -198,7 +198,7 @@ private const(Token)* findUFCSBaseToken(const(Token)[] tokens, out const(Token)[ if (t is tok!"identifier") { // If this is a function call, then extract the arguments - if (tokens[i + 1] == tok!"(" && tokens[$ - 1] == tok!")") + if (i + 1 <= tokens.length && tokens[i + 1] == tok!"(" && tokens[$ - 1] == tok!")") { auto argTokens = tokens[i + 2 .. $ - 1]; // get whats inside ( ... ) foreach (argToken; argTokens) From 949b1f17dbb1a1d51b733f594ee62d07e0ac8e8f Mon Sep 17 00:00:00 2001 From: Dan Vu Date: Mon, 6 Apr 2026 15:04:47 +0200 Subject: [PATCH 09/10] using experimental logger again --- dsymbol/src/dsymbol/ufcs.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsymbol/src/dsymbol/ufcs.d b/dsymbol/src/dsymbol/ufcs.d index 876ed5d0..0ee5f9af 100644 --- a/dsymbol/src/dsymbol/ufcs.d +++ b/dsymbol/src/dsymbol/ufcs.d @@ -14,7 +14,7 @@ import std.range; import std.string; import std.regex; import containers.hashset : HashSet; -import std.logger.core; +import std.experimental.logger; import std.typecons : nullable, Nullable; alias SortedTokens = SortedRange!(const(Token)[], "a < b"); From 1309f2b3b07a33bffebe28482cedebb05f17c4f9 Mon Sep 17 00:00:00 2001 From: Dan Vu Date: Mon, 6 Apr 2026 15:09:02 +0200 Subject: [PATCH 10/10] Fixing one off --- dsymbol/src/dsymbol/ufcs.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsymbol/src/dsymbol/ufcs.d b/dsymbol/src/dsymbol/ufcs.d index 0ee5f9af..2333cb07 100644 --- a/dsymbol/src/dsymbol/ufcs.d +++ b/dsymbol/src/dsymbol/ufcs.d @@ -198,7 +198,7 @@ private const(Token)* findUFCSBaseToken(const(Token)[] tokens, out const(Token)[ if (t is tok!"identifier") { // If this is a function call, then extract the arguments - if (i + 1 <= tokens.length && tokens[i + 1] == tok!"(" && tokens[$ - 1] == tok!")") + if (tokens.length >= 2 && i + 1 < tokens.length && tokens[i + 1] == tok!"(" && tokens[$ - 1] == tok!")") { auto argTokens = tokens[i + 2 .. $ - 1]; // get whats inside ( ... ) foreach (argToken; argTokens)