eslint-plugin-aurora-999 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # eslint-plugin-aurora
2
+
3
+ Aurora 项目 ESLint 插件:提供一套可复用的基础 Flat Config(JS/TS/import),并附带若干 Aurora 约定的结构/语义类自定义规则。
4
+
5
+ - 运行环境:Node.js >= 18
6
+ - ESLint:v9+(Flat Config)
7
+
8
+ ## 安装
9
+
10
+ ```bash
11
+ npm i -D eslint eslint-plugin-aurora
12
+ ```
13
+
14
+ 说明:本插件内部会用到 `@eslint/js`、`typescript-eslint`、`eslint-plugin-import`、`globals` 等依赖;安装本插件时会一并安装。
15
+
16
+ 如果你需要让 `eslint-plugin-import` 的 TypeScript 解析/路径解析更准确,建议项目也安装:
17
+
18
+ ```bash
19
+ npm i -D eslint-import-resolver-typescript
20
+ ```
21
+
22
+ ## 使用(推荐:Flat Config)
23
+
24
+ 在项目根目录创建/修改 `eslint.config.js`:
25
+
26
+ ```js
27
+ const aurora = require("eslint-plugin-aurora");
28
+
29
+ const baseDir = __dirname;
30
+
31
+ module.exports = [
32
+ // 1) 基础规则集(JS/TS/import)
33
+ ...aurora.configs.recommended(baseDir),
34
+
35
+ // 2) 选择性启用 Aurora 自定义规则
36
+ {
37
+ plugins: { aurora },
38
+ rules: {
39
+ "aurora/base-check": "error",
40
+ "aurora/trial-check": "error",
41
+ "aurora/scene-check": "error",
42
+ "aurora/prefab-check": "error",
43
+ },
44
+ },
45
+ ];
46
+ ```
47
+
48
+ 然后执行:
49
+
50
+ ```bash
51
+ npx eslint .
52
+ ```
53
+
54
+ ## configs
55
+
56
+ ### `configs.recommended(baseDir)`
57
+
58
+ 生成基础 Flat Config 数组,包含:
59
+
60
+ - `@eslint/js` 的 recommended
61
+ - `typescript-eslint`:
62
+ - 若 `baseDir/tsconfig.json` 存在,则启用 type-aware 的 `recommendedTypeChecked`
63
+ - 否则降级为 `recommended`
64
+ - `eslint-plugin-import` 的 recommended + 常用 resolver/settings
65
+
66
+ > `baseDir` 必须传项目根目录(用于定位 `tsconfig.json`)。
67
+
68
+ ## rules
69
+
70
+ - `aurora/base-check`
71
+ - 禁止 `console.log/debug/info/trace`
72
+ - 禁止 `debugger`
73
+ - `aurora/trial-check`
74
+ - 仅对 `trialGeneration/**/trialservice.ts` 生效
75
+ - 约束 `TrialGenerator` 子类、`generate()` 方法以及返回值包含 `target_answer`
76
+ - `aurora/scene-check`
77
+ - 仅对 `*scene.ts` 生效
78
+ - 约束 Phaser/BaseScene/NodeConfig 的导入与 `getSceneConfig()` 返回
79
+ - `aurora/prefab-check`
80
+ - 仅对 `*prefab.ts` 生效
81
+ - 约束 Phaser/phaser-kit 导入、`PrefabConfig` 结构,以及 GameObjectFactory remove/register
82
+
83
+ ## 目录结构
84
+
85
+ - `lib/index.js`:插件入口(导出 `rules` 与 `configs`)
86
+ - `lib/configs/base-config.js`:基础 Flat Config 生成函数
87
+ - `lib/rules/*.js`:Aurora 自定义规则实现
88
+
89
+ ## 备注
90
+
91
+ - 更详细的背景与设计说明见:`设计.md`
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+
3
+ const path = require("path");
4
+ const fs = require("fs");
5
+ const globals = require("globals");
6
+ const js = require("@eslint/js");
7
+ const tseslint = require("typescript-eslint");
8
+ const importPlugin = require("eslint-plugin-import");
9
+
10
+ /**
11
+ * 生成基础 ESLint 配置(JS/TS + import 语义规则)。
12
+ *
13
+ * 注意:这里返回的是 ESLint flat config 数组(用于 ESLint v9+)。
14
+ * @param {string} baseDir - 项目根目录
15
+ */
16
+ function eslintBaseConfig(baseDir) {
17
+ const tsconfigPath = path.join(baseDir, "tsconfig.json");
18
+ const hasTsConfig = fs.existsSync(tsconfigPath);
19
+
20
+ const typescriptRulesToDisable = {
21
+ "@typescript-eslint/no-unused-vars": "off",
22
+ "no-unused-vars": "off",
23
+ "@typescript-eslint/explicit-module-boundary-types": "off",
24
+ "@typescript-eslint/no-explicit-any": "off",
25
+ };
26
+
27
+ const baseConfig = {
28
+ languageOptions: {
29
+ ecmaVersion: 2022,
30
+ sourceType: "module",
31
+ globals: {
32
+ ...globals.browser,
33
+ ...globals.node,
34
+ ...globals.jest,
35
+ },
36
+ },
37
+ rules: {
38
+ ...js.configs.recommended.rules,
39
+ "no-unused-vars": "off",
40
+ "no-console": "warn",
41
+ semi: ["error", "always"],
42
+ },
43
+ };
44
+
45
+ const tsConfigs = hasTsConfig
46
+ ? tseslint.configs.recommendedTypeChecked.map((config) => ({
47
+ ...config,
48
+ files: ["**/*.ts", "**/*.tsx"],
49
+ languageOptions: {
50
+ ...config.languageOptions,
51
+ parserOptions: {
52
+ project: [tsconfigPath],
53
+ tsconfigRootDir: baseDir,
54
+ },
55
+ },
56
+ rules: {
57
+ ...(config.rules || {}),
58
+ ...typescriptRulesToDisable,
59
+ },
60
+ }))
61
+ : [
62
+ ...tseslint.configs.recommended,
63
+ {
64
+ files: ["**/*.ts", "**/*.tsx"],
65
+ rules: {
66
+ ...typescriptRulesToDisable,
67
+ },
68
+ },
69
+ ];
70
+
71
+ const importConfig = {
72
+ plugins: {
73
+ import: importPlugin,
74
+ },
75
+ settings: {
76
+ "import/parsers": {
77
+ "@typescript-eslint/parser": [".ts", ".tsx"],
78
+ },
79
+ "import/resolver": {
80
+ typescript: {
81
+ ...(hasTsConfig ? { project: tsconfigPath } : {}),
82
+ },
83
+ node: {
84
+ extensions: [".js", ".jsx", ".ts", ".tsx"],
85
+ },
86
+ },
87
+ "import/extensions": [".js", ".jsx", ".ts", ".tsx"],
88
+ },
89
+ rules: {
90
+ ...importPlugin.configs.recommended.rules,
91
+ "import/no-unresolved": "error",
92
+ "import/extensions": ["error", "never", { js: "never", jsx: "never", ts: "never", tsx: "never" }],
93
+ },
94
+ };
95
+
96
+ return [baseConfig, ...tsConfigs, importConfig];
97
+ }
98
+
99
+ module.exports = {
100
+ eslintBaseConfig,
101
+ };
package/lib/index.js ADDED
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+
3
+ const { eslintBaseConfig } = require("./configs/base-config");
4
+
5
+ module.exports = {
6
+ rules: {
7
+ "base-check": require("./rules/base-check"),
8
+ "trial-check": require("./rules/trial-check"),
9
+ "prefab-check": require("./rules/prefab-check"),
10
+ "scene-check": require("./rules/scene-check"),
11
+ },
12
+ configs: {
13
+ // 注意:这里导出的是一个函数,需要传入项目根目录 baseDir 才能生成 flat config
14
+ recommended: eslintBaseConfig,
15
+ },
16
+ };
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * @fileoverview Aurora 基础检测规则
5
+ * 说明:这里只实现 ESLint rule 本身,不包含 ESLint 配置拼装逻辑。
6
+ */
7
+
8
+ /**
9
+ * @param {import('estree').Expression} callee
10
+ */
11
+ function getConsoleMethodName(callee) {
12
+ if (!callee || callee.type !== "MemberExpression") return;
13
+ if (!callee.object || callee.object.type !== "Identifier" || callee.object.name !== "console") return;
14
+
15
+ if (callee.property?.type === "Identifier") return callee.property.name;
16
+ if (callee.property?.type === "Literal" && typeof callee.property.value === "string") {
17
+ return callee.property.value;
18
+ }
19
+ return;
20
+ }
21
+
22
+ module.exports = {
23
+ meta: {
24
+ type: "problem",
25
+ docs: {
26
+ description: "Aurora 项目基础代码规范检查",
27
+ recommended: true,
28
+ },
29
+ schema: [],
30
+ messages: {
31
+ noConsole: "生产代码中禁止使用 console.{{name}}",
32
+ noDebugger: "生产代码中禁止使用 debugger",
33
+ },
34
+ },
35
+
36
+ /**
37
+ * @param {import('eslint').Rule.RuleContext} context
38
+ */
39
+ create(context) {
40
+ const fileName = context.getFilename?.() || "";
41
+ const lower = typeof fileName === "string" ? fileName.toLowerCase() : "";
42
+
43
+ // 只关注 js/ts 文件;常见产物/依赖目录直接跳过
44
+ const isSupportedExt = /\.(js|jsx|ts|tsx)$/.test(lower);
45
+ if (!isSupportedExt || lower === "<input>") return {};
46
+
47
+ const ignoredParts = ["/node_modules/", "/dist/", "/build/", "/coverage/", "/temp/"];
48
+ for (const part of ignoredParts) {
49
+ if (lower.includes(part)) return {};
50
+ }
51
+
52
+ const bannedConsoleMethods = new Set(["log", "debug", "info", "trace"]);
53
+
54
+ return {
55
+ CallExpression(node) {
56
+ const name = getConsoleMethodName(node.callee);
57
+ if (!name) return;
58
+
59
+ if (bannedConsoleMethods.has(name)) {
60
+ context.report({
61
+ node,
62
+ messageId: "noConsole",
63
+ data: { name },
64
+ });
65
+ }
66
+ },
67
+
68
+ DebuggerStatement(node) {
69
+ context.report({
70
+ node,
71
+ messageId: "noDebugger",
72
+ });
73
+ },
74
+ };
75
+ },
76
+ };
@@ -0,0 +1,449 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * @fileoverview Prefab(*.prefab.ts / *prefab.ts)通用结构 ESLint 规则
5
+ *
6
+ * 说明:
7
+ * - 本规则仅做“语义检查”,不做 import 路径可解析性校验。
8
+ */
9
+
10
+ /**
11
+ * 判断一个 ImportDeclaration 是否是 `import Phaser from 'phaser'`
12
+ * @param {import('estree').ImportDeclaration} node
13
+ */
14
+ function isPhaserDefaultImport(node) {
15
+ if (!node || node.type !== "ImportDeclaration") return false;
16
+ if (!node.source || node.source.type !== "Literal" || node.source.value !== "phaser") return false;
17
+
18
+ const defaultSpecifier = node.specifiers?.find((s) => s.type === "ImportDefaultSpecifier");
19
+ return Boolean(defaultSpecifier && defaultSpecifier.local && defaultSpecifier.local.name === "Phaser");
20
+ }
21
+
22
+ /**
23
+ * 从指定模块的 import 中提取命名导入(忽略 type/value 形式差异)
24
+ * @param {import('estree').ImportDeclaration} node
25
+ * @param {string} sourceValue
26
+ */
27
+ function getNamedImportsFrom(node, sourceValue) {
28
+ if (!node || node.type !== "ImportDeclaration") return [];
29
+ if (!node.source || node.source.type !== "Literal" || node.source.value !== sourceValue) return [];
30
+
31
+ return (node.specifiers || [])
32
+ .filter((s) => s.type === "ImportSpecifier")
33
+ .map((s) => s.imported?.name)
34
+ .filter(Boolean);
35
+ }
36
+
37
+ /**
38
+ * 判断 `Identifier` 的 TS 类型注解是否为指定类型名
39
+ * @param {import('estree').Identifier} id
40
+ * @param {string} typeName
41
+ */
42
+ function hasTSTypeAnnotation(id, typeName) {
43
+ const typeAnnotation = id?.typeAnnotation?.typeAnnotation;
44
+ if (!typeAnnotation) return false;
45
+
46
+ // TSTypeReference: `PrefabConfig`
47
+ if (typeAnnotation.type === "TSTypeReference") {
48
+ return (
49
+ typeAnnotation.typeName?.type === "Identifier" && typeAnnotation.typeName.name === typeName
50
+ );
51
+ }
52
+
53
+ return false;
54
+ }
55
+
56
+ /**
57
+ * 从 ObjectExpression 中读取 `name` / `type` 字段
58
+ * @param {import('estree').ObjectExpression} obj
59
+ */
60
+ function readPrefabConfigFields(obj) {
61
+ /** @type {{hasName?: boolean, nameValue?: string, typeValue?: string}} */
62
+ const result = {};
63
+
64
+ for (const prop of obj.properties || []) {
65
+ // 仅处理普通对象属性:{ name: 'xxx' }
66
+ if (!prop || prop.type !== "Property") continue;
67
+
68
+ const keyName =
69
+ prop.key?.type === "Identifier"
70
+ ? prop.key.name
71
+ : prop.key?.type === "Literal"
72
+ ? String(prop.key.value)
73
+ : undefined;
74
+
75
+ if (!keyName) continue;
76
+
77
+ if (keyName === "name") {
78
+ // name 字段允许为任意表达式,这里只做“存在性”检查
79
+ result.hasName = true;
80
+
81
+ // 仅当为字符串字面量时,才记录具体值(用于后续与 factory key 的强一致校验)
82
+ if (prop.value?.type === "Literal" && typeof prop.value.value === "string") {
83
+ result.nameValue = prop.value.value;
84
+ }
85
+ }
86
+
87
+ if (keyName === "type") {
88
+ if (prop.value?.type === "Literal" && typeof prop.value.value === "string") {
89
+ result.typeValue = prop.value.value;
90
+ }
91
+ }
92
+ }
93
+
94
+ return result;
95
+ }
96
+
97
+ /**
98
+ * 获取 MemberExpression 的调用链(从根到叶)
99
+ * 例如 Phaser.GameObjects.GameObjectFactory.remove => ['Phaser','GameObjects','GameObjectFactory','remove']
100
+ * @param {import('estree').Expression} callee
101
+ */
102
+ function getMemberChain(callee) {
103
+ /** @type {string[]} */
104
+ const parts = [];
105
+
106
+ /** @type {any} */
107
+ let current = callee;
108
+
109
+ while (current && current.type === "MemberExpression") {
110
+ const propertyName =
111
+ current.property?.type === "Identifier"
112
+ ? current.property.name
113
+ : current.property?.type === "Literal"
114
+ ? String(current.property.value)
115
+ : undefined;
116
+
117
+ if (!propertyName) return [];
118
+
119
+ parts.unshift(propertyName);
120
+ current = current.object;
121
+ }
122
+
123
+ // 根节点必须是 Identifier,例如 Phaser
124
+ if (current && current.type === "Identifier") {
125
+ parts.unshift(current.name);
126
+ return parts;
127
+ }
128
+
129
+ return [];
130
+ }
131
+
132
+ /**
133
+ * 判断 CallExpression 的 callee 是否匹配指定成员链
134
+ * @param {import('estree').CallExpression} node
135
+ * @param {string[]} chain
136
+ */
137
+ function isCallOfMemberChain(node, chain) {
138
+ if (!node || node.type !== "CallExpression") return false;
139
+
140
+ const actual = getMemberChain(node.callee);
141
+ if (actual.length !== chain.length) return false;
142
+
143
+ for (let i = 0; i < chain.length; i++) {
144
+ if (actual[i] !== chain[i]) return false;
145
+ }
146
+
147
+ return true;
148
+ }
149
+
150
+ /**
151
+ * 获取一个节点用于排序的 key(优先 range,其次 loc)
152
+ * @param {import('estree').Node} node
153
+ */
154
+ function getOrderKey(node) {
155
+ const rangeStart = Array.isArray(node?.range) ? node.range[0] : undefined;
156
+ if (typeof rangeStart === "number") return rangeStart;
157
+
158
+ const line = node?.loc?.start?.line;
159
+ const column = node?.loc?.start?.column;
160
+ if (typeof line === "number" && typeof column === "number") {
161
+ return line * 1000000 + column;
162
+ }
163
+
164
+ return 0;
165
+ }
166
+
167
+ /**
168
+ * @param {import('eslint').Rule.RuleContext} context
169
+ */
170
+ function reportAtFileStart(context, messageId, data) {
171
+ context.report({
172
+ loc: { line: 1, column: 0 },
173
+ messageId,
174
+ data,
175
+ });
176
+ }
177
+
178
+ module.exports = {
179
+ meta: {
180
+ type: "problem",
181
+ docs: {
182
+ description: "约束 prefab 预制件文件的通用共性结构",
183
+ recommended: false,
184
+ },
185
+ schema: [],
186
+ messages: {
187
+ missingPhaserImport: "必须存在 `import Phaser from 'phaser'`(默认导入名必须为 Phaser)",
188
+ missingKitImports: "必须从 `@aurora.js/phaser-kit` 引入 BasePrefab、PrefabConfig、PrefabProps",
189
+ missingPrefabConfig: "必须存在至少一个 `const xxx: PrefabConfig = { ... }` 配置对象",
190
+ prefabConfigMissingName: "`PrefabConfig` 配置对象必须包含 `name` 字段",
191
+ prefabConfigInvalidType: "`PrefabConfig.type` 必须为字符串字面量 'Prefab'",
192
+ missingDefaultPrefabClass: "必须存在 `export default class Xxx extends BasePrefab`",
193
+ missingGetPrefabConfig: "默认导出 Prefab 类必须实现 `protected getPrefabConfig(): PrefabConfig`",
194
+ invalidGetPrefabConfigSignature: "`getPrefabConfig` 的签名必须为 `protected getPrefabConfig(): PrefabConfig`",
195
+ invalidGetPrefabConfigReturn:
196
+ "`getPrefabConfig()` 必须 `return` 本文件内声明的 PrefabConfig 配置对象标识符",
197
+ missingFactoryRemove: "必须包含 `Phaser.GameObjects.GameObjectFactory.remove('<key>')`",
198
+ missingFactoryRegister: "必须包含 `Phaser.GameObjects.GameObjectFactory.register('<key>', ...)`",
199
+ factoryKeyMismatch: "GameObjectFactory.remove/register 的 key 必须一致,且与 PrefabConfig.name 一致",
200
+ factoryOrderInvalid: "必须先调用 GameObjectFactory.remove,再调用 GameObjectFactory.register",
201
+ },
202
+ },
203
+
204
+ /**
205
+ * @param {import('eslint').Rule.RuleContext} context
206
+ */
207
+ create(context) {
208
+ // 为了避免误报,这里再按文件名做一次兜底过滤(配置层也会按文件模式启用)
209
+ const fileName = context.getFilename?.()?.toLowerCase?.() || "";
210
+ if (!fileName.endsWith("prefab.ts")) {
211
+ return {};
212
+ }
213
+
214
+ /** @type {Set<string>} */
215
+ const auroraKitNamedImports = new Set();
216
+
217
+ /** @type {Map<string, {hasName?: boolean, nameValue?: string, typeValue?: string}>} */
218
+ const prefabConfigMap = new Map();
219
+
220
+ /** @type {string | undefined} */
221
+ let defaultClassName;
222
+
223
+ /** @type {string | undefined} */
224
+ let getPrefabConfigReturnIdentifier;
225
+
226
+ /** @type {{key?: string, loc?: import('estree').SourceLocation, rangeStart?: number}[]} */
227
+ const removeCalls = [];
228
+
229
+ /** @type {{key?: string, loc?: import('estree').SourceLocation, rangeStart?: number}[]} */
230
+ const registerCalls = [];
231
+
232
+ let hasPhaserImport = false;
233
+
234
+ return {
235
+ /**
236
+ * Program 里做一次整文件检查,避免散落到多个 visitor 造成状态难维护
237
+ * @param {import('estree').Program} program
238
+ */
239
+ Program(program) {
240
+ // 1) import 语义检查
241
+ for (const node of program.body || []) {
242
+ if (node.type !== "ImportDeclaration") continue;
243
+
244
+ if (isPhaserDefaultImport(node)) {
245
+ hasPhaserImport = true;
246
+ }
247
+
248
+ for (const name of getNamedImportsFrom(node, "@aurora.js/phaser-kit")) {
249
+ auroraKitNamedImports.add(name);
250
+ }
251
+ }
252
+
253
+ if (!hasPhaserImport) {
254
+ reportAtFileStart(context, "missingPhaserImport");
255
+ }
256
+
257
+ const hasBasePrefab = auroraKitNamedImports.has("BasePrefab");
258
+ const hasPrefabConfig = auroraKitNamedImports.has("PrefabConfig");
259
+ const hasPrefabProps = auroraKitNamedImports.has("PrefabProps");
260
+ if (!hasBasePrefab || !hasPrefabConfig || !hasPrefabProps) {
261
+ reportAtFileStart(context, "missingKitImports");
262
+ }
263
+
264
+ // 2) 扫描变量声明:PrefabConfig 配置对象
265
+ for (const node of program.body || []) {
266
+ if (node.type !== "VariableDeclaration") continue;
267
+
268
+ for (const declarator of node.declarations || []) {
269
+ if (declarator.type !== "VariableDeclarator") continue;
270
+ if (declarator.id?.type !== "Identifier") continue;
271
+
272
+ // const xxx: PrefabConfig = { ... }
273
+ if (!hasTSTypeAnnotation(declarator.id, "PrefabConfig")) continue;
274
+
275
+ if (declarator.init?.type !== "ObjectExpression") {
276
+ // 允许先只认 ObjectExpression,避免复杂表达式误报
277
+ continue;
278
+ }
279
+
280
+ const fields = readPrefabConfigFields(declarator.init);
281
+ prefabConfigMap.set(declarator.id.name, fields);
282
+
283
+ if (!fields.hasName) {
284
+ context.report({ node: declarator, messageId: "prefabConfigMissingName" });
285
+ }
286
+ if (fields.typeValue !== "Prefab") {
287
+ context.report({ node: declarator, messageId: "prefabConfigInvalidType" });
288
+ }
289
+ }
290
+ }
291
+
292
+ if (prefabConfigMap.size === 0) {
293
+ reportAtFileStart(context, "missingPrefabConfig");
294
+ }
295
+
296
+ // 3) 扫描 export default class 是否继承 BasePrefab,并找 getPrefabConfig 的 return
297
+ for (const node of program.body || []) {
298
+ if (node.type !== "ExportDefaultDeclaration") continue;
299
+
300
+ const declaration = node.declaration;
301
+ if (!declaration || declaration.type !== "ClassDeclaration") continue;
302
+
303
+ const superClass = declaration.superClass;
304
+ const isExtendsBasePrefab =
305
+ superClass && superClass.type === "Identifier" && superClass.name === "BasePrefab";
306
+
307
+ if (!isExtendsBasePrefab) continue;
308
+
309
+ defaultClassName = declaration.id?.name;
310
+
311
+ // 找 getPrefabConfig 方法
312
+ const classBody = declaration.body?.body || [];
313
+ const getPrefabConfigMethod = classBody.find(
314
+ (m) =>
315
+ m.type === "MethodDefinition" &&
316
+ m.key?.type === "Identifier" &&
317
+ m.key.name === "getPrefabConfig"
318
+ );
319
+
320
+ if (!getPrefabConfigMethod || getPrefabConfigMethod.type !== "MethodDefinition") {
321
+ continue;
322
+ }
323
+
324
+ // 要求签名为:protected getPrefabConfig(): PrefabConfig
325
+ const accessibility = /** @type {any} */ (getPrefabConfigMethod).accessibility;
326
+ const returnType = /** @type {any} */ (getPrefabConfigMethod).value?.returnType
327
+ ?.typeAnnotation;
328
+ const isReturnPrefabConfig =
329
+ returnType?.type === "TSTypeReference" &&
330
+ returnType.typeName?.type === "Identifier" &&
331
+ returnType.typeName.name === "PrefabConfig";
332
+
333
+ if (accessibility !== "protected" || !isReturnPrefabConfig) {
334
+ context.report({
335
+ node: getPrefabConfigMethod,
336
+ messageId: "invalidGetPrefabConfigSignature",
337
+ });
338
+ }
339
+
340
+ // 获取返回值标识符:return xxx
341
+ const bodyStatements = /** @type {any} */ (getPrefabConfigMethod).value?.body?.body || [];
342
+ for (const st of bodyStatements) {
343
+ if (st.type !== "ReturnStatement") continue;
344
+ if (st.argument?.type === "Identifier") {
345
+ getPrefabConfigReturnIdentifier = st.argument.name;
346
+ break;
347
+ }
348
+ }
349
+ }
350
+
351
+ if (!defaultClassName) {
352
+ reportAtFileStart(context, "missingDefaultPrefabClass");
353
+ }
354
+
355
+ if (!getPrefabConfigReturnIdentifier) {
356
+ reportAtFileStart(context, "missingGetPrefabConfig");
357
+ }
358
+
359
+ if (getPrefabConfigReturnIdentifier && !prefabConfigMap.has(getPrefabConfigReturnIdentifier)) {
360
+ reportAtFileStart(context, "invalidGetPrefabConfigReturn");
361
+ }
362
+
363
+ // 4) 扫描 remove / register 调用
364
+ // 说明:不要求一定“文件末尾”,但要求 remove 在 register 之前
365
+ /** @type {import('estree').Node[]} */
366
+ const stack = [...(program.body || [])];
367
+
368
+ while (stack.length > 0) {
369
+ const current = stack.pop();
370
+ if (!current) continue;
371
+
372
+ // 简易 DFS:把子节点推进栈
373
+ for (const value of Object.values(current)) {
374
+ if (!value) continue;
375
+ if (Array.isArray(value)) {
376
+ for (const item of value) {
377
+ if (item && typeof item.type === "string") stack.push(item);
378
+ }
379
+ } else if (value && typeof value.type === "string") {
380
+ stack.push(value);
381
+ }
382
+ }
383
+
384
+ if (current.type !== "CallExpression") continue;
385
+
386
+ const call = current;
387
+
388
+ if (isCallOfMemberChain(call, ["Phaser", "GameObjects", "GameObjectFactory", "remove"])) {
389
+ const firstArg = call.arguments?.[0];
390
+ if (firstArg?.type === "Literal" && typeof firstArg.value === "string") {
391
+ removeCalls.push({
392
+ key: firstArg.value,
393
+ loc: call.loc,
394
+ rangeStart: getOrderKey(call),
395
+ });
396
+ }
397
+ }
398
+
399
+ if (isCallOfMemberChain(call, ["Phaser", "GameObjects", "GameObjectFactory", "register"])) {
400
+ const firstArg = call.arguments?.[0];
401
+ if (firstArg?.type === "Literal" && typeof firstArg.value === "string") {
402
+ registerCalls.push({
403
+ key: firstArg.value,
404
+ loc: call.loc,
405
+ rangeStart: getOrderKey(call),
406
+ });
407
+ }
408
+ }
409
+ }
410
+
411
+ // 5) 规则校验:remove/register 必须存在
412
+ if (removeCalls.length === 0) {
413
+ reportAtFileStart(context, "missingFactoryRemove");
414
+ }
415
+ if (registerCalls.length === 0) {
416
+ reportAtFileStart(context, "missingFactoryRegister");
417
+ }
418
+
419
+ if (removeCalls.length > 0 && registerCalls.length > 0) {
420
+ const firstRemove = removeCalls.sort((a, b) => (a.rangeStart || 0) - (b.rangeStart || 0))[0];
421
+ const firstRegister = registerCalls
422
+ .sort((a, b) => (a.rangeStart || 0) - (b.rangeStart || 0))[0];
423
+
424
+ // 先 remove 后 register
425
+ if ((firstRemove.rangeStart || 0) > (firstRegister.rangeStart || 0)) {
426
+ reportAtFileStart(context, "factoryOrderInvalid");
427
+ }
428
+
429
+ // key 必须一致
430
+ const removeKey = firstRemove.key;
431
+ const registerKey = firstRegister.key;
432
+ if (!removeKey || !registerKey || removeKey !== registerKey) {
433
+ reportAtFileStart(context, "factoryKeyMismatch");
434
+ } else {
435
+ // 且必须等于 PrefabConfig.name
436
+ const configIdentifier = getPrefabConfigReturnIdentifier;
437
+ const config = configIdentifier ? prefabConfigMap.get(configIdentifier) : undefined;
438
+ const configName = config?.nameValue;
439
+
440
+ // 仅当能够静态解析出 name 的字符串字面量时,才强校验 key 与 name 的一致性
441
+ if (typeof configName === "string" && configName !== removeKey) {
442
+ reportAtFileStart(context, "factoryKeyMismatch");
443
+ }
444
+ }
445
+ }
446
+ },
447
+ };
448
+ },
449
+ };
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * @fileoverview Scene(*.scene.ts / *scene.ts)通用结构 ESLint 规则
5
+ *
6
+ * 说明:
7
+ * - 本规则仅做“语义检查”,不做 import 路径可解析性校验。
8
+ */
9
+
10
+ /**
11
+ * 判断一个 ImportDeclaration 是否是 `import Phaser from 'phaser'`
12
+ * @param {import('estree').ImportDeclaration} node
13
+ */
14
+ function isPhaserDefaultImport(node) {
15
+ if (!node || node.type !== "ImportDeclaration") return false;
16
+ if (!node.source || node.source.type !== "Literal" || node.source.value !== "phaser") return false;
17
+
18
+ const defaultSpecifier = node.specifiers?.find((s) => s.type === "ImportDefaultSpecifier");
19
+ return Boolean(defaultSpecifier && defaultSpecifier.local && defaultSpecifier.local.name === "Phaser");
20
+ }
21
+
22
+ /**
23
+ * 从指定模块的 import 中提取命名导入(忽略 type/value 形式差异)
24
+ * @param {import('estree').ImportDeclaration} node
25
+ * @param {string} sourceValue
26
+ */
27
+ function getNamedImportsFrom(node, sourceValue) {
28
+ if (!node || node.type !== "ImportDeclaration") return [];
29
+ if (!node.source || node.source.type !== "Literal" || node.source.value !== sourceValue) return [];
30
+
31
+ return (node.specifiers || [])
32
+ .filter((s) => s.type === "ImportSpecifier")
33
+ .map((s) => s.imported?.name)
34
+ .filter(Boolean);
35
+ }
36
+
37
+ /**
38
+ * 判断 Identifier 的 TS 类型注解是否为指定类型名
39
+ * @param {import('estree').Identifier} id
40
+ * @param {string} typeName
41
+ */
42
+ function hasTSTypeAnnotation(id, typeName) {
43
+ const typeAnnotation = id?.typeAnnotation?.typeAnnotation;
44
+ if (!typeAnnotation) return false;
45
+
46
+ if (typeAnnotation.type === "TSTypeReference") {
47
+ return (
48
+ typeAnnotation.typeName?.type === "Identifier" && typeAnnotation.typeName.name === typeName
49
+ );
50
+ }
51
+
52
+ return false;
53
+ }
54
+
55
+ /**
56
+ * @param {import('eslint').Rule.RuleContext} context
57
+ */
58
+ function reportAtFileStart(context, messageId, data) {
59
+ context.report({
60
+ loc: { line: 1, column: 0 },
61
+ messageId,
62
+ data,
63
+ });
64
+ }
65
+
66
+ module.exports = {
67
+ meta: {
68
+ type: "problem",
69
+ docs: {
70
+ description: "约束 scene 场景文件的通用共性结构",
71
+ recommended: false,
72
+ },
73
+ schema: [],
74
+ messages: {
75
+ missingPhaserImport: "必须存在 `import Phaser from 'phaser'`(默认导入名必须为 Phaser)",
76
+ missingBaseSceneImports: "必须从 `src/kit/BaseScene` 引入 BaseScene 与 NodeConfig",
77
+ missingNodeConfig: "必须存在至少一个 `const xxx: NodeConfig = { ... }` 配置对象",
78
+ missingDefaultSceneClass: "必须存在 `export default class Xxx extends BaseScene`",
79
+ missingGetSceneConfig: "默认导出 Scene 类必须实现 `protected getSceneConfig(): NodeConfig`",
80
+ invalidGetSceneConfigSignature: "`getSceneConfig` 的签名必须为 `protected getSceneConfig(): NodeConfig`",
81
+ invalidGetSceneConfigReturn: "`getSceneConfig()` 必须 `return` 本文件内声明的 NodeConfig 配置对象标识符",
82
+ },
83
+ },
84
+
85
+ /**
86
+ * @param {import('eslint').Rule.RuleContext} context
87
+ */
88
+ create(context) {
89
+ const fileName = context.getFilename?.()?.toLowerCase?.() || "";
90
+ if (!fileName.endsWith("scene.ts")) {
91
+ return {};
92
+ }
93
+
94
+ /** @type {Set<string>} */
95
+ const baseSceneNamedImports = new Set();
96
+
97
+ /** @type {Set<string>} */
98
+ const nodeConfigIdentifiers = new Set();
99
+
100
+ /** @type {string | undefined} */
101
+ let defaultSceneClassName;
102
+
103
+ /** @type {string | undefined} */
104
+ let getSceneConfigReturnIdentifier;
105
+
106
+ let hasPhaserImport = false;
107
+
108
+ return {
109
+ /**
110
+ * @param {import('estree').Program} program
111
+ */
112
+ Program(program) {
113
+ // 1) import 语义检查
114
+ for (const node of program.body || []) {
115
+ if (node.type !== "ImportDeclaration") continue;
116
+
117
+ if (isPhaserDefaultImport(node)) {
118
+ hasPhaserImport = true;
119
+ }
120
+
121
+ for (const name of getNamedImportsFrom(node, "src/kit/BaseScene")) {
122
+ baseSceneNamedImports.add(name);
123
+ }
124
+ }
125
+
126
+ if (!hasPhaserImport) {
127
+ reportAtFileStart(context, "missingPhaserImport");
128
+ }
129
+
130
+ const hasBaseScene = baseSceneNamedImports.has("BaseScene");
131
+ const hasNodeConfig = baseSceneNamedImports.has("NodeConfig");
132
+ if (!hasBaseScene || !hasNodeConfig) {
133
+ reportAtFileStart(context, "missingBaseSceneImports");
134
+ }
135
+
136
+ // 2) 扫描 NodeConfig 配置对象
137
+ for (const node of program.body || []) {
138
+ if (node.type !== "VariableDeclaration") continue;
139
+
140
+ for (const declarator of node.declarations || []) {
141
+ if (declarator.type !== "VariableDeclarator") continue;
142
+ if (declarator.id?.type !== "Identifier") continue;
143
+
144
+ if (!hasTSTypeAnnotation(declarator.id, "NodeConfig")) continue;
145
+
146
+ // 只记录标识符名即可
147
+ nodeConfigIdentifiers.add(declarator.id.name);
148
+ }
149
+ }
150
+
151
+ if (nodeConfigIdentifiers.size === 0) {
152
+ reportAtFileStart(context, "missingNodeConfig");
153
+ }
154
+
155
+ // 3) 扫描默认导出类,继承 BaseScene,并找到 getSceneConfig return
156
+ for (const node of program.body || []) {
157
+ if (node.type !== "ExportDefaultDeclaration") continue;
158
+
159
+ const declaration = node.declaration;
160
+ if (!declaration || declaration.type !== "ClassDeclaration") continue;
161
+
162
+ const superClass = declaration.superClass;
163
+ const isExtendsBaseScene =
164
+ superClass && superClass.type === "Identifier" && superClass.name === "BaseScene";
165
+
166
+ if (!isExtendsBaseScene) continue;
167
+
168
+ defaultSceneClassName = declaration.id?.name;
169
+
170
+ // 找 getSceneConfig 方法
171
+ const classBody = declaration.body?.body || [];
172
+ const getSceneConfigMethod = classBody.find(
173
+ (m) =>
174
+ m.type === "MethodDefinition" &&
175
+ m.key?.type === "Identifier" &&
176
+ m.key.name === "getSceneConfig"
177
+ );
178
+
179
+ if (!getSceneConfigMethod || getSceneConfigMethod.type !== "MethodDefinition") {
180
+ continue;
181
+ }
182
+
183
+ // 要求签名为:protected getSceneConfig(): NodeConfig
184
+ const accessibility = /** @type {any} */ (getSceneConfigMethod).accessibility;
185
+ const returnType = /** @type {any} */ (getSceneConfigMethod).value?.returnType
186
+ ?.typeAnnotation;
187
+ const isReturnNodeConfig =
188
+ returnType?.type === "TSTypeReference" &&
189
+ returnType.typeName?.type === "Identifier" &&
190
+ returnType.typeName.name === "NodeConfig";
191
+
192
+ if (accessibility !== "protected" || !isReturnNodeConfig) {
193
+ context.report({
194
+ node: getSceneConfigMethod,
195
+ messageId: "invalidGetSceneConfigSignature",
196
+ });
197
+ }
198
+
199
+ // 尝试获取返回标识符:return xxx
200
+ const bodyStatements = /** @type {any} */ (getSceneConfigMethod).value?.body?.body || [];
201
+ for (const st of bodyStatements) {
202
+ if (st.type !== "ReturnStatement") continue;
203
+ if (st.argument?.type === "Identifier") {
204
+ getSceneConfigReturnIdentifier = st.argument.name;
205
+ break;
206
+ }
207
+ }
208
+ }
209
+
210
+ if (!defaultSceneClassName) {
211
+ reportAtFileStart(context, "missingDefaultSceneClass");
212
+ }
213
+
214
+ if (!getSceneConfigReturnIdentifier) {
215
+ reportAtFileStart(context, "missingGetSceneConfig");
216
+ }
217
+
218
+ if (getSceneConfigReturnIdentifier && !nodeConfigIdentifiers.has(getSceneConfigReturnIdentifier)) {
219
+ reportAtFileStart(context, "invalidGetSceneConfigReturn");
220
+ }
221
+ },
222
+ };
223
+ },
224
+ };
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * @fileoverview 校验 trialservice.ts 规范的 ESLint 规则
5
+ * 仅在匹配到 trialGeneration/<any>/trialservice.ts 时生效
6
+ */
7
+
8
+ const WINDOWS_SEPARATOR = "\\";
9
+ const POSIX_SEPARATOR = "/";
10
+
11
+ const isInTrialGeneration = (fileName) =>
12
+ fileName.includes(`${POSIX_SEPARATOR}trialGeneration${POSIX_SEPARATOR}`) ||
13
+ fileName.includes(`${WINDOWS_SEPARATOR}trialGeneration${WINDOWS_SEPARATOR}`);
14
+
15
+ // 约定:Trial 服务文件名固定为 trialservice.ts(不支持大小写变体)
16
+ const isTrialServiceFile = (fileName) => fileName.toLowerCase().endsWith("trialservice.ts");
17
+
18
+ const getBaseName = (fileName) => fileName.split(/[\\/]/).pop();
19
+
20
+ /**
21
+ * 辅助函数:检查 generate 方法的返回值
22
+ * @param {import('estree').MethodDefinition} methodNode - generate 方法 AST 节点
23
+ * @param {import('eslint').Rule.RuleContext} context - ESLint 上下文
24
+ */
25
+ const checkGenerateMethodReturn = (methodNode, context) => {
26
+ const body = methodNode.value?.body;
27
+ if (!body || body.type !== "BlockStatement") return;
28
+
29
+ const returnStatements = body.body.filter((statement) => statement.type === "ReturnStatement");
30
+
31
+ returnStatements.forEach((returnStatement) => {
32
+ const argument = returnStatement.argument;
33
+
34
+ if (argument && argument.type === "ObjectExpression") {
35
+ const hasTargetAnswer = argument.properties.some((prop) => {
36
+ if (!prop || prop.type !== "Property") return false;
37
+ if (!prop.key) return false;
38
+ return prop.key.name === "target_answer" || prop.key.value === "target_answer";
39
+ });
40
+
41
+ if (!hasTargetAnswer) {
42
+ context.report({
43
+ node: returnStatement,
44
+ messageId: "noTargetAnswer",
45
+ });
46
+ }
47
+ }
48
+ });
49
+ };
50
+
51
+ module.exports = {
52
+ meta: {
53
+ type: "problem",
54
+ docs: {
55
+ description: "约束 trialservice.ts 的实现规范",
56
+ recommended: false,
57
+ },
58
+ schema: [],
59
+ messages: {
60
+ invalidFileName: "试题服务文件必须命名为 trialservice.ts (当前: {{name}})",
61
+ noGeneratorClass: "必须定义一个继承自 TrialGenerator 的类",
62
+ noGenerateMethod: "Generator 类必须实现 generate 方法",
63
+ noTargetAnswer: "generate 方法返回值必须包含 target_answer 字段",
64
+ },
65
+ },
66
+
67
+ create(context) {
68
+ const fileName = context.getFilename?.() || "";
69
+
70
+ // 非目标目录或非目标文件直接跳过
71
+ if (!fileName || fileName === "<input>" || !isInTrialGeneration(fileName) || !isTrialServiceFile(fileName)) {
72
+ return {};
73
+ }
74
+
75
+ let hasGeneratorClass = false;
76
+
77
+ return {
78
+ Program() {
79
+ const baseName = getBaseName(fileName) || "";
80
+ if (baseName !== "trialservice.ts") {
81
+ context.report({
82
+ loc: { line: 1, column: 0 },
83
+ messageId: "invalidFileName",
84
+ data: { name: baseName },
85
+ });
86
+ }
87
+ },
88
+
89
+ ClassDeclaration(node) {
90
+ if (
91
+ node.superClass &&
92
+ node.superClass.type === "Identifier" &&
93
+ node.superClass.name === "TrialGenerator"
94
+ ) {
95
+ hasGeneratorClass = true;
96
+
97
+ const generateMethod = node.body.body.find(
98
+ (element) =>
99
+ element.type === "MethodDefinition" &&
100
+ element.key?.type === "Identifier" &&
101
+ element.key.name === "generate"
102
+ );
103
+
104
+ if (!generateMethod) {
105
+ context.report({
106
+ node: node.id || node,
107
+ messageId: "noGenerateMethod",
108
+ });
109
+ return;
110
+ }
111
+
112
+ checkGenerateMethodReturn(generateMethod, context);
113
+ }
114
+ },
115
+
116
+ "Program:exit"() {
117
+ if (!hasGeneratorClass) {
118
+ context.report({
119
+ loc: { line: 1, column: 0 },
120
+ messageId: "noGeneratorClass",
121
+ });
122
+ }
123
+ },
124
+ };
125
+ },
126
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "eslint-plugin-aurora-999",
3
+ "version": "0.1.0",
4
+ "description": "Aurora ESLint plugin (rules) + flat config (recommended)",
5
+ "main": "lib/index.js",
6
+ "exports": {
7
+ ".": "./lib/index.js"
8
+ },
9
+ "files": [
10
+ "lib"
11
+ ],
12
+ "keywords": [
13
+ "eslint",
14
+ "eslintplugin",
15
+ "eslint-plugin",
16
+ "aurora"
17
+ ],
18
+ "license": "MIT",
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "peerDependencies": {
23
+ "eslint": "^9.0.0"
24
+ },
25
+ "dependencies": {
26
+ "@eslint/js": "^9.0.0",
27
+ "eslint-import-resolver-typescript": "^3.6.0",
28
+ "eslint-plugin-import": "^2.29.0",
29
+ "globals": "^15.0.0",
30
+ "typescript-eslint": "^8.0.0"
31
+ }
32
+ }