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 实现即可,大不了就是推翻重来。
  • 解决问题的流程变了,以往是先学习 -> 再尝试解决,现在变成了问题解决了 -> 再去学习了解。
相关推荐
hahala233317 分钟前
ESLint 提交前校验技术方案
前端
鱼蛋EggfishStudio26 分钟前
后端转前端的第一次尝试:用AI辅助开发纯静态网页实现一个可编程节奏的节拍器
cursor
夕水39 分钟前
ew-vue-component:Vue 3 动态组件渲染解决方案的使用介绍
前端·vue.js
我麻烦大了42 分钟前
实现一个简单的Vue响应式
前端·vue.js
独立开阀者_FwtCoder1 小时前
你用 Cursor 写公司的代码安全吗?
前端·javascript·github
Cacciatore->1 小时前
React 基本介绍与项目创建
前端·react.js·arcgis
摸鱼仙人~1 小时前
React Ref 指南:原理、实现与实践
前端·javascript·react.js
teeeeeeemo1 小时前
回调函数 vs Promise vs async/await区别
开发语言·前端·javascript·笔记
贵沫末1 小时前
React——基础
前端·react.js·前端框架
aklry2 小时前
uniapp三步完成一维码的生成
前端·vue.js