@codyswann/lisa 2.178.5 → 2.179.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/dist/configs/eslint/phaser.d.ts +2 -8
- package/dist/configs/eslint/phaser.d.ts.map +1 -1
- package/dist/configs/eslint/phaser.js +143 -17
- package/dist/configs/eslint/phaser.js.map +1 -1
- package/eslint-plugin-phaser/README.md +19 -0
- package/eslint-plugin-phaser/index.js +35 -0
- package/eslint-plugin-phaser/package.json +10 -0
- package/eslint-plugin-phaser/rules/no-allocation-in-update.js +189 -0
- package/eslint-plugin-phaser/rules/no-create-in-update.js +200 -0
- package/eslint-plugin-phaser/rules/require-shutdown-cleanup.js +191 -0
- package/package.json +5 -3
- package/phaser/package-lisa/package.lisa.json +11 -3
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/skills/analyze-claude-remote/SKILL.md +72 -2
- package/plugins/lisa/skills/generate-claude-remote-build-script/SKILL.md +30 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/analyze-claude-remote/SKILL.md +72 -2
- package/plugins/lisa-agy/skills/generate-claude-remote-build-script/SKILL.md +30 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/skills/analyze-claude-remote/SKILL.md +72 -2
- package/plugins/lisa-copilot/skills/generate-claude-remote-build-script/SKILL.md +30 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/analyze-claude-remote/SKILL.md +72 -2
- package/plugins/lisa-cursor/skills/generate-claude-remote-build-script/SKILL.md +30 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/agents/ops-specialist.md +2 -2
- package/plugins/lisa-expo/skills/ops-check-logs/SKILL.md +8 -2
- package/plugins/lisa-expo/skills/ops-db-ops/SKILL.md +7 -3
- package/plugins/lisa-expo/skills/ops-deploy/SKILL.md +6 -2
- package/plugins/lisa-expo/skills/ops-run-local/SKILL.md +5 -2
- package/plugins/lisa-expo-agy/agents/ops-specialist.md +2 -2
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-agy/skills/ops-check-logs/SKILL.md +8 -2
- package/plugins/lisa-expo-agy/skills/ops-db-ops/SKILL.md +7 -3
- package/plugins/lisa-expo-agy/skills/ops-deploy/SKILL.md +6 -2
- package/plugins/lisa-expo-agy/skills/ops-run-local/SKILL.md +5 -2
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/agents/ops-specialist.agent.md +2 -2
- package/plugins/lisa-expo-copilot/skills/ops-check-logs/SKILL.md +8 -2
- package/plugins/lisa-expo-copilot/skills/ops-db-ops/SKILL.md +7 -3
- package/plugins/lisa-expo-copilot/skills/ops-deploy/SKILL.md +6 -2
- package/plugins/lisa-expo-copilot/skills/ops-run-local/SKILL.md +5 -2
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/agents/ops-specialist.md +2 -2
- package/plugins/lisa-expo-cursor/skills/ops-check-logs/SKILL.md +8 -2
- package/plugins/lisa-expo-cursor/skills/ops-db-ops/SKILL.md +7 -3
- package/plugins/lisa-expo-cursor/skills/ops-deploy/SKILL.md +6 -2
- package/plugins/lisa-expo-cursor/skills/ops-run-local/SKILL.md +5 -2
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/rules/phaser.md +111 -44
- package/plugins/lisa-phaser/skills/phaser-accessibility/SKILL.md +135 -0
- package/plugins/lisa-phaser/skills/phaser-accessibility/agents/openai.yaml +4 -0
- package/plugins/lisa-phaser/skills/phaser-asset-pipeline/SKILL.md +148 -0
- package/plugins/lisa-phaser/skills/phaser-asset-pipeline/agents/openai.yaml +4 -0
- package/plugins/lisa-phaser/skills/phaser-build-deploy/SKILL.md +148 -0
- package/plugins/lisa-phaser/skills/phaser-build-deploy/agents/openai.yaml +4 -0
- package/plugins/lisa-phaser/skills/phaser-i18n/SKILL.md +122 -0
- package/plugins/lisa-phaser/skills/phaser-i18n/agents/openai.yaml +4 -0
- package/plugins/lisa-phaser/skills/phaser-services/SKILL.md +208 -0
- package/plugins/lisa-phaser/skills/phaser-services/agents/openai.yaml +4 -0
- package/plugins/lisa-phaser/skills/phaser-testing/SKILL.md +103 -10
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/skills/phaser-accessibility/SKILL.md +135 -0
- package/plugins/lisa-phaser-agy/skills/phaser-asset-pipeline/SKILL.md +148 -0
- package/plugins/lisa-phaser-agy/skills/phaser-build-deploy/SKILL.md +148 -0
- package/plugins/lisa-phaser-agy/skills/phaser-i18n/SKILL.md +122 -0
- package/plugins/lisa-phaser-agy/skills/phaser-services/SKILL.md +208 -0
- package/plugins/lisa-phaser-agy/skills/phaser-testing/SKILL.md +103 -10
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/rules/phaser.md +111 -44
- package/plugins/lisa-phaser-copilot/skills/phaser-accessibility/SKILL.md +135 -0
- package/plugins/lisa-phaser-copilot/skills/phaser-asset-pipeline/SKILL.md +148 -0
- package/plugins/lisa-phaser-copilot/skills/phaser-build-deploy/SKILL.md +148 -0
- package/plugins/lisa-phaser-copilot/skills/phaser-i18n/SKILL.md +122 -0
- package/plugins/lisa-phaser-copilot/skills/phaser-services/SKILL.md +208 -0
- package/plugins/lisa-phaser-copilot/skills/phaser-testing/SKILL.md +103 -10
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/rules/phaser.mdc +111 -44
- package/plugins/lisa-phaser-cursor/skills/phaser-accessibility/SKILL.md +135 -0
- package/plugins/lisa-phaser-cursor/skills/phaser-asset-pipeline/SKILL.md +148 -0
- package/plugins/lisa-phaser-cursor/skills/phaser-build-deploy/SKILL.md +148 -0
- package/plugins/lisa-phaser-cursor/skills/phaser-i18n/SKILL.md +122 -0
- package/plugins/lisa-phaser-cursor/skills/phaser-services/SKILL.md +208 -0
- package/plugins/lisa-phaser-cursor/skills/phaser-testing/SKILL.md +103 -10
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/agents/ops-specialist.md +1 -1
- package/plugins/lisa-rails-agy/agents/ops-specialist.md +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/agents/ops-specialist.agent.md +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/agents/ops-specialist.md +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/skills/analyze-claude-remote/SKILL.md +72 -2
- package/plugins/src/base/skills/generate-claude-remote-build-script/SKILL.md +30 -1
- package/plugins/src/expo/agents/ops-specialist.md +2 -2
- package/plugins/src/expo/skills/ops-check-logs/SKILL.md +8 -2
- package/plugins/src/expo/skills/ops-db-ops/SKILL.md +7 -3
- package/plugins/src/expo/skills/ops-deploy/SKILL.md +6 -2
- package/plugins/src/expo/skills/ops-run-local/SKILL.md +5 -2
- package/plugins/src/phaser/rules/phaser.md +111 -44
- package/plugins/src/phaser/skills/phaser-accessibility/SKILL.md +135 -0
- package/plugins/src/phaser/skills/phaser-asset-pipeline/SKILL.md +148 -0
- package/plugins/src/phaser/skills/phaser-build-deploy/SKILL.md +148 -0
- package/plugins/src/phaser/skills/phaser-i18n/SKILL.md +122 -0
- package/plugins/src/phaser/skills/phaser-services/SKILL.md +208 -0
- package/plugins/src/phaser/skills/phaser-testing/SKILL.md +103 -10
- package/plugins/src/rails/agents/ops-specialist.md +1 -1
- package/tsconfig/phaser.json +8 -1
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ESLint 9 Flat Config - Phaser Stack
|
|
3
|
-
*
|
|
4
|
-
* Extends Lisa's TypeScript config with Phaser 4 enforcement: bans Phaser 3
|
|
5
|
-
* rendering idioms that were removed in v4 and non-deterministic randomness
|
|
6
|
-
* in game code.
|
|
7
|
-
* @module configs/eslint/phaser
|
|
8
|
-
*/
|
|
9
1
|
import type { Linter } from "eslint";
|
|
10
2
|
import { defaultIgnores, defaultThresholds } from "./typescript.js";
|
|
11
3
|
export { defaultIgnores, defaultThresholds };
|
|
4
|
+
declare const phaserPlugin: any;
|
|
5
|
+
export { phaserPlugin };
|
|
12
6
|
/**
|
|
13
7
|
* Default ignore patterns for Phaser projects.
|
|
14
8
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"phaser.d.ts","sourceRoot":"","sources":["../../../src/configs/eslint/phaser.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"phaser.d.ts","sourceRoot":"","sources":["../../../src/configs/eslint/phaser.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EACL,cAAc,EACd,iBAAiB,EAElB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,CAAC;AAM7C,QAAA,MAAM,YAAY,KAIjB,CAAC;AAEF,OAAO,EAAE,YAAY,EAAE,CAAC;AAExB;;GAEG;AACH,eAAO,MAAM,oBAAoB,UAKhC,CAAC;AA4MF;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAC9B,eAAe,EACf,cAAqC,EACrC,UAA8B,GAC/B,EAAE;IACD,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,UAAU,CAAC,EAAE,OAAO,iBAAiB,CAAC;CACvC,GAAG,MAAM,CAAC,MAAM,EAAE,CAKlB"}
|
|
@@ -1,5 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint 9 Flat Config - Phaser Stack
|
|
3
|
+
*
|
|
4
|
+
* Extends Lisa's TypeScript config with Phaser 4 enforcement:
|
|
5
|
+
* - bans Phaser 3 rendering/API idioms removed in v4,
|
|
6
|
+
* - enforces determinism (no Math.random/Date.now/performance.now in game code),
|
|
7
|
+
* - enforces the architecture boundary (src/logic must be Phaser-free),
|
|
8
|
+
* - bans reusing game.events, shipping physics debug:true, and raw localStorage,
|
|
9
|
+
* - registers eslint-plugin-phaser for the per-frame/lifecycle rules that cannot
|
|
10
|
+
* be expressed as static selectors (no-create-in-update,
|
|
11
|
+
* no-allocation-in-update, require-shutdown-cleanup).
|
|
12
|
+
* @module configs/eslint/phaser
|
|
13
|
+
*/
|
|
14
|
+
import { createRequire } from "module";
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
1
16
|
import { defaultIgnores, defaultThresholds, getTypescriptConfig, } from "./typescript.js";
|
|
2
17
|
export { defaultIgnores, defaultThresholds };
|
|
18
|
+
// Custom plugin — loaded via relative path so it resolves from the Lisa package
|
|
19
|
+
// itself (eslint-plugin-* dirs are in the published files array) rather than the
|
|
20
|
+
// registry. Same pattern as the code-organization plugin in typescript.ts.
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const phaserPlugin = require(fileURLToPath(new URL("../../../eslint-plugin-phaser/index.js", import.meta.url)));
|
|
23
|
+
export { phaserPlugin };
|
|
3
24
|
/**
|
|
4
25
|
* Default ignore patterns for Phaser projects.
|
|
5
26
|
*/
|
|
@@ -50,10 +71,130 @@ const PHASER_3_IDIOM_BANS = [
|
|
|
50
71
|
selector: "NewExpression[callee.object.object.name='Phaser'][callee.object.property.name='Display'][callee.property.name='BitmapMask']",
|
|
51
72
|
message: "Phaser 4 removed BitmapMask. Use the Mask filter.",
|
|
52
73
|
},
|
|
74
|
+
{
|
|
75
|
+
selector: "MemberExpression[property.name='Mesh'][object.property.name='GameObjects']",
|
|
76
|
+
message: "Phaser 4 removed Mesh. Use Sprites/Containers, or SpriteGPULayer for mass rendering.",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
selector: "MemberExpression[property.name='Plane'][object.property.name='GameObjects']",
|
|
80
|
+
message: "Phaser 4 removed Plane. Use Sprites/Containers.",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
selector: "MemberExpression[property.name='Camera3D']",
|
|
84
|
+
message: "Phaser 4 removed Camera3D. There is no 3D camera in v4.",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
selector: "MemberExpression[property.name='Layer3D']",
|
|
88
|
+
message: "Phaser 4 removed Layer3D. There is no 3D layer in v4.",
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
/**
|
|
92
|
+
* Determinism and architecture bans for Phaser game code (src/**, non-test).
|
|
93
|
+
*/
|
|
94
|
+
const PHASER_GAME_BANS = [
|
|
53
95
|
{
|
|
54
96
|
selector: "CallExpression[callee.object.name='Math'][callee.property.name='random']",
|
|
55
97
|
message: "Game code must be deterministic. Use the seeded Phaser.Math.RND (RandomDataGenerator) instead of Math.random().",
|
|
56
98
|
},
|
|
99
|
+
{
|
|
100
|
+
selector: "CallExpression[callee.object.name='Date'][callee.property.name='now']",
|
|
101
|
+
message: "Game code must be deterministic. Use the scene clock (update(time, delta)) or seeded RNG, not Date.now().",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
selector: "CallExpression[callee.object.name='performance'][callee.property.name='now']",
|
|
105
|
+
message: "Game code must be deterministic. Use the scene clock (update(time, delta)), not performance.now().",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
selector: "MemberExpression[property.name='events'][object.property.name='game'][object.object.type='ThisExpression']",
|
|
109
|
+
message: "Do not reuse this.game.events as an app bus — it collides with Phaser internals. Create a dedicated EventsCenter (new Phaser.Events.EventEmitter()) in src/services. See the phaser-services skill.",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
selector: ":matches(Property[key.name='physics'], Property[key.name='arcade'], Property[key.name='matter']) Property[key.name='debug'][value.value=true]",
|
|
113
|
+
message: "Never ship physics debug:true. Gate it behind an env flag (e.g. import.meta.env.DEV).",
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
const LOGIC_IMPORT_MESSAGE = "src/logic must be Phaser-free (pure, testable game logic). Keep all Phaser usage in scenes/objects/services and pass plain data into logic.";
|
|
117
|
+
/**
|
|
118
|
+
* Phaser-specific ESLint overrides layered on top of the TypeScript config.
|
|
119
|
+
* Hoisted to module scope so the factory stays small; they close over the
|
|
120
|
+
* module-level bans, plugin, and boundary message (no per-call parameters).
|
|
121
|
+
*/
|
|
122
|
+
const PHASER_OVERRIDES = [
|
|
123
|
+
// Game code: idiom bans, determinism, localStorage discipline, and the
|
|
124
|
+
// stateful per-frame/lifecycle rules. Test files are excluded.
|
|
125
|
+
{
|
|
126
|
+
files: ["src/**/*.ts"],
|
|
127
|
+
ignores: ["src/**/*.test.ts", "src/**/*.spec.ts"],
|
|
128
|
+
plugins: { phaser: phaserPlugin },
|
|
129
|
+
rules: {
|
|
130
|
+
"no-restricted-syntax": [
|
|
131
|
+
"error",
|
|
132
|
+
...PHASER_3_IDIOM_BANS,
|
|
133
|
+
...PHASER_GAME_BANS,
|
|
134
|
+
],
|
|
135
|
+
"no-restricted-globals": [
|
|
136
|
+
"error",
|
|
137
|
+
{
|
|
138
|
+
name: "localStorage",
|
|
139
|
+
message: "Use the typed SaveService (src/services), not raw localStorage — it centralizes schema versioning and migration. See the phaser-services skill.",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "sessionStorage",
|
|
143
|
+
message: "Use a typed service (src/services), not raw sessionStorage.",
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
"no-restricted-properties": [
|
|
147
|
+
"error",
|
|
148
|
+
{
|
|
149
|
+
object: "window",
|
|
150
|
+
property: "localStorage",
|
|
151
|
+
message: "Use the typed SaveService (src/services), not window.localStorage.",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
object: "window",
|
|
155
|
+
property: "sessionStorage",
|
|
156
|
+
message: "Use a typed service (src/services), not window.sessionStorage.",
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
"phaser/no-create-in-update": "error",
|
|
160
|
+
"phaser/no-allocation-in-update": "error",
|
|
161
|
+
"phaser/require-shutdown-cleanup": "error",
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
// The architecture boundary: pure logic may not import Phaser.
|
|
165
|
+
{
|
|
166
|
+
files: ["src/logic/**/*.ts"],
|
|
167
|
+
ignores: ["src/logic/**/*.test.ts", "src/logic/**/*.spec.ts"],
|
|
168
|
+
rules: {
|
|
169
|
+
"no-restricted-imports": [
|
|
170
|
+
"error",
|
|
171
|
+
{
|
|
172
|
+
paths: [{ name: "phaser", message: LOGIC_IMPORT_MESSAGE }],
|
|
173
|
+
patterns: [
|
|
174
|
+
{ group: ["phaser", "phaser/*"], message: LOGIC_IMPORT_MESSAGE },
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
// Services own the persistence boundary, so they may touch storage directly.
|
|
181
|
+
{
|
|
182
|
+
files: ["src/services/**/*.ts"],
|
|
183
|
+
rules: {
|
|
184
|
+
"no-restricted-globals": "off",
|
|
185
|
+
"no-restricted-properties": "off",
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
// Tests can do anything (mock removed APIs, allocate freely, etc.).
|
|
189
|
+
{
|
|
190
|
+
files: ["tests/**/*.ts", "src/**/*.test.ts", "src/**/*.spec.ts"],
|
|
191
|
+
rules: {
|
|
192
|
+
"no-restricted-syntax": "off",
|
|
193
|
+
"no-restricted-imports": "off",
|
|
194
|
+
"no-restricted-globals": "off",
|
|
195
|
+
"no-restricted-properties": "off",
|
|
196
|
+
},
|
|
197
|
+
},
|
|
57
198
|
];
|
|
58
199
|
/**
|
|
59
200
|
* Creates the Phaser ESLint configuration.
|
|
@@ -65,23 +206,8 @@ const PHASER_3_IDIOM_BANS = [
|
|
|
65
206
|
*/
|
|
66
207
|
export function getPhaserConfig({ tsconfigRootDir, ignorePatterns = defaultPhaserIgnores, thresholds = defaultThresholds, }) {
|
|
67
208
|
return [
|
|
68
|
-
...getTypescriptConfig({
|
|
69
|
-
|
|
70
|
-
ignorePatterns,
|
|
71
|
-
thresholds,
|
|
72
|
-
}),
|
|
73
|
-
{
|
|
74
|
-
files: ["src/**/*.ts"],
|
|
75
|
-
rules: {
|
|
76
|
-
"no-restricted-syntax": ["error", ...PHASER_3_IDIOM_BANS],
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
files: ["tests/**/*.ts", "src/**/*.test.ts"],
|
|
81
|
-
rules: {
|
|
82
|
-
"no-restricted-syntax": "off",
|
|
83
|
-
},
|
|
84
|
-
},
|
|
209
|
+
...getTypescriptConfig({ tsconfigRootDir, ignorePatterns, thresholds }),
|
|
210
|
+
...PHASER_OVERRIDES,
|
|
85
211
|
];
|
|
86
212
|
}
|
|
87
213
|
//# sourceMappingURL=phaser.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"phaser.js","sourceRoot":"","sources":["../../../src/configs/eslint/phaser.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"phaser.js","sourceRoot":"","sources":["../../../src/configs/eslint/phaser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @codyswann/eslint-plugin-phaser
|
|
2
|
+
|
|
3
|
+
ESLint 9 plugin enforcing Phaser 4 performance and lifecycle invariants that
|
|
4
|
+
cannot be expressed as flat `no-restricted-syntax` selectors. It is wired into
|
|
5
|
+
the Phaser stack automatically by `@codyswann/lisa/eslint/phaser`; you do not
|
|
6
|
+
configure it directly.
|
|
7
|
+
|
|
8
|
+
## Rules
|
|
9
|
+
|
|
10
|
+
| Rule | What it catches |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `no-create-in-update` | Creating GameObjects, tweens, timers, or `new Phaser.*` objects inside a Scene `update()` method (per-frame churn). Create in `create()` and pool/reuse. |
|
|
13
|
+
| `no-allocation-in-update` | Heap allocations in `update()` — object/array literals, array-iteration chains (`.map`/`.filter`/`.reduce`/`.flatMap`), and `new` collections. Hoist scratch objects; iterate in place. |
|
|
14
|
+
| `require-shutdown-cleanup` | A Scene that registers persistent external listeners (`this.input`/`this.scale`/`this.game.events`, `window`/`document`) without a cleanup path. Satisfied by a `shutdown()` method or a `this.events.once('shutdown', ...)` handler. |
|
|
15
|
+
|
|
16
|
+
All three are scoped to `src/**` game code (relaxed in tests) by the Lisa Phaser
|
|
17
|
+
ESLint config. Tests for these rules live in the Lisa repo under
|
|
18
|
+
`tests/unit/config/eslint-plugin-phaser/` and run in CI via Vitest + ESLint's
|
|
19
|
+
`RuleTester`.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is managed by Lisa.
|
|
3
|
+
* Do not edit directly — changes will be overwritten on the next `lisa` run.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ESLint plugin for Phaser 4 game-development standards
|
|
8
|
+
*
|
|
9
|
+
* Stateful rules that enforce Phaser 4 performance and lifecycle invariants that
|
|
10
|
+
* cannot be expressed as flat `no-restricted-syntax` selectors. Supports ESLint 9
|
|
11
|
+
* flat config. Wired into the Phaser stack via @codyswann/lisa/eslint/phaser.
|
|
12
|
+
*
|
|
13
|
+
* Rules:
|
|
14
|
+
* - no-create-in-update: no GameObject/tween/timer/Phaser creation in update()
|
|
15
|
+
* - no-allocation-in-update: no heap allocations in update()
|
|
16
|
+
* - require-shutdown-cleanup: persistent external listeners need a cleanup path
|
|
17
|
+
* @module eslint-plugin-phaser
|
|
18
|
+
*/
|
|
19
|
+
const noCreateInUpdate = require("./rules/no-create-in-update");
|
|
20
|
+
const noAllocationInUpdate = require("./rules/no-allocation-in-update");
|
|
21
|
+
const requireShutdownCleanup = require("./rules/require-shutdown-cleanup");
|
|
22
|
+
|
|
23
|
+
const plugin = {
|
|
24
|
+
meta: {
|
|
25
|
+
name: "eslint-plugin-phaser",
|
|
26
|
+
version: "1.0.0",
|
|
27
|
+
},
|
|
28
|
+
rules: {
|
|
29
|
+
"no-create-in-update": noCreateInUpdate,
|
|
30
|
+
"no-allocation-in-update": noAllocationInUpdate,
|
|
31
|
+
"require-shutdown-cleanup": requireShutdownCleanup,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
module.exports = plugin;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codyswann/eslint-plugin-phaser",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ESLint plugin for Phaser 4 game-development standards",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"publishConfig": { "access": "public" },
|
|
7
|
+
"peerDependencies": {
|
|
8
|
+
"eslint": ">=9.0.0"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is managed by Lisa.
|
|
3
|
+
* Do not edit directly — changes will be overwritten on the next `lisa` run.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ESLint rule: no-allocation-in-update
|
|
8
|
+
*
|
|
9
|
+
* Flags heap allocations directly inside a Scene's `update(time, delta)` method.
|
|
10
|
+
* `update` runs every frame, so allocations there create GC pressure that
|
|
11
|
+
* stutters the frame budget. Hoist scratch objects/arrays to fields created
|
|
12
|
+
* once in create(), and avoid per-frame array-iteration chains.
|
|
13
|
+
*
|
|
14
|
+
* Detected (lexically in the update method body, not in nested functions):
|
|
15
|
+
* - object literals ({ ... })
|
|
16
|
+
* - array literals ([ ... ])
|
|
17
|
+
* - array-iteration method calls (.map/.filter/.reduce/.flatMap/.flat/.concat/.slice)
|
|
18
|
+
* - new Array / Object / Map / Set / WeakMap / WeakSet / typed arrays
|
|
19
|
+
* @module eslint-plugin-phaser/rules/no-allocation-in-update
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const ARRAY_ALLOC_METHODS = new Set([
|
|
23
|
+
"map",
|
|
24
|
+
"filter",
|
|
25
|
+
"reduce",
|
|
26
|
+
"reduceRight",
|
|
27
|
+
"flatMap",
|
|
28
|
+
"flat",
|
|
29
|
+
"concat",
|
|
30
|
+
"slice",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const COLLECTION_CTORS = new Set([
|
|
34
|
+
"Array",
|
|
35
|
+
"Object",
|
|
36
|
+
"Map",
|
|
37
|
+
"Set",
|
|
38
|
+
"WeakMap",
|
|
39
|
+
"WeakSet",
|
|
40
|
+
"Float32Array",
|
|
41
|
+
"Float64Array",
|
|
42
|
+
"Int32Array",
|
|
43
|
+
"Uint8Array",
|
|
44
|
+
"Uint16Array",
|
|
45
|
+
"Uint32Array",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve a property/method key node to its string name.
|
|
50
|
+
* @param {object} key - An Identifier or Literal key node
|
|
51
|
+
* @returns {string|null} The key name, or null if not resolvable
|
|
52
|
+
*/
|
|
53
|
+
function keyName(key) {
|
|
54
|
+
if (!key) return null;
|
|
55
|
+
if (key.type === "Identifier") return key.name;
|
|
56
|
+
if (key.type === "Literal") return String(key.value);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether a class extends a Phaser Scene (superclass name ends in "Scene", e.g.
|
|
62
|
+
* Phaser.Scene, Scene, BaseScene). Keeps this per-frame rule from firing on
|
|
63
|
+
* unrelated classes that merely happen to define an update() method.
|
|
64
|
+
* @param {object} classNode - A ClassDeclaration/ClassExpression node
|
|
65
|
+
* @returns {boolean} True if the class extends a Scene-like superclass
|
|
66
|
+
*/
|
|
67
|
+
function extendsScene(classNode) {
|
|
68
|
+
const sc = classNode.superClass;
|
|
69
|
+
if (!sc) return false;
|
|
70
|
+
if (sc.type === "Identifier") return /Scene$/.test(sc.name);
|
|
71
|
+
if (sc.type === "MemberExpression" && sc.property.type === "Identifier") {
|
|
72
|
+
return /Scene$/.test(sc.property.name);
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Whether the nearest enclosing class extends a Phaser Scene.
|
|
79
|
+
* @param {object} node - Any node
|
|
80
|
+
* @returns {boolean} True if an enclosing class extends a Scene
|
|
81
|
+
*/
|
|
82
|
+
function enclosingSceneClass(node) {
|
|
83
|
+
const up = n => {
|
|
84
|
+
if (!n) return false;
|
|
85
|
+
if (n.type === "ClassDeclaration" || n.type === "ClassExpression") {
|
|
86
|
+
return extendsScene(n);
|
|
87
|
+
}
|
|
88
|
+
return up(n.parent);
|
|
89
|
+
};
|
|
90
|
+
return up(node.parent);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Determine whether a function node is the update() method of a Phaser Scene.
|
|
95
|
+
* @param {object} node - A function-like node
|
|
96
|
+
* @returns {boolean} True if it is a Scene update() method/field
|
|
97
|
+
*/
|
|
98
|
+
function isUpdateFunction(node) {
|
|
99
|
+
const parent = node.parent;
|
|
100
|
+
if (!parent) return false;
|
|
101
|
+
if (
|
|
102
|
+
(parent.type === "MethodDefinition" ||
|
|
103
|
+
parent.type === "PropertyDefinition") &&
|
|
104
|
+
parent.value === node &&
|
|
105
|
+
keyName(parent.key) === "update"
|
|
106
|
+
) {
|
|
107
|
+
return enclosingSceneClass(node);
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
meta: {
|
|
114
|
+
type: "problem",
|
|
115
|
+
docs: {
|
|
116
|
+
description:
|
|
117
|
+
"Disallow heap allocations (object/array literals, array-iteration chains, new collections) inside a Scene update() method",
|
|
118
|
+
category: "Best Practices",
|
|
119
|
+
recommended: true,
|
|
120
|
+
},
|
|
121
|
+
fixable: null,
|
|
122
|
+
schema: [],
|
|
123
|
+
messages: {
|
|
124
|
+
objectLiteral:
|
|
125
|
+
"Object literal allocated in update() (runs every frame). Hoist a reusable scratch object to a field set in create().",
|
|
126
|
+
arrayLiteral:
|
|
127
|
+
"Array literal allocated in update() (runs every frame). Hoist a reusable array, or iterate an existing one in place.",
|
|
128
|
+
arrayMethod:
|
|
129
|
+
"Array-iteration method '{{name}}' allocates in update() (runs every frame). Use a plain for-loop over the existing array.",
|
|
130
|
+
newCollection:
|
|
131
|
+
"new {{name}} allocated in update() (runs every frame). Create the collection once in create() and clear/reuse it.",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
create(context) {
|
|
136
|
+
const fnStack = [];
|
|
137
|
+
const inUpdateBody = () =>
|
|
138
|
+
fnStack.length > 0 && fnStack[fnStack.length - 1];
|
|
139
|
+
const enter = node => fnStack.push(isUpdateFunction(node));
|
|
140
|
+
const exit = () => fnStack.pop();
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
FunctionDeclaration: enter,
|
|
144
|
+
"FunctionDeclaration:exit": exit,
|
|
145
|
+
FunctionExpression: enter,
|
|
146
|
+
"FunctionExpression:exit": exit,
|
|
147
|
+
ArrowFunctionExpression: enter,
|
|
148
|
+
"ArrowFunctionExpression:exit": exit,
|
|
149
|
+
|
|
150
|
+
ObjectExpression(node) {
|
|
151
|
+
if (inUpdateBody())
|
|
152
|
+
context.report({ node, messageId: "objectLiteral" });
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
ArrayExpression(node) {
|
|
156
|
+
if (inUpdateBody()) context.report({ node, messageId: "arrayLiteral" });
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
CallExpression(node) {
|
|
160
|
+
if (!inUpdateBody()) return;
|
|
161
|
+
if (
|
|
162
|
+
node.callee.type === "MemberExpression" &&
|
|
163
|
+
node.callee.property.type === "Identifier" &&
|
|
164
|
+
ARRAY_ALLOC_METHODS.has(node.callee.property.name)
|
|
165
|
+
) {
|
|
166
|
+
context.report({
|
|
167
|
+
node,
|
|
168
|
+
messageId: "arrayMethod",
|
|
169
|
+
data: { name: node.callee.property.name },
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
NewExpression(node) {
|
|
175
|
+
if (!inUpdateBody()) return;
|
|
176
|
+
if (
|
|
177
|
+
node.callee.type === "Identifier" &&
|
|
178
|
+
COLLECTION_CTORS.has(node.callee.name)
|
|
179
|
+
) {
|
|
180
|
+
context.report({
|
|
181
|
+
node,
|
|
182
|
+
messageId: "newCollection",
|
|
183
|
+
data: { name: node.callee.name },
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
},
|
|
189
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is managed by Lisa.
|
|
3
|
+
* Do not edit directly — changes will be overwritten on the next `lisa` run.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ESLint rule: no-create-in-update
|
|
8
|
+
*
|
|
9
|
+
* Flags creation of GameObjects, tweens, timers, and Phaser objects inside a
|
|
10
|
+
* Scene's `update(time, delta)` method. `update` runs every frame; creating
|
|
11
|
+
* objects there churns the heap and stutters the frame budget. Create in
|
|
12
|
+
* `create()` and reuse (pool) instead.
|
|
13
|
+
*
|
|
14
|
+
* Detected (when lexically in the update method body, not in nested functions):
|
|
15
|
+
* - this.add.* / this.make.* (GameObjectFactory / GameObjectCreator)
|
|
16
|
+
* - this.physics.add.* (Arcade factory)
|
|
17
|
+
* - this.tweens.add/addCounter/chain/create (Tween creation)
|
|
18
|
+
* - this.time.addEvent/delayedCall/addLoop (Timer creation)
|
|
19
|
+
* - this.sound.add / this.anims.create / this.particles.add
|
|
20
|
+
* - new Phaser.* (Phaser object allocation)
|
|
21
|
+
* @module eslint-plugin-phaser/rules/no-create-in-update
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve a property/method key node to its string name.
|
|
26
|
+
* @param {object} key - An Identifier or Literal key node
|
|
27
|
+
* @returns {string|null} The key name, or null if not resolvable
|
|
28
|
+
*/
|
|
29
|
+
function keyName(key) {
|
|
30
|
+
if (!key) return null;
|
|
31
|
+
if (key.type === "Identifier") return key.name;
|
|
32
|
+
if (key.type === "Literal") return String(key.value);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Decode a MemberExpression chain into its root node and ordered property names.
|
|
38
|
+
* @param {object} node - A MemberExpression node
|
|
39
|
+
* @returns {{root: object, props: (string|null)[]}} Root node and property names
|
|
40
|
+
*/
|
|
41
|
+
function memberInfo(node) {
|
|
42
|
+
const props = [];
|
|
43
|
+
/**
|
|
44
|
+
* Walk up the member chain, collecting property names.
|
|
45
|
+
* @param {object} cur - Current node in the chain
|
|
46
|
+
* @returns {object} The non-member root node
|
|
47
|
+
*/
|
|
48
|
+
const walk = cur => {
|
|
49
|
+
if (cur && cur.type === "MemberExpression") {
|
|
50
|
+
props.unshift(
|
|
51
|
+
cur.property && cur.property.type === "Identifier"
|
|
52
|
+
? cur.property.name
|
|
53
|
+
: null
|
|
54
|
+
);
|
|
55
|
+
return walk(cur.object);
|
|
56
|
+
}
|
|
57
|
+
return cur;
|
|
58
|
+
};
|
|
59
|
+
return { root: walk(node), props };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Whether a class extends a Phaser Scene (superclass name ends in "Scene", e.g.
|
|
64
|
+
* Phaser.Scene, Scene, BaseScene). Keeps this per-frame rule from firing on
|
|
65
|
+
* unrelated classes that merely happen to define an update() method.
|
|
66
|
+
* @param {object} classNode - A ClassDeclaration/ClassExpression node
|
|
67
|
+
* @returns {boolean} True if the class extends a Scene-like superclass
|
|
68
|
+
*/
|
|
69
|
+
function extendsScene(classNode) {
|
|
70
|
+
const sc = classNode.superClass;
|
|
71
|
+
if (!sc) return false;
|
|
72
|
+
if (sc.type === "Identifier") return /Scene$/.test(sc.name);
|
|
73
|
+
if (sc.type === "MemberExpression" && sc.property.type === "Identifier") {
|
|
74
|
+
return /Scene$/.test(sc.property.name);
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Whether the nearest enclosing class extends a Phaser Scene.
|
|
81
|
+
* @param {object} node - Any node
|
|
82
|
+
* @returns {boolean} True if an enclosing class extends a Scene
|
|
83
|
+
*/
|
|
84
|
+
function enclosingSceneClass(node) {
|
|
85
|
+
const up = n => {
|
|
86
|
+
if (!n) return false;
|
|
87
|
+
if (n.type === "ClassDeclaration" || n.type === "ClassExpression") {
|
|
88
|
+
return extendsScene(n);
|
|
89
|
+
}
|
|
90
|
+
return up(n.parent);
|
|
91
|
+
};
|
|
92
|
+
return up(node.parent);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Determine whether a function node is the update() method of a Phaser Scene.
|
|
97
|
+
* @param {object} node - A function-like node
|
|
98
|
+
* @returns {boolean} True if it is a Scene update() method/field
|
|
99
|
+
*/
|
|
100
|
+
function isUpdateFunction(node) {
|
|
101
|
+
const parent = node.parent;
|
|
102
|
+
if (!parent) return false;
|
|
103
|
+
if (
|
|
104
|
+
(parent.type === "MethodDefinition" ||
|
|
105
|
+
parent.type === "PropertyDefinition") &&
|
|
106
|
+
parent.value === node &&
|
|
107
|
+
keyName(parent.key) === "update"
|
|
108
|
+
) {
|
|
109
|
+
return enclosingSceneClass(node);
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Patterns that create new objects, keyed by the leading `this.<segment>` chain.
|
|
116
|
+
* @param {(string|null)[]} props - Ordered property names from `this`
|
|
117
|
+
* @returns {boolean} True if the chain is a known creation call
|
|
118
|
+
*/
|
|
119
|
+
function isCreationChain(props) {
|
|
120
|
+
const [a, b] = props;
|
|
121
|
+
if (a === "add" || a === "make") return true;
|
|
122
|
+
if (a === "physics" && b === "add") return true;
|
|
123
|
+
if (
|
|
124
|
+
a === "tweens" &&
|
|
125
|
+
(b === "add" || b === "addCounter" || b === "chain" || b === "create")
|
|
126
|
+
) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
if (
|
|
130
|
+
a === "time" &&
|
|
131
|
+
(b === "addEvent" || b === "delayedCall" || b === "addLoop")
|
|
132
|
+
)
|
|
133
|
+
return true;
|
|
134
|
+
if (a === "sound" && b === "add") return true;
|
|
135
|
+
if (a === "anims" && b === "create") return true;
|
|
136
|
+
if (a === "particles" && b === "add") return true;
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
meta: {
|
|
142
|
+
type: "problem",
|
|
143
|
+
docs: {
|
|
144
|
+
description:
|
|
145
|
+
"Disallow creating GameObjects, tweens, timers, or Phaser objects inside a Scene update() method",
|
|
146
|
+
category: "Best Practices",
|
|
147
|
+
recommended: true,
|
|
148
|
+
},
|
|
149
|
+
fixable: null,
|
|
150
|
+
schema: [],
|
|
151
|
+
messages: {
|
|
152
|
+
createInUpdate:
|
|
153
|
+
"Do not create objects in update() — it runs every frame. Create in create() and pool/reuse (Group.get / killAndHide).",
|
|
154
|
+
newInUpdate:
|
|
155
|
+
"Do not allocate Phaser objects (new Phaser.*) in update() — it runs every frame. Hoist a reusable scratch instance to create().",
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
create(context) {
|
|
160
|
+
const fnStack = [];
|
|
161
|
+
const inUpdateBody = () =>
|
|
162
|
+
fnStack.length > 0 && fnStack[fnStack.length - 1];
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Push function scope state on entry.
|
|
166
|
+
* @param {object} node - The function node being entered
|
|
167
|
+
* @returns {void}
|
|
168
|
+
*/
|
|
169
|
+
const enter = node => fnStack.push(isUpdateFunction(node));
|
|
170
|
+
const exit = () => fnStack.pop();
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
FunctionDeclaration: enter,
|
|
174
|
+
"FunctionDeclaration:exit": exit,
|
|
175
|
+
FunctionExpression: enter,
|
|
176
|
+
"FunctionExpression:exit": exit,
|
|
177
|
+
ArrowFunctionExpression: enter,
|
|
178
|
+
"ArrowFunctionExpression:exit": exit,
|
|
179
|
+
|
|
180
|
+
CallExpression(node) {
|
|
181
|
+
if (!inUpdateBody()) return;
|
|
182
|
+
if (node.callee.type !== "MemberExpression") return;
|
|
183
|
+
const { root, props } = memberInfo(node.callee);
|
|
184
|
+
if (root.type !== "ThisExpression") return;
|
|
185
|
+
if (isCreationChain(props)) {
|
|
186
|
+
context.report({ node, messageId: "createInUpdate" });
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
NewExpression(node) {
|
|
191
|
+
if (!inUpdateBody()) return;
|
|
192
|
+
if (node.callee.type !== "MemberExpression") return;
|
|
193
|
+
const { root } = memberInfo(node.callee);
|
|
194
|
+
if (root.type === "Identifier" && root.name === "Phaser") {
|
|
195
|
+
context.report({ node, messageId: "newInUpdate" });
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
};
|