Cursor 排查 eslint 问题全过程记录

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 实现即可,大不了就是推翻重来。
  • 解决问题的流程变了,以往是先学习 -> 再尝试解决,现在变成了问题解决了 -> 再去学习了解。
相关推荐
拾光拾趣录13 分钟前
for..in 和 Object.keys 的区别:从“遍历对象属性的坑”说起
前端·javascript
OpenTiny社区24 分钟前
把 SearchBox 塞进项目,搜索转化率怒涨 400%?
前端·vue.js·github
编程猪猪侠1 小时前
Tailwind CSS 自定义工具类与主题配置指南
前端·css
qhd吴飞1 小时前
mybatis 差异更新法
java·前端·mybatis
YGY Webgis糕手之路1 小时前
OpenLayers 快速入门(九)Extent 介绍
前端·经验分享·笔记·vue·web
患得患失9491 小时前
【前端】【vueDevTools】使用 vueDevTools 插件并修改默认打开编辑器
前端·编辑器
ReturnTrue8681 小时前
Vue路由状态持久化方案,优雅实现记住表单历史搜索记录!
前端·vue.js
UncleKyrie1 小时前
一个浏览器插件帮你查看Figma设计稿代码图片和转码
前端
遂心_1 小时前
深入解析前后端分离中的 /api 设计:从路由到代理的完整指南
前端·javascript·api
你听得到112 小时前
Flutter - 手搓一个日历组件,集成单日选择、日期范围选择、国际化、农历和节气显示
前端·flutter·架构