Javascript is required
实现原理发布于 2026-07-28审校于 2026-08-087 分钟阅读

SQL Formatter Lexer 与高亮引擎:Token、缩进状态和能力边界

SQL 高亮和基础格式化通常先把文本切成 token,再根据关键字、字符串、注释和括号状态决定颜色与缩进。lexer 只识别局部词法,不一定建立完整 AST,也不能据此验证 SQL 语义。本文聚焦 token、状态和缩进边界,不把高亮引擎描述成完整 parser。

SQL FormatterLexerTokensSyntax Highlighting

一、问题概述:按空格切分无法支持 SQL 高亮

SELECT 'a,b' -- note 中的逗号属于字符串,-- 之后是注释;如果直接 split,高亮器会把字符串内容当成关键字或把注释中的词染色。lexer 需要记录当前状态,至少区分普通文本、引号和注释。

二、最小复现:字符串和注释中的关键字不应高亮为 SQL

下面的输入只用于说明 token 边界。关键字集合是演示用的有限集合,不代表完整方言词表。

const sql = "SELECT 'FROM users' AS note -- SELECT inside comment\nFROM users";
// Expected conceptual tokens:
// KEYWORD SELECT, STRING 'FROM users', WORD AS, WORD note, COMMENT -- ..., KEYWORD FROM, WORD users

三、根因:lexer 依赖状态机而不是正则替换

字符串可能包含转义或成对引号,注释可能跨行,标识符还可能使用方言特有的引号。lexer 在读取每个字符时要决定何时进入或离开状态;状态错误会让后续整行的高亮和缩进一起偏移。

四、推荐方案:先产出 token,再让 formatter/highlighter 消费

将 token 类型、原始文本和起止位置保留下来,高亮只映射 token 到 CSS 类,缩进逻辑只根据括号和关键字状态调整。遇到未知扩展时保留原 token。这样可以明确区分词法识别、视觉高亮和格式化规则。

五、完整代码:一个受限 lexer 与高亮映射

下面的 lexer 支持普通单词、数字、字符串、行/块注释和标点,并记录 token 类型;它不构造 AST、不验证表名和列是否存在,也不覆盖所有 SQL 方言。

type TokenKind = "keyword" | "word" | "number" | "string" | "comment" | "punctuation";
type Token = { kind: TokenKind; text: string; start: number; end: number };
const keywords = new Set(["SELECT", "FROM", "WHERE", "JOIN", "AND", "OR", "AS"]);

function lexSql(source: string): Token[] {
  const tokens: Token[] = [];
  let i = 0;
  while (i < source.length) {
    const start = i;
    const ch = source[i];
    if (/\s/.test(ch)) { i += 1; continue; }
    if (ch === "-" && source[i + 1] === "-") {
      i += 2;
      while (i < source.length && source[i] !== "\n") i += 1;
      tokens.push({ kind: "comment", text: source.slice(start, i), start, end: i });
      continue;
    }
    if (ch === "/" && source[i + 1] === "*") {
      i += 2;
      while (i + 1 < source.length && !(source[i] === "*" && source[i + 1] === "/")) i += 1;
      i = Math.min(source.length, i + 2);
      tokens.push({ kind: "comment", text: source.slice(start, i), start, end: i });
      continue;
    }
    if (ch === "'" || ch === "\"" || ch === String.fromCharCode(96)) {
      const quote = ch;
      i += 1;
      while (i < source.length) {
        if (source[i] === quote && source[i + 1] === quote) { i += 2; continue; }
        if (source[i] === quote) { i += 1; break; }
        i += 1;
      }
      tokens.push({ kind: "string", text: source.slice(start, i), start, end: i });
      continue;
    }
    if (/\d/.test(ch)) {
      i += 1;
      while (i < source.length && /[\d.]/.test(source[i])) i += 1;
      tokens.push({ kind: "number", text: source.slice(start, i), start, end: i });
      continue;
    }
    if (/[A-Za-z_]/.test(ch)) {
      i += 1;
      while (i < source.length && /[A-Za-z0-9_$]/.test(source[i])) i += 1;
      const text = source.slice(start, i);
      tokens.push({ kind: keywords.has(text.toUpperCase()) ? "keyword" : "word", text, start, end: i });
      continue;
    }
    i += 1;
    tokens.push({ kind: "punctuation", text: ch, start, end: i });
  }
  return tokens;
}

function highlightClass(token: Token): string {
  return "sql-" + token.kind;
}

console.log(lexSql("SELECT 'FROM users' -- note\nFROM users").map(highlightClass));

六、常见错误方案

用正则把所有大写单词包成关键字会误伤字符串和注释;只按括号数量缩进会忽略注释与方言构造;高亮 lexer 成功就宣称 SQL 有效,也无法发现列名、类型或语义错误。

七、边界条件:转义、反引号与缩进状态

不同方言对反斜杠、成对引号、反引号标识符和 dollar-quoted 字符串的规则不同。缩进器还要决定括号、CASE、JOIN 和子查询何时增加或减少层级;未知 token 应保持原文并避免破坏后续位置。

八、如何验证 lexer 与高亮

用字符串中的关键字、单/双/反引号、行/块注释、数字、小数、嵌套括号和跨行查询测试。断言 token 原文拼接后等于输入,字符串/注释不会被标成 keyword,token 起止位置单调递增;再单独验证缩进规则。

九、FAQ

问:lexer 能替代 SQL parser 吗?答:不能,它只提供词法 token,通常没有完整 AST 和语义验证。

问:为什么高亮看起来正确但 formatter 仍会错位?答:高亮和缩进可能使用不同状态规则,任一状态机遗漏方言语法都会影响格式。

问:未知函数应当高亮成关键字吗?答:不应猜测,保留为普通 token 或扩展 token,并由目标方言配置决定。

十、总结

SQL 高亮和基础格式化依赖 lexer 的状态、token 原文和位置;字符串、注释、引号和括号会影响缩进状态。lexer/highlighter 不是完整 SQL parser,验证时应检查 token 保真,再把语义校验交给真正的 parser 或数据库。

来源与延伸阅读

技术审校所依据的规范与权威参考资料。

相关文章

继续阅读

可打开关联的浏览器工具,使用自己的样本验证文中的处理流程。

打开关联工具