第 54 章

Background Monitor:让 Claude 主动响应日志和状态变化的实现机制

第五十四章:LSP 插件开发:让 Claude Code 理解你的领域语言

54.1 为什么需要 LSP Plugin

当你在 Claude Code 中请求分析一个 .sol 文件(Solidity 智能合约),或者一个 .tf 文件(Terraform 配置),或者你公司内部自研的 DSL,会发生什么?

默认情况下,Claude 依赖自己训练数据中的知识来理解这些语言。对于 Solidity 这样相对主流的语言,Claude 的理解尚可接受。但对于:

Claude 的理解就会出现明显的盲区。LSP Plugin 正是为填补这些盲区而存在的。

54.2 Language Server Protocol 基础

LSP 的设计哲学

LSP(Language Server Protocol)由 Microsoft 在 2016 年提出,最初是为了解决编辑器与语言服务之间的 N×M 集成问题。在 LSP 之前,每个编辑器(VS Code、Vim、Emacs)都需要为每种语言(Python、Java、Go)分别实现智能功能,形成 N 个编辑器 × M 种语言 = N×M 个集成工作。

LSP 将这个问题变成了 N+M:每个语言只需实现一个 LSP Server,每个编辑器只需实现一个 LSP Client。

Claude Code 的 LSP Plugin 系统继承了这一设计,让语言服务可以向 Claude 提供精确的语言理解能力。

LSP 通信格式

LSP 基于 JSON-RPC 2.0 协议,消息通过标准输入/输出或 HTTP 传输。每条消息包含:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "textDocument/hover",
  "params": {
    "textDocument": {
      "uri": "file:///project/contracts/Token.sol"
    },
    "position": {
      "line": 42,
      "character": 15
    }
  }
}

LSP 核心能力

Claude Code 目前集成了以下 LSP 能力:

LSP 方法 作用 Claude Code 的使用方式
textDocument/diagnostics 提供错误和警告 注入到 Claude 的上下文,帮助识别问题
textDocument/hover 光标处的符号文档 Claude 分析代码时获取符号详情
textDocument/definition 跳转到定义 Claude 追踪代码引用链
textDocument/references 查找所有引用 Claude 分析变量/函数的使用范围
textDocument/documentSymbol 文件中的所有符号 Claude 快速了解文件结构
workspace/symbol 全工作区符号搜索 Claude 在大型项目中定位符号
textDocument/completion 代码补全建议 Claude 生成代码时参考领域规则
textDocument/codeAction 快速修复建议 Claude 提供修复方案时参考 LSP 建议

54.3 实现一个 LSP Server

项目准备

我们将实现一个 Solidity LSP Server 作为示例。Solidity 是以太坊智能合约的编程语言,有一些独特的安全规范需要实时检查。

mkdir solidity-lsp-plugin
cd solidity-lsp-plugin
claude-plugin init --type lsp

npm install vscode-languageserver vscode-languageserver-textdocument
npm install -D typescript @types/node

LSP Server 骨架

// lsp/server.ts
import {
  createConnection,
  TextDocuments,
  ProposedFeatures,
  InitializeParams,
  TextDocumentSyncKind,
  InitializeResult,
  Diagnostic,
  DiagnosticSeverity,
  CompletionItem,
  CompletionItemKind,
  TextDocumentPositionParams,
  HoverParams,
  Hover,
  MarkupKind,
} from "vscode-languageserver/node";
import { TextDocument } from "vscode-languageserver-textdocument";

// 创建 LSP 连接(通过 STDIO)
const connection = createConnection(ProposedFeatures.all);
const documents = new TextDocuments(TextDocument);

// 初始化握手
connection.onInitialize((params: InitializeParams): InitializeResult => {
  return {
    capabilities: {
      textDocumentSync: TextDocumentSyncKind.Incremental,
      completionProvider: {
        triggerCharacters: [".", "("],
        resolveProvider: true,
      },
      hoverProvider: true,
      diagnosticProvider: {
        interFileDependencies: false,
        workspaceDiagnostics: false,
      },
    },
    serverInfo: {
      name: "Solidity Language Server",
      version: "1.0.0",
    },
  };
});

// 启动文档管理和连接
documents.listen(connection);
connection.listen();

实现诊断(Diagnostics)

诊断是 LSP 最核心的功能。下面实现 Solidity 的常见安全漏洞检测:

// lsp/diagnostics/solidity.ts

interface DiagnosticRule {
  id: string;
  pattern: RegExp;
  message: string;
  severity: DiagnosticSeverity;
  suggestion?: string;
}

const SOLIDITY_RULES: DiagnosticRule[] = [
  {
    id: "SWC-107",
    pattern: /\.call\{value:/g,
    message: "Potential reentrancy vulnerability: external call with value transfer",
    severity: DiagnosticSeverity.Warning,
    suggestion: "Use the checks-effects-interactions pattern. Update state before making external calls.",
  },
  {
    id: "SWC-101",
    pattern: /\+\+|\-\-|\+|\-|\*|\//g,
    message: "Unchecked arithmetic: Consider using SafeMath or Solidity 0.8+",
    severity: DiagnosticSeverity.Information,
    suggestion: "Upgrade to Solidity ^0.8.0 for built-in overflow protection, or use OpenZeppelin's SafeMath.",
  },
  {
    id: "SWC-115",
    pattern: /tx\.origin/g,
    message: "Use of tx.origin for authorization is dangerous",
    severity: DiagnosticSeverity.Error,
    suggestion: "Use msg.sender instead of tx.origin for authorization checks.",
  },
  {
    id: "STYLE-001",
    pattern: /function\s+\w+\s*\([^)]*\)\s*public(?!\s+view|\s+pure|\s+returns)/g,
    message: "Public function that modifies state should consider adding a reentrancy guard",
    severity: DiagnosticSeverity.Hint,
  },
];

export function analyzeSolidity(
  document: TextDocument
): Diagnostic[] {
  const text = document.getText();
  const diagnostics: Diagnostic[] = [];
  const lines = text.split("\n");
  
  for (const rule of SOLIDITY_RULES) {
    // 跳过注释行(简化处理)
    lines.forEach((line, lineIndex) => {
      if (line.trimStart().startsWith("//") || 
          line.trimStart().startsWith("*")) {
        return;
      }
      
      let match;
      rule.pattern.lastIndex = 0; // 重置正则状态
      
      while ((match = rule.pattern.exec(line)) !== null) {
        const diagnostic: Diagnostic = {
          range: {
            start: { line: lineIndex, character: match.index },
            end: { line: lineIndex, character: match.index + match[0].length },
          },
          severity: rule.severity,
          code: rule.id,
          source: "solidity-lsp",
          message: rule.message,
          data: rule.suggestion, // 传递修复建议
        };
        diagnostics.push(diagnostic);
      }
    });
  }
  
  return diagnostics;
}

注册诊断处理器

// 在 server.ts 中注册
import { analyzeSolidity } from "./diagnostics/solidity.js";

// 文档变更时重新分析
documents.onDidChangeContent((change) => {
  const document = change.document;
  if (!document.uri.endsWith(".sol")) return;
  
  const diagnostics = analyzeSolidity(document);
  connection.sendDiagnostics({ uri: document.uri, diagnostics });
});

实现悬停文档(Hover)

当 Claude 分析 Solidity 代码时,悬停功能可以提供关键字和函数的详细解释:

// lsp/hover/solidity-docs.ts

const SOLIDITY_KEYWORDS: Record<string, string> = {
  "msg.sender": `**msg.sender** (address)
  
The address of the account that called this function.
For external function calls, this is the contract address of the caller.

**Security Note**: Never use \`tx.origin\` as a substitute for \`msg.sender\`.
\`tx.origin\` refers to the original external account that started the transaction,
which can be exploited in phishing attacks.

**Example**:
\`\`\`solidity
require(msg.sender == owner, "Not authorized");
\`\`\``,

  "msg.value": `**msg.value** (uint256)
  
The amount of Wei (1 Ether = 10^18 Wei) sent with the current call.
Only available in \`payable\` functions.

**Best Practice**: Always check \`msg.value\` before processing payments.
Consider using a pull-over-push pattern for withdrawals.`,

  "selfdestruct": `**selfdestruct(address payable recipient)**

Destroys the current contract and sends its remaining Ether balance to \`recipient\`.

⚠️ **DEPRECATION WARNING**: \`selfdestruct\` is deprecated since EIP-6049.
Avoid using it in new contracts. It may be removed or its behavior changed
in future hard forks.`,

  "delegatecall": `**delegatecall(bytes memory data)** → (bool success, bytes memory returndata)

Executes the code of the target contract in the context of the calling contract.
Storage, \`msg.sender\`, and \`msg.value\` all remain those of the calling contract.

⚠️ **Security Risk**: Improper use of \`delegatecall\` is a critical vulnerability.
Ensure the target contract's storage layout is compatible with the calling contract.
Never \`delegatecall\` to user-provided addresses.`,
};

connection.onHover((params: HoverParams): Hover | null => {
  const document = documents.get(params.textDocument.uri);
  if (!document) return null;
  
  // 获取光标处的词
  const wordRange = getWordRangeAtPosition(document, params.position);
  if (!wordRange) return null;
  
  const word = document.getText(wordRange);
  
  // 查找文档
  const docs = SOLIDITY_KEYWORDS[word];
  if (!docs) return null;
  
  return {
    contents: {
      kind: MarkupKind.Markdown,
      value: docs,
    },
    range: wordRange,
  };
});

实现代码补全(Completion)

// Solidity 关键字和常用函数的补全
connection.onCompletion(
  (params: TextDocumentPositionParams): CompletionItem[] => {
    const document = documents.get(params.textDocument.uri);
    if (!document?.uri.endsWith(".sol")) return [];
    
    const completions: CompletionItem[] = [
      {
        label: "require",
        kind: CompletionItemKind.Keyword,
        detail: "require(condition, message)",
        documentation: {
          kind: MarkupKind.Markdown,
          value: "Validates a condition. Reverts the transaction with `message` if condition is false.",
        },
        insertText: "require(${1:condition}, \"${2:error message}\");",
        insertTextFormat: 2, // Snippet
      },
      {
        label: "ReentrancyGuard",
        kind: CompletionItemKind.Class,
        detail: "OpenZeppelin ReentrancyGuard",
        documentation: {
          kind: MarkupKind.Markdown,
          value: "Modifier to prevent reentrant calls.\n\n```solidity\ncontract MyContract is ReentrancyGuard {\n    function withdraw() external nonReentrant {\n        // safe from reentrancy\n    }\n}\n```",
        },
        insertText: "ReentrancyGuard",
      },
      {
        label: "SPDX-License-Identifier",
        kind: CompletionItemKind.Snippet,
        detail: "SPDX license comment",
        insertText: "// SPDX-License-Identifier: ${1|MIT,Apache-2.0,GPL-3.0,UNLICENSED|}",
        insertTextFormat: 2,
      },
    ];
    
    return completions;
  }
);

54.4 将 LSP Server 集成进 Plugin

plugin.json 配置

{
  "name": "solidity-lsp-plugin",
  "version": "1.0.0",
  "description": "Solidity language support with security analysis for Claude Code",
  
  "lsp": {
    "languages": ["solidity"],
    "server": {
      "command": "node",
      "args": ["./dist/lsp/server.js"],
      "transport": "stdio"
    },
    "documentSelector": [
      { "language": "solidity", "pattern": "**/*.sol" }
    ],
    "initializationOptions": {
      "solcVersion": "0.8.24",
      "enableSecurityAnalysis": true
    }
  },
  
  "engines": {
    "claude-code": ">=1.0.0"
  }
}

Claude Code 如何使用 LSP 数据

当 Claude 分析 .sol 文件时,Claude Code 会自动:

  1. 启动 LSP Server(如果尚未运行)
  2. 通知 LSP Server 打开文件(textDocument/didOpen
  3. 请求文件诊断(textDocument/diagnostic
  4. 将诊断结果追加到 Claude 的上下文:
[LSP Diagnostics for Token.sol]
Line 47: WARNING (SWC-107) - Potential reentrancy vulnerability: external call with value transfer
  Suggestion: Use the checks-effects-interactions pattern. Update state before making external calls.
Line 52: ERROR (SWC-115) - Use of tx.origin for authorization is dangerous
  Suggestion: Use msg.sender instead of tx.origin for authorization checks.

Claude 收到这些上下文后,在分析代码时就拥有了 LSP Server 提供的专业领域知识。

54.5 为企业内部 DSL 构建 LSP

DSL 解析器设计

假设你的公司有一种内部工作流定义语言 .flow

# example.flow
workflow OrderProcessing {
  trigger: order.created
  
  step ValidateOrder {
    action: validate_order
    on_failure: NotifyTeam
  }
  
  step ChargePayment {
    action: charge_stripe
    retry: 3
    on_failure: RefundAndNotify
  }
  
  step FulfillOrder {
    action: create_fulfillment
    depends_on: ChargePayment
  }
}

为这个 DSL 构建 LSP Server:

// lsp/flow-parser.ts

interface WorkflowAST {
  name: string;
  trigger: string;
  steps: StepNode[];
}

interface StepNode {
  name: string;
  action: string;
  retry?: number;
  onFailure?: string;
  dependsOn?: string[];
  range: Range;
}

export function parseFlowDocument(text: string): {
  ast: WorkflowAST | null;
  errors: ParseError[];
} {
  const errors: ParseError[] = [];
  const lines = text.split("\n");
  
  // 简化的递归下降解析器
  // 生产环境中建议使用 ANTLR、tree-sitter 或 Ohm.js
  
  const workflowMatch = text.match(/workflow\s+(\w+)\s*\{/);
  if (!workflowMatch) {
    return { ast: null, errors: [{ message: "Missing workflow declaration", line: 0 }] };
  }
  
  const ast: WorkflowAST = {
    name: workflowMatch[1],
    trigger: "",
    steps: [],
  };
  
  // 检测步骤的循环依赖
  const stepNames = new Set<string>();
  const dependencies: Map<string, string[]> = new Map();
  
  const stepRegex = /step\s+(\w+)\s*\{([^}]+)\}/g;
  let match;
  
  while ((match = stepRegex.exec(text)) !== null) {
    const stepName = match[1];
    const stepBody = match[2];
    
    stepNames.add(stepName);
    
    const dependsOnMatch = stepBody.match(/depends_on:\s*(\w+)/);
    if (dependsOnMatch) {
      dependencies.set(stepName, [dependsOnMatch[1]]);
    }
  }
  
  // 检查依赖是否指向不存在的步骤
  for (const [step, deps] of dependencies) {
    for (const dep of deps) {
      if (!stepNames.has(dep)) {
        const lineNum = findLineNumber(text, `depends_on: ${dep}`);
        errors.push({
          message: `Step "${step}" depends on undefined step "${dep}"`,
          line: lineNum,
          severity: DiagnosticSeverity.Error,
        });
      }
    }
  }
  
  return { ast, errors };
}

54.6 LSP 性能优化

增量更新

避免每次文档变更都重新分析整个文件。利用 LSP 的增量同步机制:

// 使用防抖减少分析频率
const analysisDebounce = new Map<string, NodeJS.Timeout>();

documents.onDidChangeContent((change) => {
  const uri = change.document.uri;
  
  // 清除之前的定时器
  const existing = analysisDebounce.get(uri);
  if (existing) clearTimeout(existing);
  
  // 300ms 后执行分析(等待用户停止输入)
  const timer = setTimeout(() => {
    const document = documents.get(uri);
    if (document) {
      const diagnostics = analyzeDocument(document);
      connection.sendDiagnostics({ uri, diagnostics });
    }
    analysisDebounce.delete(uri);
  }, 300);
  
  analysisDebounce.set(uri, timer);
});

缓存解析结果

// 缓存 AST 避免重复解析
const astCache = new Map<string, { version: number; ast: WorkflowAST }>();

function getCachedAST(document: TextDocument) {
  const cached = astCache.get(document.uri);
  if (cached && cached.version === document.version) {
    return cached.ast;
  }
  
  const { ast } = parseFlowDocument(document.getText());
  if (ast) {
    astCache.set(document.uri, { version: document.version, ast });
  }
  return ast;
}

54.7 调试 LSP Server

# 以调试模式启动 LSP Server(输出通信日志)
LSP_DEBUG=1 node dist/lsp/server.js 2>lsp-debug.log

# 使用 LSP Inspector 手动测试
npx @vscode/lsp-inspector

# 查看 Claude Code 收到的 LSP 数据
CLAUDE_LSP_DEBUG=1 claude

常见问题排查

LSP Server 无法识别文件类型:检查 documentSelector 中的 pattern 是否正确匹配文件路径。

诊断没有显示在 Claude 的上下文中:确认 Claude Code 版本是否支持你使用的 LSP 能力(查看 engines.claude-code 声明)。

补全建议不出现:确认 capabilities.completionProvider 中声明了正确的触发字符。


小结

LSP Plugin 通过 Language Server Protocol 向 Claude 提供精确的领域语言理解能力:诊断暴露代码中的问题,悬停文档提供符号解释,代码补全帮助生成符合领域规范的代码。实现一个 LSP Server 的核心步骤是:建立 JSON-RPC 通信连接、注册 onInitialize、实现具体的 LSP 能力处理器,最后在 plugin.json 中配置语言绑定。企业内部 DSL 的 LSP 支持,是提升 Claude 在特定代码库中准确率的最有效手段之一。下一章讨论监控与可观测性插件。

本章评分
4.7  / 5  (3 评分)

💬 留言讨论