Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/eslint/compat-headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const compatMap = {
storageName: [],
"early-start": [],
"require-css": [],
allFrames: [],
},
};

Expand Down
14 changes: 7 additions & 7 deletions packages/eslint/linter-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ const config = {
rules: {
"constructor-super": ["error"],
"for-direction": ["error"],
"getter-return": ["error"],
"getter-return": ["warn"], // implicitly means return undefined
"no-async-promise-executor": ["error"],
"no-case-declarations": ["error"],
"no-class-assign": ["error"],
"no-compare-neg-zero": ["error"],
"no-cond-assign": ["error"],
"no-cond-assign": ["warn"], // this is common writing style in JavaScript
"no-const-assign": ["error"],
"no-constant-condition": ["error"],
"no-control-regex": ["error"],
Expand All @@ -37,15 +37,15 @@ const config = {
"no-dupe-else-if": ["error"],
"no-dupe-keys": ["error"],
"no-duplicate-case": ["error"],
"no-empty": ["error"],
"no-empty": ["error", { allowEmptyCatch: true }],
"no-empty-character-class": ["error"],
"no-empty-pattern": ["error"],
"no-ex-assign": ["error"],
"no-extra-boolean-cast": ["error"],
"no-extra-semi": ["error"],
"no-fallthrough": ["error"],
"no-func-assign": ["error"],
"no-global-assign": ["error"],
"no-global-assign": ["warn"], // we always modify global variable in UserScript
"no-import-assign": ["error"],
"no-inner-declarations": ["error"],
"no-invalid-regexp": ["error"],
Expand All @@ -58,10 +58,10 @@ const config = {
"no-obj-calls": ["error"],
"no-octal": ["error"],
"no-prototype-builtins": ["error"],
"no-redeclare": ["error"],
"no-redeclare": ["error", { builtinGlobals: false }],
"no-regex-spaces": ["error"],
"no-self-assign": ["error"],
"no-setter-return": ["error"],
"no-setter-return": ["warn"], // sometimes developers like to return true in setter
"no-shadow-restricted-names": ["error"],
"no-sparse-arrays": ["error"],
"no-this-before-super": ["error"],
Expand All @@ -75,7 +75,7 @@ const config = {
"no-unused-vars": ["warn"],
"no-useless-backreference": ["error"],
"no-useless-catch": ["error"],
"no-useless-escape": ["error"],
"no-useless-escape": ["error", { allowRegexCharacters: ["-", "&", "/"] }],
"no-with": ["error"],
"require-yield": ["error"],
"use-isnan": ["error"],
Expand Down
24 changes: 23 additions & 1 deletion src/linter.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,30 @@ const { rules } = require("eslint-plugin-userscripts");

const linter = new Linter({ configType: "eslintrc" });

// ScriptCat 不适用 - 有必要存在的用法
const omitKeys = new Set([
// 不是所有 @include 都要改为 @match
"better-use-match",
// 不是所有语言的 @name 都要放在最前
"require-name",
// ScriptCat 不用指定 ==UserScript== 放最前。在 ==UserScript== 前面可以写其他注释
"no-invalid-metadata",
]);

// 额外定义 userscripts 规则
const formatRules = Object.fromEntries(Object.entries(rules).map(([key, metas]) => ["userscripts/" + key, metas]));
const formatRules = Object.fromEntries(
Object.entries(rules).map(([key, metas]) => [
"userscripts/" + key,
omitKeys.has(key)
? {
meta: {},
create() {
return { CallExpression() {} };
},
}
: metas,
])
);
linter.defineRules(formatRules as any);

const getRules = linter.getRules();
Expand Down
11 changes: 8 additions & 3 deletions src/pages/components/CodeEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, { useEffect, useImperativeHandle, useRef, useState } from "react";
import { systemConfig } from "@App/pages/store/global";
import { LinterWorkerController, registerEditor } from "@App/pkg/utils/monaco-editor";
import { fnPlaceHolder } from "@App/pages/store/AppContext";
import { clearModelEslintFixes, getModelEslintFixKey } from "@App/pkg/utils/monaco-editor/eslintFixCache";

fnPlaceHolder.setEditorTheme = (theme: string) => editor.setTheme(theme);

Expand Down Expand Up @@ -259,13 +260,13 @@ const CodeEditor = React.forwardRef<{ editor: editor.IStandaloneCodeEditor | und

editor.setModelMarkers(model, "ESLint", message.markers);

// 更新 eslint-fix 快取(每次替换整个 map,避免已修复问题的过期条目残留)
// 更新当前 model 的 eslint-fix 快取,避免多个脚本编辑器互相覆盖 quick-fix。
const eslintFixMap = (window.MonacoEnvironment as any)?.eslintFixMap;
if (eslintFixMap) {
eslintFixMap.clear();
clearModelEslintFixes(eslintFixMap, model);
message.markers.forEach((m: TMarker) => {
if (m.fix) {
const key = `${m.code.value}|${m.startLineNumber}|${m.endLineNumber}|${m.startColumn}|${m.endColumn}`;
const key = getModelEslintFixKey(model, m.code.value, m);
eslintFixMap.set(key, m.fix);
}
});
Expand All @@ -288,6 +289,10 @@ const CodeEditor = React.forwardRef<{ editor: editor.IStandaloneCodeEditor | und
timer = null;
}
changeListener.dispose();
const eslintFixMap = (window.MonacoEnvironment as any)?.eslintFixMap;
if (eslintFixMap) {
clearModelEslintFixes(eslintFixMap, model);
}
LinterWorkerController.hookRemoveListener("message", messageHandler);
};
}, [monacoEditor, enableEslint, eslintConfig, id]);
Expand Down
54 changes: 54 additions & 0 deletions src/pkg/utils/monaco-editor/eslintFixCache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import type { editor } from "monaco-editor";
import { clearModelEslintFixes, getModelEslintFixKey, type EslintFix } from "./eslintFixCache";

const createMockModel = (uri: string): editor.ITextModel =>
({
uri: {
toString: () => uri,
},
}) as editor.ITextModel;

const marker = {
startLineNumber: 1,
endLineNumber: 5,
startColumn: 1,
endColumn: 19,
};

const fix: EslintFix = {
range: {
startLineNumber: 2,
endLineNumber: 2,
startColumn: 9,
endColumn: 10,
},
text: " ",
};

describe("eslint fix cache", () => {
it("uses the model uri in fix keys so identical markers from different editors do not collide", () => {
const modelA = createMockModel("inmemory://model/a");
const modelB = createMockModel("inmemory://model/b");

expect(getModelEslintFixKey(modelA, "userscripts/align-attributes", marker)).not.toBe(
getModelEslintFixKey(modelB, "userscripts/align-attributes", marker)
);
});

it("clears only fixes for the current model", () => {
const modelA = createMockModel("inmemory://model/a");
const modelB = createMockModel("inmemory://model/b");
const map = new Map<string, EslintFix>();
const keyA = getModelEslintFixKey(modelA, "userscripts/align-attributes", marker);
const keyB = getModelEslintFixKey(modelB, "userscripts/align-attributes", marker);

map.set(keyA, fix);
map.set(keyB, fix);

clearModelEslintFixes(map, modelA);

expect(map.has(keyA)).toBe(false);
expect(map.has(keyB)).toBe(true);
});
});
27 changes: 27 additions & 0 deletions src/pkg/utils/monaco-editor/eslintFixCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { editor, IRange } from "monaco-editor";

export type EslintFix = {
range: IRange;
text: string;
};

type EslintFixMarkerPosition = Pick<
editor.IMarkerData,
"startLineNumber" | "endLineNumber" | "startColumn" | "endColumn"
>;

export const getEslintFixKey = (modelUri: string, eslintRuleId: string, marker: EslintFixMarkerPosition) => {
return `${modelUri}|${eslintRuleId}|${marker.startLineNumber}|${marker.endLineNumber}|${marker.startColumn}|${marker.endColumn}`;
};

export const getModelEslintFixKey = (model: editor.ITextModel, eslintRuleId: string, marker: EslintFixMarkerPosition) =>
getEslintFixKey(model.uri.toString(), eslintRuleId, marker);

export const clearModelEslintFixes = (eslintFixMap: Map<string, EslintFix>, model: editor.ITextModel) => {
const prefix = `${model.uri.toString()}|`;
for (const key of eslintFixMap.keys()) {
if (key.startsWith(prefix)) {
eslintFixMap.delete(key);
}
}
};
Loading
Loading