毕昇C语言服务器代码补全实现细节
毕昇C语言服务器代码补全实现细节
实现关键字的代码补全主要分为以下几步
-
测试当前语法 是否会导致语言服务器崩溃
-
在合适的上下文补全函数中添加关键字
-
写测试代码,确保测试通过
-
合并main分支代码,做全量测试。
一 测试当前语法 是否会导致语言服务器崩溃
目的是测试语法在语言服务器环境下是否正常。设计语言编译器时如果不考虑语言服务器的特殊场景,通常解析逻辑不会考虑代码不完整的情况。但是在使用语言服务器即编辑代码时,通常是没有完整代码的。
毕昇C语言的Trait语法示例
标准写法
trait ShapeA{
int getA(This* this);
}
使用IDE编写如下代码时,输入字符T后,clangd语言服务器崩溃。 崩溃日志
I[09:43:57.927] <-- textDocument/semanticTokens/full(5)
clangd: /home/aaa/llvm-project/llvm/include/llvm/Support/Casting.h:109: static bool llvm::isa_impl_cl<To, const From*>::doit(const From*) [with To = clang::ParmVarDecl; From = clang::Decl]: Assertion `Val && "isa<> used on a null pointer"' failed.
/lib/x86_64-linux-gnu/libpthread.so.0(+0x14420)[0x7f9738860420]
...
...
/home/aaa/llvm-project/build/bin/clangd(+0x192bf30)[0x557cd9617f30]
/lib/x86_64-linux-gnu/libpthread.so.0(+0x8609)[0x7f9738854609]
Stack dump without symbol names (ensure you have llvm-symbolizer in your PATH or set the environment var `LLVM_SYMBOLIZER_PATH` to point to it):
Signalled during AST worker action: Build AST
Filename: /home/aaa/Projects/bishengDemo/complete_test.cbs
Directory: /home/aaa/Projects/bishengDemo/
Command Line: /home/aaa/llvm-project/build/bin/clang -fsyntax-only -resource-dir=/home/aaa/llvm-project/build/lib/clang/15.0.4 -- /home/aaa/Projects/bishengDemo/complete_test.cbs
Version: 38
[Error - 09:43:58] Server process exited with signal SIGABRT.
[Error - 09:43:58] The Clang Language Server server crashed 5 times in the last 3 minutes. The server will not be restarted. See the output for more information.
[Error - 09:43:58] Request textDocument/documentSymbol failed.
[object Object]
[Error - 09:43:58] Request textDocument/codeAction failed.
[object Object]
[Error - 09:43:58] Request textDocument/documentLink failed.
[object Object]
[Error - 09:43:58] Request textDocument/inlayHint failed.
[object Object]
[Error - 09:43:58] Request textDocument/semanticTokens/full failed.
[object Object]
崩溃原因
在毕昇C中解析Trait成员函数逻辑ParseTraitMemberDeclaration函数模仿C++ 通过ThisDecl 指针调用了一个延迟解析函数。但是在语言服务器调用ParseTraitMemberDeclaration这个函数时成员函数还没有完成,所以ThisDecl 指针是Null。再调用其他函数会导致语言服务器崩溃。
部分代码:
Parser::ParseTraitMemberDeclaration(ParsedAttributes &AccessAttrs) {
....
while (1) {
NamedDecl *ThisDecl =
Actions.ActOnTraitMemberDeclarator(getCurScope(), DeclaratorInfo);
if (ThisDecl) {
Actions.ProcessDeclAttributeList(getCurScope(), ThisDecl, AccessAttrs);
if (!ThisDecl->isInvalidDecl()) {
// Set the Decl for any late parsed attributes
for (unsigned i = 0, ni = CommonLateParsedAttrs.size(); i < ni; ++i)
CommonLateParsedAttrs[i]->addDecl(ThisDecl);
for (unsigned i = 0, ni = LateParsedAttrs.size(); i < ni; ++i)
LateParsedAttrs[i]->addDecl(ThisDecl);
}
Actions.FinalizeDeclaration(ThisDecl);
DeclsInGroup.push_back(ThisDecl); // Put each Decl inside struct Foo
if (DeclaratorInfo.isFunctionDeclarator() &&
DeclaratorInfo.getDeclSpec().getStorageClassSpec() !=
DeclSpec::SCS_typedef)
HandleMemberFunctionDeclDelays(DeclaratorInfo, ThisDecl);
}
}
解决方案
这个例子中Trait成员函数不需要延迟解析,所以可以直接删掉对应代码不影响Trait语法编译。
通用流程
在IDE中编辑代码,确定编辑到哪个字母后语言服务器发生崩溃。
分析崩溃日志,使用调试工具定位到发生错误的函数。
修改函数处理逻辑,修复错误
二 在合适的上下文补全函数中添加关键字
以Trait语法为例,实现trait和impl两个关键字的代码补全功能。
llvm项目中关键字补全逻辑通常在文件 clang/lib/Sema/SemaCodeComplete.cpp 函数
static void AddOrdinaryNameResults(Sema::ParserCompletionContext CCC, Scope *S,
Sema &SemaRef, ResultBuilder &Results)
在这个函数中添加trait和impl逻辑
if (SemaRef.getLangOpts().BSC) {
bool InTrait = false;
for (Scope *TempS = S; TempS; TempS = TempS->getParent()) {
if (TempS->isTraitScope()) {
InTrait = true;
break;
}
}
if (WantTypesInContext(CCC, SemaRef.getLangOpts())) {
Results.AddResult(Result("trait", CodeCompletionResult::RK_Keyword));
}
// trait
if (!InTrait && CCC == Sema::PCC_Namespace) {
if (Results.includeCodePatterns()) {
// trait <identifier> { declarations }
Builder.AddTypedTextChunk("trait");
Builder.AddChunk(CodeCompletionString::CK_HorizontalSpace);
Builder.AddPlaceholderChunk("name");
Builder.AddChunk(CodeCompletionString::CK_HorizontalSpace);
Builder.AddChunk(CodeCompletionString::CK_LeftBrace);
Builder.AddChunk(CodeCompletionString::CK_VerticalSpace);
Builder.AddPlaceholderChunk("declarations");
Builder.AddChunk(CodeCompletionString::CK_VerticalSpace);
Builder.AddChunk(CodeCompletionString::CK_RightBrace);
Builder.AddChunk(CodeCompletionString::CK_SemiColon);
Results.AddResult(Result(Builder.TakeString()));
}
if (Results.includeCodePatterns()) {
// impl trait <TraitName> for <TypeName>
Builder.AddTypedTextChunk("impl");
Builder.AddChunk(CodeCompletionString::CK_HorizontalSpace);
Builder.AddTextChunk("trait");
Builder.AddChunk(CodeCompletionString::CK_HorizontalSpace);
Builder.AddPlaceholderChunk("TraitName");
Builder.AddChunk(CodeCompletionString::CK_HorizontalSpace);
Builder.AddTextChunk("for");
Builder.AddChunk(CodeCompletionString::CK_HorizontalSpace);
Builder.AddPlaceholderChunk("TypeName");
Builder.AddChunk(CodeCompletionString::CK_SemiColon);
Results.AddResult(Result(Builder.TakeString()));
}
Results.AddResult(Result("impl trait", CodeCompletionResult::RK_Keyword));
}
三 测试语言服务器代码补全
在clang-tools-extra/clangd/test/bsc中添加测试用例。 测试补全结果是否正确。
# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace -v %s
# RUN: clangd -lit-test -pch-storage=memory < %s | FileCheck -strict-whitespace %s
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}}
---
{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cbs","languageId":"bsc","version":1,"text":"tra"}}}
---
{"jsonrpc":"2.0","id":2,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:///main.cbs"},"position":{"line":0,"character":3}}}
# CHECK: "id": 2,
# CHECK-NEXT: "jsonrpc": "2.0",
# CHECK-NEXT: "result": {
# CHECK-NEXT: "isIncomplete": {{.*}},
# CHECK-NEXT: "items": [
# CHECK-NEXT: {
# CHECK-NEXT: "filterText": "trait",
# CHECK-NEXT: "insertText": "trait",
# CHECK-NEXT: "insertTextFormat": 1,
# CHECK-NEXT: "kind": 14,
# CHECK-NEXT: "label": " trait",
# CHECK-NEXT: "score": {{.*}},
# CHECK-NEXT: "sortText": "{{.*}}",
# CHECK-NEXT: "textEdit": {
# CHECK-NEXT: "newText": "trait",
# CHECK-NEXT: "range": {
# CHECK-NEXT: "end": {
# CHECK-NEXT: "character": 3,
# CHECK-NEXT: "line": 0
# CHECK-NEXT: },
# CHECK-NEXT: "start": {
# CHECK-NEXT: "character": 0,
# CHECK-NEXT: "line": 0
---
{"jsonrpc":"2.0","method":"textDocument/didChange","params":{"textDocument":{"uri":"test:///main.cbs","languageId":"c","version":2,"text":"trait Drawable {\n void draw(T)\n};"}}}
---
# 位置在第 1 行(第二行),字符 'T' 之后(character: 13)
# 注意:第一行末尾有 \n,所以 ' void draw(T' 的 T 在 index 13
{"jsonrpc":"2.0","id":4,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:///main.cbs"},"position":{"line":1,"character":13}}}
# CHECK: "id": 4,
# CHECK: "jsonrpc": "2.0",
# CHECK: "result": {
# CHECK: "isIncomplete": {{.*}},
# CHECK: "items": [
# CHECK: {
# CHECK: "label": "{{.*}}"
# CHECK: }
# CHECK: ]
# CHECK: }
---
{"jsonrpc":"2.0","id":5,"method":"shutdown"}
---
{"jsonrpc":"2.0","method":"exit"}
使用下面的命令完成测试
llvm-lit -sv clang-tools-extra/clangd/test
合并main分支代码,做回归测试
自己修改代码后合并main分支代码,在提交前做一次完整的回归测试。因为修改语言服务器只涉及Parser和Sema, 也可以只做这部分测试。
# 测试所有内容
ninja check-all
# 只测试
llvm-lit -sv clang/test/Parser
llvm-lit -sv clang/test/Sema
llvm-lit -sv clang/test/BSC
需要注意官方仓库使用ninja check-all测试时也会出现测试失败的例子。错误主要在bsc clang-tidy部分。使用llvm-lit测试Parser、Sema和BSC没有问题,都能通过测试。