@gjsify/oxlint-plugin-gjsify 0.53.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.
@@ -0,0 +1,190 @@
1
+ // `gjsify/no-literal-widget-label` — user-visible text hard-coded into a widget from TypeScript.
2
+ //
3
+ // This is the half of the Blueprint problem that survives even after a class HAS a template: a
4
+ // label handed to a constructor or a setter as a bare string literal. It is not a style question.
5
+ // `xgettext` collects `translatable="yes"` out of Blueprint and `_("…")` out of TypeScript, and a
6
+ // bare literal is neither — so the string is invisible to extraction, absent from every catalogue,
7
+ // and shows up in the user's language only by accident. The failure is silent in the worst way:
8
+ // the interface looks untranslated rather than untranslatABLE, so it never gets filed.
9
+ //
10
+ // Two repairs, and the rule accepts both because the right one depends on the string:
11
+ // · move it into the co-located `.blp` as `title: _("…")` — correct for anything static;
12
+ // · wrap it here as `_("…")` — correct for a string a runtime value picks.
13
+ // Either turns the literal into something extraction can see, which is why wrapping silences it.
14
+ //
15
+ // Only PROSE positions are checked. `icon-name`, `css-classes`, `action-name`, a stack page's
16
+ // `name` and an `Adw.EntryRow`'s `text` (which carries user data, not a caption) are deliberately
17
+ // absent: a false finding on those would teach people to disable the rule.
18
+ //
19
+ // The setter half matches on METHOD NAME alone — the rule cannot see whether the receiver is a
20
+ // widget — so a non-GTK object with a `set_title` method is reported too. That is the deliberate
21
+ // trade: the alternative is missing every setter call on a variable whose type the linter has no
22
+ // access to, and `_()` around a genuinely non-UI string costs a catalogue entry and nothing else.
23
+ import { walk } from "./walk.js";
24
+ /** Constructor-object keys whose value is shown to a person. Both GJS spellings. */
25
+ const PROSE_PROPERTIES = new Set([
26
+ 'label',
27
+ 'title',
28
+ 'subtitle',
29
+ 'description',
30
+ 'heading',
31
+ 'body',
32
+ 'tooltipText',
33
+ 'tooltip_text',
34
+ 'placeholderText',
35
+ 'placeholder_text',
36
+ 'secondaryText',
37
+ 'secondary_text',
38
+ ]);
39
+ /**
40
+ * Setters that write the same prose positions, mapped to the PROPERTY they set — so the repair the
41
+ * message suggests is one Blueprint can actually express. `set_title: _("…")` is not a Blueprint
42
+ * property; `title` is.
43
+ */
44
+ const PROSE_SETTERS = new Map([
45
+ ['set_label', 'label'],
46
+ ['set_title', 'title'],
47
+ ['set_subtitle', 'subtitle'],
48
+ ['set_description', 'description'],
49
+ ['set_heading', 'heading'],
50
+ ['set_body', 'body'],
51
+ ['set_tooltip_text', 'tooltip-text'],
52
+ ['set_placeholder_text', 'placeholder-text'],
53
+ ]);
54
+ /**
55
+ * Prose that arrives as a LATER argument, with the index it sits at.
56
+ *
57
+ * `Adw.AlertDialog` gets its button captions only through these, and nothing else covered them: a
58
+ * dialog whose `heading` this rule reports would still ship English-only buttons, in the very same
59
+ * function. Measured across the two consumer apps: 24 live `add_response(` call sites.
60
+ *
61
+ * The surface is small and closed — `Adw.AlertDialog` and `Adw.MessageDialog` are the only classes
62
+ * in `@girs/adw-1` that declare `set_response_label`.
63
+ */
64
+ const PROSE_ARGUMENTS = new Map([
65
+ ['add_response', 1],
66
+ ['set_response_label', 1],
67
+ ]);
68
+ /**
69
+ * Is this literal worth reporting?
70
+ *
71
+ * A caption needs letters. `""`, `"—"`, `"…"`, `"%"` and `"3"` are punctuation or data with
72
+ * nothing to translate, and reporting them is how a rule loses its audience.
73
+ */
74
+ function isProse(value) {
75
+ return typeof value === 'string' && /\p{L}/u.test(value);
76
+ }
77
+ /** The string value of a plain literal (not a template, not a concatenation); else null. */
78
+ function literalString(node) {
79
+ if (!node)
80
+ return null;
81
+ if (node.type === 'Literal' && isProse(node.value))
82
+ return node.value;
83
+ // A no-substitution template literal is a literal in every way that matters here.
84
+ if (node.type === 'TemplateLiteral') {
85
+ const expressions = node.expressions;
86
+ const quasis = node.quasis;
87
+ if (expressions?.length === 0 && quasis?.length === 1) {
88
+ const cooked = quasis[0].value?.cooked;
89
+ if (isProse(cooked))
90
+ return cooked;
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+ /** `Gtk`/`Adw` namespaced callee — `new Adw.ActionRow(…)`. */
96
+ function isGtkAdwNew(node) {
97
+ if (node.type !== 'NewExpression')
98
+ return false;
99
+ const callee = node.callee;
100
+ if (!callee || callee.type !== 'MemberExpression' || callee.computed === true)
101
+ return false;
102
+ const object = callee.object;
103
+ if (object?.type !== 'Identifier')
104
+ return false;
105
+ const ns = object.name;
106
+ return ns === 'Gtk' || ns === 'Adw';
107
+ }
108
+ export const noLiteralWidgetLabelRule = {
109
+ create(context) {
110
+ /** For a caption with no declarative form: the only repair is `_()` at the call site. */
111
+ const reportInPlace = (node, method, text) => {
112
+ const shown = text.length > 40 ? `${text.slice(0, 40)}…` : text;
113
+ context.report({
114
+ message: `\`${method}\` is given the literal "${shown}". Extraction sees neither a Blueprint ` +
115
+ `\`translatable\` attribute nor a \`_()\` call here, so this text can never reach a ` +
116
+ `catalogue. There is no Blueprint property for it — the caption is added by a call — ` +
117
+ `so wrap it: \`${method}(…, _("…"))\`.`,
118
+ node,
119
+ });
120
+ };
121
+ const report = (node, key, text) => {
122
+ const shown = text.length > 40 ? `${text.slice(0, 40)}…` : text;
123
+ context.report({
124
+ message: `\`${key}\` is set to the literal "${shown}". Extraction sees neither a Blueprint ` +
125
+ `\`translatable\` attribute nor a \`_()\` call here, so this text can never reach a ` +
126
+ `catalogue. Move it into the co-located .blp as \`${key}: _("…")\`, or wrap it as \`_("…")\` ` +
127
+ `if a runtime value picks it.`,
128
+ node,
129
+ });
130
+ };
131
+ return {
132
+ Program(program) {
133
+ // The statements, NOT the Program node: oxlint hangs `tokens` and `comments` off
134
+ // Program, and `walk`'s node test is structural, so handing it Program walked the
135
+ // whole token stream — the large majority of this rule's cost, for nothing.
136
+ const visit = (node) => {
137
+ // new Gtk.X({ title: "…" })
138
+ if (isGtkAdwNew(node)) {
139
+ for (const arg of node.arguments ?? []) {
140
+ if (arg.type !== 'ObjectExpression')
141
+ continue;
142
+ for (const prop of arg.properties ?? []) {
143
+ if (prop.type !== 'Property' || prop.computed === true)
144
+ continue;
145
+ const key = prop.key;
146
+ if (key?.type !== 'Identifier')
147
+ continue;
148
+ const name = key.name;
149
+ if (!PROSE_PROPERTIES.has(name))
150
+ continue;
151
+ const text = literalString(prop.value);
152
+ if (text !== null)
153
+ report(prop, name, text);
154
+ }
155
+ }
156
+ return;
157
+ }
158
+ // widget.set_title("…") / dialog.add_response("cancel", "…")
159
+ if (node.type === 'CallExpression') {
160
+ const callee = node.callee;
161
+ if (!callee || callee.type !== 'MemberExpression' || callee.computed === true)
162
+ return;
163
+ const property = callee.property;
164
+ if (property?.type !== 'Identifier')
165
+ return;
166
+ const name = property.name;
167
+ const args = node.arguments ?? [];
168
+ const setterProperty = PROSE_SETTERS.get(name);
169
+ if (setterProperty !== undefined) {
170
+ const text = literalString(args[0]);
171
+ if (text !== null)
172
+ report(node, setterProperty, text);
173
+ return;
174
+ }
175
+ const index = PROSE_ARGUMENTS.get(name);
176
+ if (index !== undefined) {
177
+ const text = literalString(args[index]);
178
+ // No Blueprint property to name here — a response label is ADDED by a
179
+ // call, not declared — so the message says to wrap it in place.
180
+ if (text !== null)
181
+ reportInPlace(node, name, text);
182
+ }
183
+ }
184
+ };
185
+ for (const statement of program.body ?? [])
186
+ walk(statement, visit);
187
+ },
188
+ };
189
+ },
190
+ };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from './types.ts';
2
+ export declare const preferBlueprintTemplateRule: Rule;
@@ -0,0 +1,380 @@
1
+ // `gjsify/prefer-blueprint-template` — code that BUILDS a widget tree in TypeScript instead of
2
+ // declaring it in a Blueprint template.
3
+ //
4
+ // NOT ONLY A CLASS. The rule shipped visiting `ClassDeclaration`/`ClassExpression` only, and the
5
+ // one interface in this repository that every new user inherits was assembled somewhere else:
6
+ // `templates/gtk-minimal/src/index.ts` built its window, box and two labels inside the
7
+ // `activate` callback, with a literal caption no extraction can see, and reported ZERO — a green
8
+ // that meant "did not look". So the rule now also walks module scope (top-level statements,
9
+ // callbacks, factory functions) and reports the DEEPEST function that holds both signals.
10
+ //
11
+ // The incident this rule exists for is structural rather than a crash, which is why nothing else
12
+ // catches it. Measured across this workspace in 2026-08:
13
+ //
14
+ // Learn6502 (the reference app) 24 .blp 8 programmatic `new Gtk/Adw.X`
15
+ // bauplaner 0 .blp 422
16
+ // steuererklaerung 15 .blp 895 — and those 15 are empty scaffolds
17
+ //
18
+ // Learn6502 is proof that a whole application's interface fits in Blueprint. The other two grew
19
+ // the opposite way, one `append()` at a time, and the cost only shows up later and all at once:
20
+ //
21
+ // · A string built into a widget from TypeScript cannot be marked `translatable`, so xgettext
22
+ // never sees it. The interface is then untranslatable by construction, and it looks exactly
23
+ // like an interface nobody has translated yet.
24
+ // · `bind` and `notify` wiring has to be hand-written, so state and widget drift apart.
25
+ // · There is no artifact a designer can read or a screenshot tool can diff against.
26
+ //
27
+ // WHAT IS NOT A FINDING. A template that populates DATA-DRIVEN children at runtime is the
28
+ // intended pattern, not a violation — declare the container in Blueprint, fill it in TypeScript.
29
+ // Such a class has a `Template`, so it is silent here. So is a class that constructs no widget at
30
+ // all (a model, a controller, a `vfunc_snapshot` drawing widget), and so is one that constructs
31
+ // widgets but never parents any — the rule requires BOTH signals before it says anything, because
32
+ // either one alone is ordinary code.
33
+ //
34
+ // Library code that implements widgets for others (a renderer, a host, a storybook harness) is
35
+ // exempt by nature; scope the rule to application packages in `.oxlintrc.json` rather than
36
+ // disabling it line by line. A site blocked by the TOOLCHAIN rather than by its nature is the
37
+ // other case and keeps a line-level disable with its measurement, so the blocker is still
38
+ // visible where it applies and still deletable when it lifts.
39
+ //
40
+ // WHAT STILL ESCAPES, measured with fixtures rather than assumed: a class that already declares a
41
+ // `Template` and assembles a SECOND tree in one of its methods. That is the price of the promise
42
+ // two paragraphs up — a templated class is silent — and it is bounded, because such a class has
43
+ // already declared most of its interface. Everything else that was tried is caught: construction
44
+ // and parenting split across two sibling functions, a factory that returns a widget, assembly in
45
+ // a top-level statement, a method of a plain class, an object-literal method, an arrow stored in
46
+ // a const, `.tsx`, and a `vfunc_activate()` inside an application subclass.
47
+ import { isNode, memberCallName, newGtkAdwType, walk } from "./walk.js";
48
+ /**
49
+ * Methods that take a WIDGET and place it inside another one. Membership here is what separates
50
+ * "constructs a widget" from "assembles an interface" — the second is the finding.
51
+ */
52
+ const PARENTING_METHODS = new Set([
53
+ 'set_child',
54
+ 'append',
55
+ 'prepend',
56
+ 'insert_child_after',
57
+ 'add',
58
+ 'add_row',
59
+ 'add_prefix',
60
+ 'add_suffix',
61
+ 'add_child',
62
+ 'add_overlay',
63
+ 'add_titled',
64
+ 'add_named',
65
+ 'add_page',
66
+ 'add_top_bar',
67
+ 'add_bottom_bar',
68
+ 'add_toolbar',
69
+ 'set_content',
70
+ 'set_title_widget',
71
+ 'set_start_widget',
72
+ 'set_end_widget',
73
+ 'set_center_widget',
74
+ 'set_header_suffix',
75
+ 'set_extra_child',
76
+ 'attach',
77
+ ]);
78
+ /**
79
+ * Constructions that are NOT widgets, so building one is never "assembling an interface".
80
+ *
81
+ * The rule needs both its signals to be about widgets, and it cannot type-check: it sees
82
+ * `new Gtk.X` and a method NAME, never a GType. Most non-widgets are harmless because their own
83
+ * API shares no name with a parenting method — `new Gtk.GestureClick()` + `add_controller` is
84
+ * already silent. The ones that DO collide are listed here, `Gtk.StringList` being the case that
85
+ * actually occurs: a combo row filling a model with `.append(<string>)` was reported as assembling
86
+ * an interface, which is exactly the model case the header promises is silent.
87
+ *
88
+ * Families are matched by prefix below rather than enumerated, because their members are many and
89
+ * the list would drift against the GIR.
90
+ */
91
+ const NON_WIDGET_CONSTRUCTIONS = new Set([
92
+ 'Gtk.Adjustment',
93
+ 'Gtk.AnyFilter',
94
+ 'Gtk.BoolFilter',
95
+ 'Gtk.Builder',
96
+ 'Gtk.BuilderListItemFactory',
97
+ 'Gtk.CssProvider',
98
+ 'Gtk.CustomFilter',
99
+ 'Gtk.CustomSorter',
100
+ 'Gtk.EntryBuffer',
101
+ 'Gtk.EveryFilter',
102
+ 'Gtk.FileDialog',
103
+ 'Gtk.FileFilter',
104
+ 'Gtk.ListStore',
105
+ 'Gtk.NumericSorter',
106
+ 'Gtk.SignalListItemFactory',
107
+ 'Gtk.SizeGroup',
108
+ 'Gtk.StringFilter',
109
+ 'Gtk.StringList',
110
+ 'Gtk.StringObject',
111
+ 'Gtk.StringSorter',
112
+ 'Gtk.TextBuffer',
113
+ 'Gtk.TextTag',
114
+ 'Gtk.TextTagTable',
115
+ 'Adw.Breakpoint',
116
+ 'Adw.BreakpointCondition',
117
+ 'Adw.CallbackAnimationTarget',
118
+ 'Adw.EnumListModel',
119
+ 'Adw.PropertyAnimationTarget',
120
+ 'Adw.SpringAnimation',
121
+ 'Adw.SpringParams',
122
+ 'Adw.StyleManager',
123
+ 'Adw.TimedAnimation',
124
+ 'Adw.Toast',
125
+ // An application object is not a widget, and every scaffold constructs one at module scope.
126
+ // Listed here (not only in `NON_WIDGET_BASES`, which answers a different question) so that
127
+ // `new Adw.Application(…)` is not the construction half of a module-scope finding.
128
+ 'Gtk.Application',
129
+ 'Adw.Application',
130
+ ]);
131
+ /** Prefixes whose whole family is non-widget: controllers, gestures and list models. */
132
+ const NON_WIDGET_PREFIXES = ['Gtk.EventController', 'Gtk.Gesture', 'Gtk.MultiSorter'];
133
+ /** Suffixes covering the list-model and selection families without enumerating them. */
134
+ const NON_WIDGET_SUFFIXES = ['ListModel', 'Selection'];
135
+ function isNonWidgetConstruction(type) {
136
+ if (NON_WIDGET_CONSTRUCTIONS.has(type))
137
+ return true;
138
+ if (NON_WIDGET_PREFIXES.some((prefix) => type.startsWith(prefix)))
139
+ return true;
140
+ return NON_WIDGET_SUFFIXES.some((suffix) => type.endsWith(suffix));
141
+ }
142
+ /**
143
+ * Bases that are NOT widgets, so there is no interface for a template to hold. `Gtk.Application` /
144
+ * `Adw.Application` are the ones that actually occur: an application object legitimately builds a
145
+ * `Gtk.CssProvider` and an `Adw.AboutDialog` from its own metadata, and reporting that taught the
146
+ * rule's first reader that it cries wolf.
147
+ */
148
+ const NON_WIDGET_BASES = new Set(['Gtk.Application', 'Adw.Application']);
149
+ /** `class X extends Gtk.Widget` / `extends Adw.Bin` → the base name; null for anything else. */
150
+ function gtkAdwSuperClass(node) {
151
+ const superClass = node.superClass;
152
+ if (!superClass || superClass.type !== 'MemberExpression' || superClass.computed === true)
153
+ return null;
154
+ const object = superClass.object;
155
+ const property = superClass.property;
156
+ if (object?.type !== 'Identifier' || property?.type !== 'Identifier')
157
+ return null;
158
+ const ns = object.name;
159
+ if (ns !== 'Gtk' && ns !== 'Adw')
160
+ return null;
161
+ const base = `${ns}.${property.name}`;
162
+ return NON_WIDGET_BASES.has(base) ? null : base;
163
+ }
164
+ /** A `Template` key directly on this object literal — not nested anywhere inside it. */
165
+ function objectDeclaresTemplate(node) {
166
+ if (node.type !== 'ObjectExpression')
167
+ return false;
168
+ for (const prop of node.properties ?? []) {
169
+ if (prop.type !== 'Property' || prop.computed === true)
170
+ continue;
171
+ const key = prop.key;
172
+ if (key?.type === 'Identifier' && key.name === 'Template')
173
+ return true;
174
+ }
175
+ return false;
176
+ }
177
+ /** A `registerClass(…)` call inside this static block whose metadata object carries `Template`. */
178
+ function staticBlockDeclaresTemplate(block) {
179
+ let found = false;
180
+ walk(block, (node) => {
181
+ if (found || node.type !== 'CallExpression')
182
+ return;
183
+ const callee = node.callee;
184
+ const named = (callee?.type === 'MemberExpression' &&
185
+ callee.computed !== true &&
186
+ callee.property?.type === 'Identifier' &&
187
+ callee.property.name === 'registerClass') ||
188
+ (callee?.type === 'Identifier' && callee.name === 'registerClass');
189
+ if (!named)
190
+ return;
191
+ for (const arg of node.arguments ?? []) {
192
+ if (objectDeclaresTemplate(arg))
193
+ found = true;
194
+ }
195
+ });
196
+ return found;
197
+ }
198
+ /**
199
+ * True when THIS class body declares a `Template`: a `static Template` field of its own, or a
200
+ * `registerClass` metadata object inside its own `static {}` block, which is this repo's
201
+ * convention (see the sibling `gjsify/register-class-order`).
202
+ *
203
+ * Deliberately NOT a subtree search. An unbounded walk for the identifier `Template` was silenced
204
+ * by things that declare nothing — a data literal that merely uses the word (`{ Template: false }`,
205
+ * `{ columns: [{ Template: 'row.ui' }] }`), and a NESTED helper class carrying the only real
206
+ * template. The rule's whole value is that it cannot be switched off by accident, so an accidental
207
+ * silencer is worse here than a missed case.
208
+ *
209
+ * A class defined INSIDE the static block still counts, which is pathological enough to leave.
210
+ */
211
+ function declaresTemplate(body) {
212
+ for (const element of body.body) {
213
+ if (element.type === 'PropertyDefinition' || element.type === 'TSAbstractPropertyDefinition') {
214
+ const field = element;
215
+ const key = field.key;
216
+ if (field.static && !field.computed && key.type === 'Identifier' && key.name === 'Template') {
217
+ return true;
218
+ }
219
+ continue;
220
+ }
221
+ if (element.type === 'StaticBlock' && staticBlockDeclaresTemplate(element))
222
+ return true;
223
+ }
224
+ return false;
225
+ }
226
+ /** The nodes that own a scope here: a function is the unit a reader can move into a `.blp`. */
227
+ const SCOPE_TYPES = new Set(['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression']);
228
+ /**
229
+ * How a scope is named in its own report. A class gets to be named by its base; a function has no
230
+ * base, so the message names the SHAPE the reader is looking at instead of saying "somewhere".
231
+ */
232
+ function describeScope(node) {
233
+ if (node.type === 'Program')
234
+ return 'This module assembles an interface at top level';
235
+ if (node.type === 'FunctionDeclaration') {
236
+ const id = node.id;
237
+ const name = id?.type === 'Identifier' ? id.name : null;
238
+ return name === null ? 'This function assembles an interface' : `\`${name}()\` assembles an interface`;
239
+ }
240
+ return 'This callback assembles an interface';
241
+ }
242
+ /**
243
+ * Depth-first descent that attributes every construction and every parenting call to the nearest
244
+ * enclosing function, and refuses to enter a Gtk/Adw class body.
245
+ *
246
+ * It cannot be {@link walk}: walk's visitor is flat, and this needs a scope that is pushed on the
247
+ * way down and popped on the way out. The three skipped keys are walk's, for walk's reasons
248
+ * (`parent` loops; `tokens`/`comments` hang off Program and pass the structural node test).
249
+ */
250
+ function descend(node, scope) {
251
+ // Skip exactly what `checkClass` owns — a widget subclass — so the two halves can never
252
+ // report one tree twice. NOT the wider `extends Gtk.*|Adw.*` test: that also skipped the
253
+ // APPLICATION bases, and an `activate` handler moved from a callback into
254
+ // `vfunc_activate()` of a `Gtk.Application` subclass then escaped the rule completely,
255
+ // which is the same bug this rule exists for wearing the most idiomatic GJS spelling.
256
+ // Measured before widening the entry: 0 new findings across this repository, and the
257
+ // false positive the application exemption was written for does not come back —
258
+ // `Gtk.CssProvider` is already a non-widget construction and an `Adw.AboutDialog` is
259
+ // presented rather than parented, so the both-signals requirement stays unsatisfied.
260
+ if ((node.type === 'ClassDeclaration' || node.type === 'ClassExpression') && gtkAdwSuperClass(node) !== null) {
261
+ return;
262
+ }
263
+ let current = scope;
264
+ if (SCOPE_TYPES.has(node.type)) {
265
+ current = { node, constructed: [], parents: false, children: [] };
266
+ scope.children.push(current);
267
+ }
268
+ const type = newGtkAdwType(node);
269
+ if (type !== null) {
270
+ if (!isNonWidgetConstruction(type) && !current.constructed.includes(type))
271
+ current.constructed.push(type);
272
+ }
273
+ else {
274
+ const method = memberCallName(node);
275
+ if (method !== null && PARENTING_METHODS.has(method))
276
+ current.parents = true;
277
+ }
278
+ for (const key of Object.keys(node)) {
279
+ if (key === 'parent' || key === 'tokens' || key === 'comments')
280
+ continue;
281
+ const value = node[key];
282
+ if (Array.isArray(value)) {
283
+ for (const item of value)
284
+ if (isNode(item))
285
+ descend(item, current);
286
+ }
287
+ else if (isNode(value)) {
288
+ descend(value, current);
289
+ }
290
+ }
291
+ }
292
+ /** `Gtk.Box, Gtk.Label, Gtk.Button, +2 more` — the evidence, capped so a message stays readable. */
293
+ function listConstructed(constructed) {
294
+ const shown = constructed.slice(0, 3).join(', ');
295
+ return constructed.length > 3 ? `${shown}, +${constructed.length - 3} more` : shown;
296
+ }
297
+ /** The half of the message that is the same wherever the assembly happened. */
298
+ const WHY = 'a string set from TypeScript can never be marked translatable, so an interface built this ' +
299
+ 'way cannot be translated at all. Populating data-driven children inside a template is fine ' +
300
+ 'and is not reported.';
301
+ export const preferBlueprintTemplateRule = {
302
+ create(context) {
303
+ const checkClass = (node) => {
304
+ const base = gtkAdwSuperClass(node);
305
+ if (base === null)
306
+ return;
307
+ const body = node.body;
308
+ if (!body || body.type !== 'ClassBody')
309
+ return;
310
+ if (declaresTemplate(body))
311
+ return;
312
+ const constructed = [];
313
+ let parents = false;
314
+ walk(body, (inner) => {
315
+ const type = newGtkAdwType(inner);
316
+ if (type !== null) {
317
+ if (!isNonWidgetConstruction(type) && !constructed.includes(type))
318
+ constructed.push(type);
319
+ return;
320
+ }
321
+ const method = memberCallName(inner);
322
+ if (method !== null && PARENTING_METHODS.has(method))
323
+ parents = true;
324
+ });
325
+ // BOTH signals, deliberately: constructing without parenting is a model or an
326
+ // adjustment, and parenting without constructing is moving an existing widget.
327
+ if (constructed.length === 0 || !parents)
328
+ return;
329
+ context.report({
330
+ message: `\`${base}\` subclass assembles its interface in TypeScript ` +
331
+ `(${listConstructed(constructed)}) with no Blueprint \`Template\`. Declare the widget ` +
332
+ `tree in a co-located \`.blp\` and keep only logic here — ${WHY}`,
333
+ node,
334
+ });
335
+ };
336
+ /**
337
+ * Report the DEEPEST scope that holds both signals, once.
338
+ *
339
+ * Signals bubble outward so that a function which builds a container and a callback which
340
+ * fills it are one finding rather than two halves that each look ordinary; reporting at the
341
+ * innermost scope that is already complete then puts the message on the code a reader can
342
+ * actually move into a `.blp`, instead of on the whole module.
343
+ */
344
+ const settle = (scope) => {
345
+ const constructed = [...scope.constructed];
346
+ let parents = scope.parents;
347
+ let reported = false;
348
+ for (const child of scope.children) {
349
+ const inner = settle(child);
350
+ for (const type of inner.constructed)
351
+ if (!constructed.includes(type))
352
+ constructed.push(type);
353
+ if (inner.parents)
354
+ parents = true;
355
+ if (inner.reported)
356
+ reported = true;
357
+ }
358
+ if (!reported && constructed.length > 0 && parents) {
359
+ context.report({
360
+ message: `${describeScope(scope.node)} in TypeScript (${listConstructed(constructed)}) outside ` +
361
+ `any widget class, so no Blueprint \`Template\` can hold it. Declare the widget tree ` +
362
+ `in a co-located \`.blp\`, register it on a widget subclass and keep only logic here ` +
363
+ `— ${WHY}`,
364
+ node: scope.node,
365
+ });
366
+ reported = true;
367
+ }
368
+ return { constructed, parents, reported };
369
+ };
370
+ const checkModule = (program) => {
371
+ // The statements, not the Program node: `descend` skips `tokens`/`comments` by key, but
372
+ // starting at the body keeps that a belt-and-braces guard rather than the only one.
373
+ const root = { node: program, constructed: [], parents: false, children: [] };
374
+ for (const statement of program.body ?? [])
375
+ descend(statement, root);
376
+ settle(root);
377
+ };
378
+ return { ClassDeclaration: checkClass, ClassExpression: checkClass, Program: checkModule };
379
+ },
380
+ };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from './types.ts';
2
+ export declare const registerClassOrderRule: Rule;