@defold-typescript/tstl-plugin 0.29.0 → 0.31.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/build-config.d.ts +1 -2
- package/dist/completion-harness.d.ts +49 -0
- package/dist/index.cjs +6 -0
- package/dist/index.js +249 -103
- package/dist/scene-diagnostics.d.ts +29 -0
- package/dist/scene-index-cache.d.ts +3 -1
- package/dist/scene-provenance.d.ts +20 -1
- package/dist/scene-relative-addresses.d.ts +55 -0
- package/package.json +8 -6
- package/src/build-config.ts +6 -2
- package/src/index.ts +103 -49
- package/src/require-entry.cjs +6 -0
- package/src/scene-diagnostics.ts +92 -0
- package/src/scene-index-cache.ts +50 -2
- package/src/scene-provenance.ts +87 -21
- package/src/scene-relative-addresses.ts +145 -0
- package/dist/scene-documents.d.ts +0 -18
- package/src/scene-documents.ts +0 -126
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type NamingContext, type SceneObjectPathIndex } from "@defold-typescript/transpiler";
|
|
2
|
+
import { type SceneIndexCache } from "./scene-index-cache";
|
|
3
|
+
export interface RelativeUniverse {
|
|
4
|
+
/** Every object hosting this file's generated script, sorted by object path. */
|
|
5
|
+
readonly contexts: readonly NamingContext[];
|
|
6
|
+
/** Relative game-object paths, valid from at least one context. */
|
|
7
|
+
readonly paths: ReadonlySet<string>;
|
|
8
|
+
/** Those paths joined to the components their object owns. */
|
|
9
|
+
readonly addresses: ReadonlySet<string>;
|
|
10
|
+
/** Entry name -> the sorted object paths it resolves from. */
|
|
11
|
+
readonly contextsByEntry: ReadonlyMap<string, readonly string[]>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Every game-object path the project declares and the components each owns —
|
|
15
|
+
* the index a check scopes an absolute `#fragment` address against.
|
|
16
|
+
*
|
|
17
|
+
* Read off the walk `projectContexts` already caches rather than built again:
|
|
18
|
+
* the naming contexts are derived *from* this index, so the reachability path
|
|
19
|
+
* costs no second `.go`/`.collection` parse.
|
|
20
|
+
*/
|
|
21
|
+
export declare function projectObjectIndex(cache: SceneIndexCache): SceneObjectPathIndex;
|
|
22
|
+
/**
|
|
23
|
+
* The relative universe the file at `fileName` may write from, keyed per file
|
|
24
|
+
* under the same cache the absolute indexes live in.
|
|
25
|
+
*
|
|
26
|
+
* The file is mapped *forward* to the script resource a scene would name — the
|
|
27
|
+
* same direction `nodeEntries` takes, because an output path cannot say which
|
|
28
|
+
* include base produced it. Both script kinds are resolved and their contexts
|
|
29
|
+
* unioned: a `.ts` is named as a `.script` by a game object and as a
|
|
30
|
+
* `.gui_script` by a `.gui`, and nothing in the file itself says which.
|
|
31
|
+
*/
|
|
32
|
+
export declare function relativeUniverseFor(cache: SceneIndexCache, fileName: string): RelativeUniverse;
|
|
33
|
+
/**
|
|
34
|
+
* The worlds the file's script runs in, as `checkCrossWorldAddresses` asks for
|
|
35
|
+
* them: the de-duplicated sockets of every naming context hosting it, with the
|
|
36
|
+
* bootstrap world carried as `undefined`.
|
|
37
|
+
*
|
|
38
|
+
* The same answer `scriptWorldsForBuild` gives the CLI, reached through the
|
|
39
|
+
* chain that resolver was modelled on — so an address the build calls foreign is
|
|
40
|
+
* foreign in the editor too. An empty answer is the honest unknown: a script no
|
|
41
|
+
* scene hosts runs in no world this project can prove, and the check reports
|
|
42
|
+
* nothing for it.
|
|
43
|
+
*/
|
|
44
|
+
export declare function worldsFor(cache: SceneIndexCache, fileName: string): readonly (string | undefined)[];
|
|
45
|
+
/**
|
|
46
|
+
* Whether a bare `/path` can resolve from where this file runs. A bare address
|
|
47
|
+
* continues the caller's own world, so one offered to a script running behind a
|
|
48
|
+
* proxy names an object that world cannot reach.
|
|
49
|
+
*
|
|
50
|
+
* A file with no naming context offers them: nothing is known about where it
|
|
51
|
+
* runs, and withholding every bare path would delete the completions the
|
|
52
|
+
* absolute universe has always provided. Socket-qualified keys are never
|
|
53
|
+
* touched — whether a slot accepts a foreign socket is a different question.
|
|
54
|
+
*/
|
|
55
|
+
export declare function offersBareWorld(universe: RelativeUniverse): boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defold-typescript/tstl-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "TypeScript language-service plugin surfacing Defold transpiler diagnostics live in the editor.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -11,32 +11,34 @@
|
|
|
11
11
|
"homepage": "https://github.com/defold-typescript/toolchain#readme",
|
|
12
12
|
"bugs": "https://github.com/defold-typescript/toolchain/issues",
|
|
13
13
|
"type": "module",
|
|
14
|
-
"main": "./dist/index.
|
|
14
|
+
"main": "./dist/index.cjs",
|
|
15
15
|
"types": "./dist/index.d.ts",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
19
19
|
"bun": "./src/index.ts",
|
|
20
|
-
"import": "./dist/index.js"
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"require": "./dist/index.cjs"
|
|
21
22
|
}
|
|
22
23
|
},
|
|
23
24
|
"files": [
|
|
24
25
|
"dist",
|
|
25
26
|
"src",
|
|
26
27
|
"!src/**/*.test.ts",
|
|
28
|
+
"!src/**/completion-harness.ts",
|
|
27
29
|
"!src/**/__snapshots__"
|
|
28
30
|
],
|
|
29
31
|
"publishConfig": {
|
|
30
32
|
"access": "public"
|
|
31
33
|
},
|
|
32
34
|
"scripts": {
|
|
33
|
-
"build": "rm -rf dist && bun build src/index.ts --target=node --format=esm --packages=external --outdir=dist && tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
35
|
+
"build": "rm -rf dist && bun build src/index.ts --target=node --format=esm --packages=external --outdir=dist && cp src/require-entry.cjs dist/index.cjs && tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
34
36
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
35
37
|
"test": "bun test"
|
|
36
38
|
},
|
|
37
39
|
"dependencies": {
|
|
38
|
-
"@defold-typescript/transpiler": "0.
|
|
39
|
-
"@defold-typescript/types": "0.
|
|
40
|
+
"@defold-typescript/transpiler": "0.31.0",
|
|
41
|
+
"@defold-typescript/types": "0.31.0"
|
|
40
42
|
},
|
|
41
43
|
"peerDependencies": {
|
|
42
44
|
"typescript": ">=5.0.0 <7"
|
package/src/build-config.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
type BuildConfig,
|
|
3
|
+
DEFAULT_INCLUDE,
|
|
4
|
+
parseBuildConfig,
|
|
5
|
+
type SceneReadHost,
|
|
6
|
+
} from "@defold-typescript/transpiler";
|
|
3
7
|
|
|
4
8
|
// Alongside output is what the build itself does with no `outDir`, so it is also
|
|
5
9
|
// the honest answer when the config cannot be read: a wrong guess would offer
|
package/src/index.ts
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import {
|
|
4
|
+
ANIMATION_ASSET_EXTENSIONS,
|
|
5
|
+
buildComponentAnimationIndex,
|
|
4
6
|
buildConfigKeyIndex,
|
|
7
|
+
buildGuiFlipbookIndex,
|
|
5
8
|
buildGuiNodeIndex,
|
|
6
9
|
buildInputActionIndex,
|
|
7
|
-
buildSceneComponentIndex,
|
|
8
10
|
buildSceneObjectPathIndex,
|
|
9
|
-
buildSpriteAnimationIndex,
|
|
10
11
|
type ClassifiedSlot,
|
|
11
12
|
componentIdOfSameObjectAddress,
|
|
12
13
|
computeOutputRel,
|
|
14
|
+
displayPathOf,
|
|
15
|
+
GAME_PROJECT_DOCUMENT,
|
|
16
|
+
GUI_EXTENSIONS,
|
|
13
17
|
getProgramDiagnostics,
|
|
18
|
+
INPUT_BINDING_EXTENSIONS,
|
|
14
19
|
isAddressClass,
|
|
15
20
|
isFragmentCaret,
|
|
21
|
+
PROJECT_EXTENSIONS,
|
|
16
22
|
resolveClassifiedSlotAtPosition,
|
|
17
23
|
} from "@defold-typescript/transpiler";
|
|
18
24
|
import type { UrlParameterTable } from "@defold-typescript/types";
|
|
@@ -25,20 +31,16 @@ import {
|
|
|
25
31
|
CONTRIBUTED_ENTRY_KIND,
|
|
26
32
|
DEFOLD_COMPLETION_SOURCE,
|
|
27
33
|
} from "./scene-completions";
|
|
28
|
-
import {
|
|
29
|
-
ANIMATION_ASSET_EXTENSIONS,
|
|
30
|
-
displayPathOf,
|
|
31
|
-
GAME_PROJECT_DOCUMENT,
|
|
32
|
-
GUI_EXTENSIONS,
|
|
33
|
-
INPUT_BINDING_EXTENSIONS,
|
|
34
|
-
PROJECT_EXTENSIONS,
|
|
35
|
-
} from "./scene-documents";
|
|
34
|
+
import { sceneReachabilityDiagnostics } from "./scene-diagnostics";
|
|
36
35
|
import {
|
|
37
36
|
createSceneIndexCache,
|
|
38
37
|
type SceneIndexCache,
|
|
39
38
|
type SceneWatchHost,
|
|
39
|
+
sceneCollectionRolesOf,
|
|
40
|
+
sceneComponentIndexOf,
|
|
40
41
|
} from "./scene-index-cache";
|
|
41
|
-
import { resolveEntryProvenance } from "./scene-provenance";
|
|
42
|
+
import { resolveEntryProvenance, resolveRelativeEntryProvenance } from "./scene-provenance";
|
|
43
|
+
import { offersBareWorld, relativeUniverseFor } from "./scene-relative-addresses";
|
|
42
44
|
|
|
43
45
|
const requireFromHere = createRequire(import.meta.url);
|
|
44
46
|
|
|
@@ -78,35 +80,50 @@ function componentEntries(
|
|
|
78
80
|
// suggestion claims nothing about what is absent.
|
|
79
81
|
return buildSceneCompletionEntries({
|
|
80
82
|
slot,
|
|
81
|
-
ids: cache.
|
|
82
|
-
"component-ids",
|
|
83
|
-
() => buildSceneComponentIndex(cache.documents().documents).ids,
|
|
84
|
-
),
|
|
83
|
+
ids: sceneComponentIndexOf(cache).ids,
|
|
85
84
|
baseEntries,
|
|
86
85
|
});
|
|
87
86
|
}
|
|
88
87
|
|
|
89
88
|
// The exact complement of `componentEntries`' guard, so precisely one of the two
|
|
90
|
-
// universes answers any caret in an address.
|
|
91
|
-
// universe, and for the same reason: what a path resolves to
|
|
92
|
-
// on the collection that was loaded, which the file being
|
|
89
|
+
// universes answers any caret in an address. The absolute half is project-wide
|
|
90
|
+
// like the component universe, and for the same reason: what a path resolves to
|
|
91
|
+
// at runtime depends on the collection that was loaded, which the file being
|
|
92
|
+
// edited does not say. The relative half cannot be — a relative address
|
|
93
|
+
// continues the collection path of the object hosting *this* script — so it is
|
|
94
|
+
// read per file and merged in, and the caller's own world decides whether a bare
|
|
95
|
+
// absolute path can resolve at all.
|
|
93
96
|
function objectPathEntries(
|
|
94
97
|
slot: ClassifiedSlot,
|
|
95
98
|
position: number,
|
|
96
99
|
cache: SceneIndexCache,
|
|
100
|
+
fileName: string,
|
|
97
101
|
baseEntries: readonly ts.CompletionEntry[],
|
|
98
102
|
): ts.CompletionEntry[] {
|
|
99
103
|
if (isFragmentCaret(slot, position)) {
|
|
100
104
|
return [];
|
|
101
105
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
const universe = relativeUniverseFor(cache, fileName);
|
|
107
|
+
const bare = offersBareWorld(universe);
|
|
108
|
+
const absolute = cache.derived(
|
|
109
|
+
"object-paths",
|
|
110
|
+
() =>
|
|
111
|
+
buildSceneObjectPathIndex(cache.documents().documents, sceneCollectionRolesOf(cache)).paths,
|
|
112
|
+
);
|
|
113
|
+
const paths = new Set<string>();
|
|
114
|
+
for (const key of absolute) {
|
|
115
|
+
if (bare || !key.startsWith("/")) paths.add(key);
|
|
116
|
+
}
|
|
117
|
+
for (const key of universe.paths) paths.add(key);
|
|
118
|
+
const entries = buildAddressPathCompletionEntries({ slot, paths, baseEntries });
|
|
119
|
+
// A join is the whole literal, so accepting one where the author already typed
|
|
120
|
+
// a `#` would silently rewrite the fragment they are holding.
|
|
121
|
+
if (slot.fragmentStart === -1) {
|
|
122
|
+
entries.push(
|
|
123
|
+
...buildWholeLiteralCompletionEntries({ slot, ids: universe.addresses, baseEntries }),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return entries;
|
|
110
127
|
}
|
|
111
128
|
|
|
112
129
|
// No caret guard: the span is the whole literal, so an entry is well-formed
|
|
@@ -133,24 +150,46 @@ function nodeEntries(
|
|
|
133
150
|
}
|
|
134
151
|
|
|
135
152
|
// Also no caret guard, and the same forward output-path mapping — but scoped
|
|
136
|
-
// one step further than a node id
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
153
|
+
// one step further than a node id, in whichever of the two ways the slot's own
|
|
154
|
+
// entry declares. Every unresolved link returns nothing rather than a
|
|
155
|
+
// project-wide guess, because an animation id the resolved atlas does not
|
|
156
|
+
// declare is a runtime crash.
|
|
157
|
+
//
|
|
158
|
+
// The branch is the table's own discriminator rather than a second arm in the
|
|
159
|
+
// class chain: an `animation` entry names either an address companion or a node
|
|
160
|
+
// companion, and the drift guard is what keeps it to exactly one.
|
|
141
161
|
function animationEntries(
|
|
142
162
|
slot: ClassifiedSlot,
|
|
143
163
|
cache: SceneIndexCache,
|
|
144
164
|
fileName: string,
|
|
145
165
|
baseEntries: readonly ts.CompletionEntry[],
|
|
146
166
|
): ts.CompletionEntry[] {
|
|
167
|
+
const { projectRoot } = cache;
|
|
168
|
+
// Scoped to the `.gui` scene naming this file's generated gui script, never
|
|
169
|
+
// to the node the call addresses: `gui.set_texture` retargets a node at
|
|
170
|
+
// runtime, so the scene's textures are the honest universe.
|
|
171
|
+
if (slot.nodeParameter !== undefined) {
|
|
172
|
+
const index = cache.derived("gui-flipbook", () =>
|
|
173
|
+
buildGuiFlipbookIndex({
|
|
174
|
+
scenes: cache.documents(GUI_EXTENSIONS).documents,
|
|
175
|
+
assets: cache.documents(ANIMATION_ASSET_EXTENSIONS).documents,
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
const config = readBuildConfigFromHost(cache.host, projectRoot);
|
|
179
|
+
const resource = computeOutputRel(displayPathOf(projectRoot, fileName), config, "gui-script");
|
|
180
|
+
const declared = index.byScriptResource.get(resource);
|
|
181
|
+
return declared === undefined
|
|
182
|
+
? []
|
|
183
|
+
: buildWholeLiteralCompletionEntries({ slot, ids: new Set(declared.keys()), baseEntries });
|
|
184
|
+
}
|
|
185
|
+
// Scoped to the sprite or model component the slot's *sibling* literal
|
|
186
|
+
// addresses on the one game object that owns this script.
|
|
147
187
|
const component = componentIdOfSameObjectAddress(slot.addressText ?? "");
|
|
148
188
|
if (component === undefined) {
|
|
149
189
|
return [];
|
|
150
190
|
}
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
buildSpriteAnimationIndex({
|
|
191
|
+
const index = cache.derived("component-animations", () =>
|
|
192
|
+
buildComponentAnimationIndex({
|
|
154
193
|
scenes: cache.documents().documents,
|
|
155
194
|
assets: cache.documents(ANIMATION_ASSET_EXTENSIONS).documents,
|
|
156
195
|
}),
|
|
@@ -212,23 +251,17 @@ function actionIdEntries(
|
|
|
212
251
|
}
|
|
213
252
|
|
|
214
253
|
// The panel a claimed request is answered with. `documentation` rather than
|
|
215
|
-
// `displayParts` carries the
|
|
216
|
-
// body of the panel, which is where a list of files reads as
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
): ts.CompletionEntryDetails {
|
|
254
|
+
// `displayParts` carries the sentence because a host renders the former as the
|
|
255
|
+
// body of the panel, which is where a list of files — or of objects — reads as
|
|
256
|
+
// one. The sentence is the caller's, since an absolute entry is answered with
|
|
257
|
+
// the files declaring it and a relative one with the objects it resolves from.
|
|
258
|
+
function provenancePanel(entryName: string, documentation: string): ts.CompletionEntryDetails {
|
|
221
259
|
return {
|
|
222
260
|
name: entryName,
|
|
223
261
|
kind: CONTRIBUTED_ENTRY_KIND,
|
|
224
262
|
kindModifiers: "",
|
|
225
263
|
displayParts: [{ kind: "stringLiteral", text: JSON.stringify(entryName) }],
|
|
226
|
-
documentation: [
|
|
227
|
-
{
|
|
228
|
-
kind: "text",
|
|
229
|
-
text: `Declared in ${declaredIn.join(", ")}`,
|
|
230
|
-
},
|
|
231
|
-
],
|
|
264
|
+
documentation: [{ kind: "text", text: documentation }],
|
|
232
265
|
source: [{ kind: "text", text: DEFOLD_COMPLETION_SOURCE }],
|
|
233
266
|
};
|
|
234
267
|
}
|
|
@@ -306,8 +339,23 @@ export default function init(modules: { typescript: typeof import("typescript")
|
|
|
306
339
|
if (!slot) {
|
|
307
340
|
return forward();
|
|
308
341
|
}
|
|
342
|
+
// Tried first because the two universes are disjoint by construction and
|
|
343
|
+
// only this one knows which object an entry is relative to; a name it does
|
|
344
|
+
// not carry falls straight through to the declaring-file answer.
|
|
345
|
+
const relative = resolveRelativeEntryProvenance({
|
|
346
|
+
slot,
|
|
347
|
+
position,
|
|
348
|
+
cache,
|
|
349
|
+
fileName,
|
|
350
|
+
entryName,
|
|
351
|
+
});
|
|
352
|
+
if (relative !== undefined) {
|
|
353
|
+
return provenancePanel(entryName, relative);
|
|
354
|
+
}
|
|
309
355
|
const declaredIn = resolveEntryProvenance({ slot, position, cache, fileName, entryName });
|
|
310
|
-
return declaredIn.length === 0
|
|
356
|
+
return declaredIn.length === 0
|
|
357
|
+
? forward()
|
|
358
|
+
: provenancePanel(entryName, `Declared in ${declaredIn.join(", ")}`);
|
|
311
359
|
};
|
|
312
360
|
|
|
313
361
|
proxy.getSemanticDiagnostics = (fileName: string): ts.Diagnostic[] => {
|
|
@@ -321,7 +369,13 @@ export default function init(modules: { typescript: typeof import("typescript")
|
|
|
321
369
|
const transpiler = getProgramDiagnostics(program, program.getSourceFile(fileName)).map(
|
|
322
370
|
(diagnostic) => ({ ...diagnostic, category: ts.DiagnosticCategory.Suggestion }),
|
|
323
371
|
);
|
|
324
|
-
|
|
372
|
+
// The same advisory posture, and the same absent-cache early return the
|
|
373
|
+
// completion path takes: a host that cannot enumerate files has no scene
|
|
374
|
+
// universe, and an empty one would report every address as unreachable.
|
|
375
|
+
const table = loadUrlParameterTable();
|
|
376
|
+
const scene =
|
|
377
|
+
cache && table ? sceneReachabilityDiagnostics({ ts, program, table, cache, fileName }) : [];
|
|
378
|
+
return [...prior, ...transpiler, ...scene];
|
|
325
379
|
};
|
|
326
380
|
|
|
327
381
|
// Strictly additive: every path that cannot produce a suggestion returns the
|
|
@@ -356,7 +410,7 @@ export default function init(modules: { typescript: typeof import("typescript")
|
|
|
356
410
|
const entries = isAddressClass(slot.class)
|
|
357
411
|
? [
|
|
358
412
|
...componentEntries(slot, position, cache, baseEntries),
|
|
359
|
-
...objectPathEntries(slot, position, cache, baseEntries),
|
|
413
|
+
...objectPathEntries(slot, position, cache, fileName, baseEntries),
|
|
360
414
|
]
|
|
361
415
|
: slot.class === "gui-node"
|
|
362
416
|
? nodeEntries(slot, cache, fileName, baseEntries)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// tsserver resolves a plugin with Node10 resolution — `exports` is not
|
|
2
|
+
// consulted — and then rejects the loaded value unless it is itself a function.
|
|
3
|
+
// `require()` of the ESM entry yields the namespace object instead, so this
|
|
4
|
+
// CommonJS shim is what `main` points at: it unwraps the factory, and the ESM
|
|
5
|
+
// bundle stays the single build of the plugin.
|
|
6
|
+
module.exports = require("./index.js").default;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkCrossWorldAddresses,
|
|
3
|
+
checkUrlFragmentReachability,
|
|
4
|
+
} from "@defold-typescript/transpiler";
|
|
5
|
+
import type { UrlParameterTable } from "@defold-typescript/types";
|
|
6
|
+
import type * as ts from "typescript";
|
|
7
|
+
import { type SceneIndexCache, sceneComponentIndexOf } from "./scene-index-cache";
|
|
8
|
+
import { projectObjectIndex, worldsFor } from "./scene-relative-addresses";
|
|
9
|
+
|
|
10
|
+
/** The `source` an editor renders beside the message, and `--json` never sees. */
|
|
11
|
+
export const DEFOLD_DIAGNOSTIC_SOURCE = "defold-typescript";
|
|
12
|
+
|
|
13
|
+
// A range neither TypeScript (< 100000) nor tstl occupies, so a host filtering
|
|
14
|
+
// by code can tell the two address findings apart from each other and from
|
|
15
|
+
// everything else the language service reports.
|
|
16
|
+
const UNREACHABLE_FRAGMENT_CODE = 9_000_101;
|
|
17
|
+
const FOREIGN_SOCKET_CODE = 9_000_102;
|
|
18
|
+
|
|
19
|
+
function suggestion(
|
|
20
|
+
typescript: typeof import("typescript"),
|
|
21
|
+
file: ts.SourceFile,
|
|
22
|
+
finding: { readonly start: number; readonly length: number; readonly message: string },
|
|
23
|
+
code: number,
|
|
24
|
+
): ts.Diagnostic {
|
|
25
|
+
return {
|
|
26
|
+
file,
|
|
27
|
+
start: finding.start,
|
|
28
|
+
length: finding.length,
|
|
29
|
+
messageText: finding.message,
|
|
30
|
+
category: typescript.DiagnosticCategory.Suggestion,
|
|
31
|
+
source: DEFOLD_DIAGNOSTIC_SOURCE,
|
|
32
|
+
code,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The address findings the editor surfaces on the file being edited, as
|
|
38
|
+
* advisory suggestions.
|
|
39
|
+
*
|
|
40
|
+
* Every path that cannot produce one answers with an empty array. A suppressed
|
|
41
|
+
* reachability report is the important case: there is no file span on which a
|
|
42
|
+
* "the check did not run" notice could honestly sit, and a speculative squiggle
|
|
43
|
+
* is worse than none — the build says so in prose instead, where a whole-project
|
|
44
|
+
* line has somewhere to go.
|
|
45
|
+
*
|
|
46
|
+
* The *whole* hole is one index, composed the way `sceneIndexForBuild` composes
|
|
47
|
+
* it: what the parse could not read plus what the walk could not reach. The
|
|
48
|
+
* completion path deliberately reads a partial universe — a suggestion claims
|
|
49
|
+
* nothing about what is absent — but a finding does, so a component declared
|
|
50
|
+
* only by an unresolved dependency suppresses the check rather than reporting
|
|
51
|
+
* the address as unreachable.
|
|
52
|
+
*/
|
|
53
|
+
export function sceneReachabilityDiagnostics(input: {
|
|
54
|
+
ts: typeof import("typescript");
|
|
55
|
+
program: ts.Program;
|
|
56
|
+
table: UrlParameterTable;
|
|
57
|
+
cache: SceneIndexCache;
|
|
58
|
+
fileName: string;
|
|
59
|
+
}): ts.Diagnostic[] {
|
|
60
|
+
const { ts: typescript, program, table, cache, fileName } = input;
|
|
61
|
+
const sourceFile = program.getSourceFile(fileName);
|
|
62
|
+
if (!sourceFile) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
const index = sceneComponentIndexOf(cache);
|
|
66
|
+
const report = checkUrlFragmentReachability({
|
|
67
|
+
program,
|
|
68
|
+
table,
|
|
69
|
+
index: { ids: index.ids, incomplete: [...index.incomplete, ...cache.documents().unreadable] },
|
|
70
|
+
objects: projectObjectIndex(cache),
|
|
71
|
+
sourceFiles: [sourceFile],
|
|
72
|
+
});
|
|
73
|
+
const fragments =
|
|
74
|
+
report.kind === "suppressed"
|
|
75
|
+
? []
|
|
76
|
+
: report.findings.map((finding) =>
|
|
77
|
+
suggestion(typescript, sourceFile, finding, UNREACHABLE_FRAGMENT_CODE),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Outside the branch above, never inside it: the two honesty rules are
|
|
81
|
+
// independent — one suppresses index-wide on an incomplete component universe,
|
|
82
|
+
// the other withholds per file on an unresolvable world — which is the reason
|
|
83
|
+
// the transpiler kept them as two exports rather than one.
|
|
84
|
+
const crossWorld = checkCrossWorldAddresses({
|
|
85
|
+
program,
|
|
86
|
+
table,
|
|
87
|
+
worldsOf: (name) => worldsFor(cache, name),
|
|
88
|
+
sourceFiles: [sourceFile],
|
|
89
|
+
}).map((finding) => suggestion(typescript, sourceFile, finding, FOREIGN_SOCKET_CODE));
|
|
90
|
+
|
|
91
|
+
return [...fragments, ...crossWorld];
|
|
92
|
+
}
|
package/src/scene-index-cache.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import type * as ts from "typescript";
|
|
2
1
|
import {
|
|
2
|
+
buildSceneCollectionRoles,
|
|
3
|
+
buildSceneComponentIndex,
|
|
4
|
+
COLLECTION_REFERENCE_EXTENSIONS,
|
|
3
5
|
displayPathOf,
|
|
6
|
+
GAME_PROJECT_DOCUMENT,
|
|
4
7
|
isExcludedProjectPath,
|
|
8
|
+
LIBRARY_DEPENDENCY_ROOT,
|
|
5
9
|
listProjectResourcePaths,
|
|
10
|
+
PROJECT_EXTENSIONS,
|
|
6
11
|
readSceneDocuments,
|
|
7
12
|
SCENE_EXTENSIONS,
|
|
13
|
+
type SceneCollectionRoles,
|
|
14
|
+
type SceneComponentIndex,
|
|
8
15
|
type SceneReadHost,
|
|
9
|
-
} from "
|
|
16
|
+
} from "@defold-typescript/transpiler";
|
|
17
|
+
import type * as ts from "typescript";
|
|
10
18
|
|
|
11
19
|
// `SceneReadHost` plus the two watch facilities `ts.System` declares as
|
|
12
20
|
// optional, typed off the editor's own callback types so the real
|
|
@@ -81,6 +89,19 @@ export function createSceneIndexCache(host: SceneWatchHost, projectRoot: string)
|
|
|
81
89
|
|
|
82
90
|
function affectsCache(hostPath: string): boolean {
|
|
83
91
|
const displayPath = displayPathOf(projectRoot, hostPath);
|
|
92
|
+
// `resolve` rewrites this surface wholesale, and `.defold-types/` is
|
|
93
|
+
// `.defignore`d — so the exclusion test below would throw the event away and
|
|
94
|
+
// leave the editor offering ids from a dependency the author just removed.
|
|
95
|
+
// The manifest counts too: it alone changes when a dependency is dropped.
|
|
96
|
+
// The root itself, not only its descendants: removing the last dependency
|
|
97
|
+
// deletes the whole directory and writes nothing inside it, so that event is
|
|
98
|
+
// the only one a watcher reports.
|
|
99
|
+
if (
|
|
100
|
+
displayPath === LIBRARY_DEPENDENCY_ROOT ||
|
|
101
|
+
displayPath.startsWith(`${LIBRARY_DEPENDENCY_ROOT}/`)
|
|
102
|
+
) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
84
105
|
if (isExcludedProjectPath(displayPath)) return false;
|
|
85
106
|
for (const extension of servedExtensions) {
|
|
86
107
|
if (displayPath.endsWith(extension)) return true;
|
|
@@ -166,3 +187,30 @@ export function createSceneIndexCache(host: SceneWatchHost, projectRoot: string)
|
|
|
166
187
|
},
|
|
167
188
|
};
|
|
168
189
|
}
|
|
190
|
+
|
|
191
|
+
// The component-id universe, whole — ids *and* the walk's own record of what it
|
|
192
|
+
// could not prove — read through the cache so one walk serves both readers. The
|
|
193
|
+
// two disagree about what to do with the hole rather than about how to build it:
|
|
194
|
+
// a completion suggests from a partial universe, a finding must not, so the
|
|
195
|
+
// index is shared here and each caller applies its own rule.
|
|
196
|
+
export function sceneComponentIndexOf(cache: SceneIndexCache): SceneComponentIndex {
|
|
197
|
+
return cache.derived("component-index", () =>
|
|
198
|
+
buildSceneComponentIndex(cache.documents().documents),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// The world each collection is, read through the cache so the completion path
|
|
203
|
+
// and the provenance path share one classification — and so the walk over the
|
|
204
|
+
// proxy/factory documents and `game.project` is invalidated by the same
|
|
205
|
+
// watchers the scene walk is. Both call sites go through here rather than
|
|
206
|
+
// building roles of their own, which is what would let one of them drift back
|
|
207
|
+
// to offering every collection's paths bare.
|
|
208
|
+
export function sceneCollectionRolesOf(cache: SceneIndexCache): SceneCollectionRoles {
|
|
209
|
+
return cache.derived("collection-roles", () =>
|
|
210
|
+
buildSceneCollectionRoles({
|
|
211
|
+
documents: cache.documents().documents,
|
|
212
|
+
references: cache.documents(COLLECTION_REFERENCE_EXTENSIONS).documents,
|
|
213
|
+
gameProject: cache.documents(PROJECT_EXTENSIONS).documents.get(GAME_PROJECT_DOCUMENT),
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
}
|