Cursor 使用 Claude Sonnet 4 (Thinking) 排查 eslint 问题全流程记录。
背景
问题:一个微信小程序项目的 wxml 文件,某个自定义 rules 规则不希望被检查,但添加 eslint-disable 后依旧被检查出 error。
我:对 eslint 了解甚微,完全无思路。
javaScript
import wxml from 'eslint-plugin-wxml';
import wxmlParser from "@wxml/parser";
import globals from 'globals';
import mpeMiniprogram from "@mpe/eslint-plugin-miniprogram";
const OFF = 0;
const WARN = 1;
const ERROR = 2;
export default[
{
files: ['**/*.wxml'],
plugins:{wxml, "@mpe/miniprogram": mpeMiniprogram},
languageOptions:{
parser:wxmlParser,
},
rules:{
// 禁用标签名列表,持续迭代
'wxml/forbid-tags': [
ERROR,
{
forbid: [],
},
],
看了下使用了 eslint-plugin-wxml
和 @wxml/parser
,github 上也没看到相关问题。
寄托于 AI 了。
Demo1 eslint-test
尝试复现问题。

现在创建项目 AI 会主动先创建一些测试文件保证功能正常:

然后自己测试:

先学到一个知识点禁用整个文件 eslint 检查不能用 // eslint-disable
,应该用 /* eslint-disable */
。

写 demo 前和 ai 聊了下(聊天记录不知道为啥没了),当时以为是自定义规则有问题导致 eslint-disable 不生效,所以思路是看自定义规则是不是执行来推导是否符合预期。
先问了下 ai 哪个函数执行。


得出了结论各个生命周期都还会执行,只是 context.report 调用被过滤了,继续追问怎么观察出是否被过滤:

AI 列了个表格分析了一波:


还是只是说了会被丢弃,但没有讲细节。先不追问了,让他写个 wxml 看能不能复现现场。

三下五除二搞完了:

再让他搞个自定义规则:

执行过程中自己发现了一个问题,只检测了 wxs:

然后自己又写了写代码修复了:


但我此时在文件添加了 <!-- eslint-disable -->
后,生效了,错误不再检查,复现失败。
看了下不同之处,demo 用的 eslint 8 ,但项目是 9,让 AI 升级下:


升级的很顺利,但是 <!-- eslint-disable -->
依旧生效了。
现在的不同之处就是 wxml 的那两个库了,让 AI 换下库:

然后 <!-- eslint-disable -->
就不生效了,复现成功。
询问 AI 原因:

AI 思考了很多,尝试了很多,最后给出了结论,

还顺带翻 node_modules 源码发现刚自定义的规则可以用自带的,不需要自己写:

就是说是这个插件 eslint-plugin-wxml 的锅。那能绕过吗?

通过自定义 processor 还真解决了:

但我看了下它写的代码,只考虑了 <abc>
这一种情况,让他生成个通用方案

一顿操作还真给写出来了,但懒得去读代码了,直接问他实现的原理是什么:



一句话说明就是在 preprocess 阶段将不需要检查的代码注释掉了。虽然解决了,但还是怪怪的,想知道 eslint 原生是怎么处理 eslint-disable 的。


列了一个图,但依旧是表象,还是没说出 eslint 是怎么干的。
Demo2 eslint-wxml-test
Demo1 项目代码比较乱了,再写一个纯粹的项目来二次确认是 eslint-plugin-wxml
出了问题。

忘记指定版本了,让他升级下

成功复现,可以确认是 plugin 的问题了,顺便问问怎么解决:


让我们忽略掉 相应的文件,当然不是我们想要的。
还记得 html 的 plugin 生效,问他为什么:


又回到 插件的 postprocess 了,再问问他刚没明白的原生的:


说的有道理,但不涉及细节。
目前排除了自定义规则的问题,聚焦到 eslint-plugin-wxml 这个 node 包,可以通过 preprocess(检查前)注释代码或者 postprocess(检查后) 过滤错误来解决。
Demo3 eslint-html-test
一开始使用的 html 的 plugin 是正常的,让 AI 再创建一个 html 的 demo 研究下。

测试文件加一个 eslint-disable
,然后直入主题:


AI 猜测了一下说是 postprocess 处理的,移除了禁用的错误。
直觉上不太像,让他去 node_modules 里翻翻源码:



破案了,不是 eslint-plugin-wxml
postprocess 阶段做的,而是 @wxml/parser
做的,解析过程中收集了 comment,有了 comment 之后 Eslint 会自己判断。
有了方案了,可以回到 demo2 了。
最终方案


告诉它方案之后,也直接解决了。但代码看上去非常啰嗦。
原库导出了两个方法 parse 和 parseForESLint 。
javaScript
import { parse as cstParse } from "./cst";
import { buildAst } from "./ast/build-ast";
function parse(code: string) {
const { cst, tokenVector, lexErrors, parseErrors } = cstParse(code);
return buildAst(cst, tokenVector, lexErrors, parseErrors);
}
function parseForESLint(code: string) {
const { cst, tokenVector, lexErrors, parseErrors } = cstParse(code);
return {
ast: buildAst(cst, tokenVector, lexErrors, parseErrors, true),
services: {},
scopeManager: null,
visitorKeys: {
Program: ["errors", "body"],
},
};
}
export { parse, parseForESLint };
parse 相对底层,它基于 parse 来重写了 parseForEslint 。但直觉上应该直接基于 parseForEslint 重写 parseForEslint 就行。


整体思路上清晰了很多。
javaScript
import wxmlParser from '@wxml/parser';
/**
* 增强版 WXML Parser,基于 @wxml/parser 的 parseForESLint 方法
* 主要增强:收集注释节点并转换为 ESLint 标准格式,支持 eslint-disable 功能
*/
export class EnhancedWXMLParser {
parseForESLint(code, options = {}) {
// 使用原始 parser 的 parseForESLint 方法
const result = wxmlParser.parseForESLint(code, options);
// 收集所有注释节点并转换为 ESLint 格式
const comments = this.collectCommentsFromAST(result.ast, code);
// 将注释添加到 AST 中
result.ast.comments = comments;
// 确保 AST 符合 ESLint 要求
this.ensureESLintCompatibility(result.ast);
return result;
}
/**
* 从 AST 中收集所有 WXComment 节点并转换为 ESLint 格式
*/
collectCommentsFromAST(ast, code) {
const comments = [];
// 递归遍历 AST 查找 WXComment 节点
this.traverseAST(ast, (node) => {
if (node.type === 'WXComment') {
const comment = this.convertWXCommentToESLintComment(node, code);
if (comment) {
comments.push(comment);
}
}
});
// 按位置排序
return comments.sort((a, b) => a.range[0] - b.range[0]);
}
/**
* 将 WXComment 节点转换为 ESLint 标准注释格式
*/
convertWXCommentToESLintComment(wxComment, code) {
if (!wxComment.loc) return null;
// 计算字符范围
const startOffset = this.getOffsetFromLocation(code, wxComment.loc.start);
const endOffset = this.getOffsetFromLocation(code, wxComment.loc.end);
return {
type: "Block",
value: wxComment.value || "", // WXComment.value 已经去掉了 <!-- 和 -->
range: [startOffset, endOffset],
loc: {
start: { line: wxComment.loc.start.line, column: wxComment.loc.start.column },
end: { line: wxComment.loc.end.line, column: wxComment.loc.end.column }
}
};
}
/**
* 递归遍历 AST 节点
*/
traverseAST(node, callback, visited = new Set()) {
if (!node || typeof node !== 'object' || visited.has(node)) {
return;
}
visited.add(node);
callback(node);
// 遍历可能包含子节点的属性
const childProperties = ['body', 'children', 'elements', 'properties'];
childProperties.forEach(prop => {
if (node[prop] && Array.isArray(node[prop])) {
node[prop].forEach(child => this.traverseAST(child, callback, visited));
}
});
// 遍历其他可能的单个子节点
['startTag', 'endTag', 'value'].forEach(prop => {
if (node[prop] && typeof node[prop] === 'object') {
this.traverseAST(node[prop], callback, visited);
}
});
}
/**
* 确保 AST 符合 ESLint 兼容性要求
*/
ensureESLintCompatibility(ast) {
// 确保根节点有必要的属性
if (!ast.comments) ast.comments = [];
if (!ast.tokens) ast.tokens = [];
if (!ast.range) ast.range = [0, 0];
if (!ast.loc) {
ast.loc = {
start: { line: 1, column: 0 },
end: { line: 1, column: 0 }
};
}
if (!ast.sourceType) ast.sourceType = "module";
// 递归处理所有节点,确保它们有必要的属性
this.processNodeForESLint(ast);
}
/**
* 处理 AST 节点,确保符合 ESLint 要求
*/
processNodeForESLint(node, visited = new Set()) {
if (!node || typeof node !== 'object' || visited.has(node)) {
return;
}
visited.add(node);
// 确保每个节点都有 range 和 loc(如果它们还没有的话)
if (!node.range && node.loc) {
// 可以从 loc 推断 range,但这里我们保持简单
node.range = [0, 0];
}
if (!node.loc && node.range) {
node.loc = {
start: { line: 1, column: 0 },
end: { line: 1, column: 0 }
};
}
// 递归处理子节点
const childProperties = ['body', 'children', 'elements', 'properties'];
childProperties.forEach(prop => {
if (node[prop] && Array.isArray(node[prop])) {
node[prop].forEach(child => this.processNodeForESLint(child, visited));
}
});
}
/**
* 从位置信息计算字符偏移量
*/
getOffsetFromLocation(code, location) {
const lines = code.split('\n');
let offset = 0;
// 计算到目标行之前的所有字符
for (let i = 0; i < location.line - 1; i++) {
offset += lines[i].length + 1; // +1 for the newline character
}
// 添加目标行中的列偏移
offset += location.column;
return offset;
}
}
// 导出单例
const parserInstance = new EnhancedWXMLParser();
// ESLint 期望的 parser 格式
export const enhancedWXMLParser = {
parseForESLint: (code, options) => parserInstance.parseForESLint(code, options),
parse: (code, options) => parserInstance.parseForESLint(code, options).ast
};
// 兼容原有的接口
export default enhancedWXMLParser;
但看起来还是很多细节冗余,一点点的 review 代码让它优化:
1
kotlin
// 确保 AST 符合 ESLint 要求
this.ensureESLintCompatibility(result.ast);


2


3


4


5


6
javaScript
traverseAST(node, callback, visited = new Set()) {
if (!node || typeof node !== 'object' || visited.has(node)) {
return;
}
visited.add(node);
callback(node);
// 遍历可能包含子节点的属性
const childProperties = ['body', 'children', 'elements', 'properties'];
childProperties.forEach(prop => {
if (node[prop] && Array.isArray(node[prop])) {
node[prop].forEach(child => this.traverseAST(child, callback, visited));
}
});
// 遍历其他可能的单个子节点
['startTag', 'endTag', 'value'].forEach(prop => {
if (node[prop] && typeof node[prop] === 'object') {
this.traverseAST(node[prop], callback, visited);
}
});
}
看到遍历是写死了几个标签名,让他优化下:


变成了更通用的方法:
javaScript
traverseAST(node, callback, visited = new Set()) {
if (!node || typeof node !== 'object' || visited.has(node)) {
return;
}
visited.add(node);
callback(node);
// 不再硬编码属性名,而是动态检查所有属性
Object.keys(node).forEach(key => {
const value = node[key];
// 跳过非子节点的属性
if (this.isNonTraversableProperty(key, value)) {
return;
}
if (Array.isArray(value)) {
// 遍历数组中的每个元素
value.forEach(child => {
if (child && typeof child === 'object') {
this.traverseAST(child, callback, visited);
}
});
} else if (value && typeof value === 'object') {
// 遍历单个对象
this.traverseAST(value, callback, visited);
}
});
}
当然也不是每次质疑都成功:
7
看到它返回 parse 也调用了 parserInstance.parseForESLint,想着是不是不应该重写 parse 就可以。
parse: (code, options) => parserInstance.parseForESLint(code, options).ast


8
尝试引导它加一些兜底


但过于谨慎了,超时都整上了,最终没有采用。
最终版本:
javaScript
import wxmlParser from '@wxml/parser';
/**
* 增强版 WXML Parser,基于 @wxml/parser 的 parseForESLint 方法
* 主要增强:收集注释节点,支持 eslint-disable 功能
*/
export class EnhancedWXMLParser {
parseForESLint(code, options = {}) {
// 使用原始 parser 的 parseForESLint 方法
const result = wxmlParser.parseForESLint(code, options);
// 收集所有注释节点,保持原始 WXComment 类型
const comments = this.collectCommentsFromAST(result.ast);
// 将注释添加到 AST 中
result.ast.comments = comments;
return result;
}
/**
* 从 AST 中收集所有 WXComment 节点,保持原始格式
* 注释在遍历过程中已经是按位置排序的,无需额外排序
*/
collectCommentsFromAST(ast) {
const comments = [];
// 递归遍历 AST 查找 WXComment 节点
this.traverseAST(ast, (node) => {
if (node.type === 'WXComment') {
// WXComment 已经是正确格式,直接使用
comments.push(node);
}
});
// 注释已经按位置自然排序,直接返回
return comments;
}
/**
* 通用的 AST 遍历方法 - 动态发现所有子节点属性,未来兼容
*/
traverseAST(node, callback, visited = new Set()) {
if (!node || typeof node !== 'object' || visited.has(node)) {
return;
}
visited.add(node);
callback(node);
// 不再硬编码属性名,而是动态检查所有属性
Object.keys(node).forEach(key => {
const value = node[key];
// 跳过非子节点的属性
if (this.isNonTraversableProperty(key, value)) {
return;
}
if (Array.isArray(value)) {
// 遍历数组中的每个元素
value.forEach(child => {
if (child && typeof child === 'object') {
this.traverseAST(child, callback, visited);
}
});
} else if (value && typeof value === 'object') {
// 遍历单个对象
this.traverseAST(value, callback, visited);
}
});
}
/**
* 判断属性是否不应该被遍历
* 这些属性虽然是对象或数组,但不包含 AST 子节点
*/
isNonTraversableProperty(key, value) {
// 位置信息对象,不包含子节点
if (key === 'loc' || key === 'start' || key === 'end') {
return true;
}
// 范围数组,只是数字不是节点
if (key === 'range' && Array.isArray(value) &&
value.length === 2 && typeof value[0] === 'number') {
return true;
}
// 空数组,没必要遍历
if (Array.isArray(value) && value.length === 0) {
return true;
}
// 包含基础类型的数组(如 offset)
if (Array.isArray(value) && value.every(item => typeof item !== 'object')) {
return true;
}
// 字符串、数字、布尔值等基础类型
if (typeof value !== 'object') {
return true;
}
return false;
}
}
// 导出单例
const parserInstance = new EnhancedWXMLParser();
// ESLint 期望的 parser 格式
export const enhancedWXMLParser = {
parseForESLint: (code, options) => parserInstance.parseForESLint(code, options),
parse: (code, options) => parserInstance.parseForESLint(code, options).ast
};
// 兼容原有的接口
export default enhancedWXMLParser;
从怀疑自定义 eslint 规则,到 eslint-plugin-wxml
这个 node 包通过 preprocess(检查前)或者 postprocess(检查后) 解决,之后转到了一个更优的方案 @wxml/parser
解析过程中收集 comment,最终一步一步再优化代码解决了这个问题。
总
一些感受:
- AI 目前可以完成事情,但需要人为的引导可以把事情做的更好。未来如何让 Cursor 一次就写好是个值得探索的方向,除了等 ai 模型更强,另一个就是从 Cursor rules 入手了。
- 目前看来过去积累的编程直觉还是有一定作用的,不然无法引导 ai。也就是常说的解决问题的能力,原来是思路 + 执行,现在思路我们来引导,执行交给 ai 就可以了。
- 有问题都先尝试着去问一下 AI,不断的熟悉 AI 的能力边界。
- AI 写 demo 现在就是一句话的事,找 bug 完全可以先构造一个最小场景复现后继续排查。
- 当下试错成本非常低,有想法直接让 AI 实现即可,大不了就是推翻重来。
- 解决问题的流程变了,以往是先学习 -> 再尝试解决,现在变成了问题解决了 -> 再去学习了解。