标题:PEG.js 中正则字符类 [A-z] 的陷阱与变量名解析修复指南

18次阅读

标题:PEG.js 中正则字符类 [A-z] 的陷阱与变量名解析修复指南

本文揭示 peg.js 语法中 `[a-z]` 字符类的常见误解——它并非仅匹配字母,而是包含 ASCII 表中 `’z’`(ascii 90)到 `’a’`(ascii 97)之间的所有字符(如 `[`, “, `]`, `^`, `_`, “ ` “),导致变量名意外吞并左方括号,引发 `test[` 被误识别为非法变量名的错误;并提供安全、可维护的修复方案。

在 PEG.js 中,字符类(character class)如 [A-z] 并非等价于“大小写字母”,而是一个基于 ASCII 码值的连续区间匹配。’A’ 的 ASCII 值为 65,’z’ 为 122,但 ‘Z’ 是 90,’a’ 是 97 —— 因此 [A-z] 实际覆盖了 A–Z(65–90)、[–(91)、–(92)、](93)、^(94)、_(95)、`(96)以及 a–z(97–122)。这正是问题根源:当输入为 test[“foobar”] 时,Varname 规则中的 [A-z0-9]+ 会贪婪匹配 test[(因为 [ 属于该范围),导致后续的 ‘[‘ 无法被 Getvar 中的属性访问语法 ‘[‘, _, exp, _, ‘]’ 捕获,最终抛出 Variable ‘test[‘ does not exist. 错误。

✅ 正确做法是显式限定字母范围,使用 [A-Za-z] 或更推荐的 不区分大小写的内建修饰符 i 配合 [A-Z0-9]:

Varname "variable name"   = [A-Za-z][A-Za-z0-9]* { return text(); }   // 或更简洁、语义清晰的写法:   // = [A-Z][A-Z0-9]*i { return text(); }

⚠️ 注意:i 修饰符必须作用于整个字符类(如 [A-Z0-9]i),而非单个字符;且首字符应确保为字母(避免数字开头的非法标识符),因此建议拆分为「首字母」+「字母数字续字符」结构:

Varname "variable name"   = first:[A-Za-z] rest:[A-Za-z0-9]* {       const name = first + rest.join('');       if (!/[A-Za-z]/.test(first)) {         error(`Variable name must start with a letter. (got '${name}')`);       }       return name;     }

此外,原 Getvar 规则末尾缺少跳过空白的 _,易导致 test[“foobar”] 中引号前空格解析失败。应修正为:

Getvar   = name:Varname _ path:('[' _ exp:(String / Integer) _ ']' { return exp; })* {       let rt = glob[name];       if (rt === undefined && name !== 'undefined' && name !== 'null') {         error(`Variable '${name}' does not exist.`);       }       for (const p of path) {         rt = rt?.[p]; // 使用可选链增强健壮性         if (rt === undefined) break;       }       return rt;     }

? 总结关键修复点:

  • ❌ 删除 [A-z] —— 它是危险的 ASCII 区间陷阱;
  • ✅ 使用 [A-Za-z] 或 [A-Z]i 明确表达“字母”意图;
  • ✅ 为 Varname 添加首字符校验,禁止数字开头;
  • ✅ 在所有语法连接处(如 name 与 ‘[‘ 之间)插入 _ 消除空白干扰;
  • ✅ 在属性访问中加入 ?. 可选链,避免 undefined[“prop”] 报错;
  • ✅ 优先用规则拆分(如 Vstart/Vtail)替代复杂字符类量化,提升可读性与回溯可控性。

遵循以上原则,你的 PEG.js 解析器将准确识别 test 和 test[“foobar”],并稳定支持嵌套属性访问语法。

text=ZqhQzanResources