@rettangoli/check 0.0.1

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.
Files changed (42) hide show
  1. package/README.md +295 -0
  2. package/ROADMAP.md +175 -0
  3. package/package.json +46 -0
  4. package/src/cli/bin.js +325 -0
  5. package/src/cli/check.js +232 -0
  6. package/src/cli/index.js +1 -0
  7. package/src/core/analyze.js +227 -0
  8. package/src/core/discovery.js +83 -0
  9. package/src/core/exportedFunctions.js +235 -0
  10. package/src/core/model.js +898 -0
  11. package/src/core/parsers.js +2726 -0
  12. package/src/core/registry.js +779 -0
  13. package/src/core/schema.js +161 -0
  14. package/src/core/scopeGraph.js +1400 -0
  15. package/src/core/semantic.js +329 -0
  16. package/src/diagnostics/autofix.js +191 -0
  17. package/src/diagnostics/catalog.js +89 -0
  18. package/src/index.js +2 -0
  19. package/src/reporters/index.js +13 -0
  20. package/src/reporters/json.js +42 -0
  21. package/src/reporters/sarif.js +213 -0
  22. package/src/reporters/text.js +145 -0
  23. package/src/rules/compatibility.js +318 -0
  24. package/src/rules/constants.js +22 -0
  25. package/src/rules/crossFileSymbols.js +108 -0
  26. package/src/rules/expression.js +338 -0
  27. package/src/rules/feParity.js +65 -0
  28. package/src/rules/index.js +39 -0
  29. package/src/rules/jempl.js +80 -0
  30. package/src/rules/lifecycle.js +4 -0
  31. package/src/rules/listenerConfig.js +556 -0
  32. package/src/rules/listenerSymbols.js +49 -0
  33. package/src/rules/methods.js +117 -0
  34. package/src/rules/refs.js +20 -0
  35. package/src/rules/schema.js +118 -0
  36. package/src/rules/shared.js +20 -0
  37. package/src/rules/yahtmlAttrs.js +238 -0
  38. package/src/semantic/engine.js +778 -0
  39. package/src/semantic/index.js +9 -0
  40. package/src/types/lattice.js +281 -0
  41. package/src/utils/case.js +9 -0
  42. package/src/utils/fs.js +30 -0
@@ -0,0 +1,556 @@
1
+ import {
2
+ getListenerSymbols,
3
+ isListenerEventConfig,
4
+ isValidHandlerSymbol,
5
+ resolveListenerLine,
6
+ } from "./listenerSymbols.js";
7
+ import { collectInvalidRefKeys } from "./refs.js";
8
+ import { getModelFilePath, getYamlPathLine, isObjectRecord } from "./shared.js";
9
+ import {
10
+ validateElementIdForRefs,
11
+ validateEventConfig as validateFeEventConfig,
12
+ } from "@rettangoli/fe/contracts";
13
+ import { parseSync } from "oxc-parser";
14
+ import { parseNamedExportedFunctions } from "../core/exportedFunctions.js";
15
+ const GLOBAL_REF_KEYS = new Set(["window", "document"]);
16
+
17
+ const BOOLEAN_OPTIONS = new Set([
18
+ "preventDefault",
19
+ "stopPropagation",
20
+ "stopImmediatePropagation",
21
+ "targetOnly",
22
+ "once",
23
+ ]);
24
+
25
+ const NUMBER_OPTIONS = new Set([
26
+ "debounce",
27
+ "throttle",
28
+ ]);
29
+
30
+ const SYMBOL_OPTIONS = [
31
+ "handler",
32
+ "action",
33
+ ];
34
+
35
+ const TEMPLATE_OPTIONS = [
36
+ "payload",
37
+ ];
38
+
39
+ const KNOWN_LISTENER_OPTIONS = new Set([
40
+ ...SYMBOL_OPTIONS,
41
+ ...TEMPLATE_OPTIONS,
42
+ ...BOOLEAN_OPTIONS,
43
+ ...NUMBER_OPTIONS,
44
+ ]);
45
+
46
+ const getSelectorId = (selector = "") => {
47
+ if (typeof selector !== "string") {
48
+ return null;
49
+ }
50
+ const match = selector.match(/#([^.#\s]+)/);
51
+ return match ? match[1] : null;
52
+ };
53
+
54
+ const isIdRefKeyCandidate = (refKey = "") => {
55
+ if (typeof refKey !== "string" || !refKey) {
56
+ return false;
57
+ }
58
+ if (GLOBAL_REF_KEYS.has(refKey)) {
59
+ return false;
60
+ }
61
+ return !refKey.startsWith(".");
62
+ };
63
+
64
+ const isDynamicTemplateToken = (value = "") => {
65
+ if (typeof value !== "string") {
66
+ return false;
67
+ }
68
+ return value.includes("${") || value.includes("#{") || value.includes("{{");
69
+ };
70
+
71
+ const normalizeEventConfigForFeValidation = ({ eventConfig, listenerSymbols }) => {
72
+ if (!isObjectRecord(eventConfig)) {
73
+ return eventConfig;
74
+ }
75
+
76
+ const normalized = { ...eventConfig };
77
+ const invalidHandler = listenerSymbols.handler.isDefined && !listenerSymbols.handler.isValid;
78
+ const invalidAction = listenerSymbols.action.isDefined && !listenerSymbols.action.isValid;
79
+
80
+ if (!invalidHandler && !invalidAction) {
81
+ return normalized;
82
+ }
83
+
84
+ delete normalized.handler;
85
+ delete normalized.action;
86
+
87
+ if (listenerSymbols.handler.isValid) {
88
+ normalized.handler = listenerSymbols.handler.value;
89
+ }
90
+ if (listenerSymbols.action.isValid) {
91
+ normalized.action = listenerSymbols.action.value;
92
+ }
93
+
94
+ if (!normalized.handler && !normalized.action) {
95
+ normalized.handler = "__invalid_dispatch_symbol__";
96
+ }
97
+
98
+ return normalized;
99
+ };
100
+
101
+ const mapFeEventValidationError = ({
102
+ errMessage,
103
+ listenerLine,
104
+ optionLines,
105
+ model,
106
+ eventType,
107
+ refKey,
108
+ }) => {
109
+ if (errMessage.includes("Each listener can have handler or action but not both")) {
110
+ return {
111
+ code: "RTGL-CHECK-LISTENER-002",
112
+ line: resolveListenerLine({
113
+ listenerLine,
114
+ optionLines,
115
+ preferredKeys: ["handler", "action"],
116
+ }),
117
+ message: `${model.componentKey}: event '${eventType}' on ref '${refKey}' cannot define both handler and action.`,
118
+ };
119
+ }
120
+
121
+ if (errMessage.includes("Each listener must define either handler or action")) {
122
+ return {
123
+ code: "RTGL-CHECK-LISTENER-003",
124
+ line: resolveListenerLine({ listenerLine, optionLines }),
125
+ message: `${model.componentKey}: event '${eventType}' on ref '${refKey}' must define either handler or action.`,
126
+ };
127
+ }
128
+
129
+ const invalidOptionMatch = errMessage.match(/Invalid '([^']+)'/);
130
+ if (invalidOptionMatch) {
131
+ const optionName = invalidOptionMatch[1];
132
+ const code = BOOLEAN_OPTIONS.has(optionName)
133
+ ? "RTGL-CHECK-LISTENER-004"
134
+ : "RTGL-CHECK-LISTENER-005";
135
+ const expectedText = BOOLEAN_OPTIONS.has(optionName)
136
+ ? "boolean"
137
+ : "non-negative number";
138
+
139
+ return {
140
+ code,
141
+ optionName,
142
+ line: resolveListenerLine({
143
+ listenerLine,
144
+ optionLines,
145
+ preferredKeys: [optionName],
146
+ }),
147
+ message: `${model.componentKey}: invalid '${optionName}' for event '${eventType}' on ref '${refKey}'. Expected ${expectedText}.`,
148
+ };
149
+ }
150
+
151
+ if (errMessage.includes("cannot define both 'debounce' and 'throttle'")) {
152
+ return {
153
+ code: "RTGL-CHECK-LISTENER-006",
154
+ line: resolveListenerLine({
155
+ listenerLine,
156
+ optionLines,
157
+ preferredKeys: ["debounce", "throttle"],
158
+ }),
159
+ message: `${model.componentKey}: event '${eventType}' on ref '${refKey}' cannot define both debounce and throttle.`,
160
+ };
161
+ }
162
+
163
+ if (errMessage.includes("Invalid event config")) {
164
+ return {
165
+ code: "RTGL-CHECK-LISTENER-001",
166
+ line: resolveListenerLine({ listenerLine, optionLines }),
167
+ message: `${model.componentKey}: invalid event config for event '${eventType}' on ref '${refKey}'.`,
168
+ };
169
+ }
170
+
171
+ return null;
172
+ };
173
+
174
+ const applyValidationFix = ({ validationConfig, issue }) => {
175
+ if (!isObjectRecord(validationConfig) || !issue?.code) {
176
+ return false;
177
+ }
178
+
179
+ if (issue.code === "RTGL-CHECK-LISTENER-002") {
180
+ if (Object.prototype.hasOwnProperty.call(validationConfig, "action")) {
181
+ delete validationConfig.action;
182
+ return true;
183
+ }
184
+ if (Object.prototype.hasOwnProperty.call(validationConfig, "handler")) {
185
+ delete validationConfig.handler;
186
+ return true;
187
+ }
188
+ return false;
189
+ }
190
+
191
+ if (issue.code === "RTGL-CHECK-LISTENER-003") {
192
+ validationConfig.handler = "__missing_dispatch__";
193
+ return true;
194
+ }
195
+
196
+ if (issue.code === "RTGL-CHECK-LISTENER-004") {
197
+ if (!issue.optionName) {
198
+ return false;
199
+ }
200
+ validationConfig[issue.optionName] = false;
201
+ return true;
202
+ }
203
+
204
+ if (issue.code === "RTGL-CHECK-LISTENER-005") {
205
+ if (!issue.optionName) {
206
+ return false;
207
+ }
208
+ validationConfig[issue.optionName] = 0;
209
+ return true;
210
+ }
211
+
212
+ if (issue.code === "RTGL-CHECK-LISTENER-006") {
213
+ if (Object.prototype.hasOwnProperty.call(validationConfig, "throttle")) {
214
+ delete validationConfig.throttle;
215
+ return true;
216
+ }
217
+ if (Object.prototype.hasOwnProperty.call(validationConfig, "debounce")) {
218
+ delete validationConfig.debounce;
219
+ return true;
220
+ }
221
+ return false;
222
+ }
223
+
224
+ return false;
225
+ };
226
+
227
+ const getObjectExpressionKeys = (expressionNode) => {
228
+ if (expressionNode?.type === "ParenthesizedExpression") {
229
+ return getObjectExpressionKeys(expressionNode.expression);
230
+ }
231
+
232
+ if (!expressionNode || expressionNode.type !== "ObjectExpression" || !Array.isArray(expressionNode.properties)) {
233
+ return null;
234
+ }
235
+
236
+ const keys = new Set();
237
+ expressionNode.properties.forEach((property) => {
238
+ if (!property || property.type !== "Property" || property.computed) {
239
+ return;
240
+ }
241
+ if (property.key?.type === "Identifier" && property.key.name) {
242
+ keys.add(property.key.name);
243
+ return;
244
+ }
245
+ if (property.key?.type === "StringLiteral" && property.key.value) {
246
+ keys.add(property.key.value);
247
+ return;
248
+ }
249
+ if (property.key?.type === "Literal" && typeof property.key.value === "string" && property.key.value) {
250
+ keys.add(property.key.value);
251
+ }
252
+ });
253
+
254
+ return keys;
255
+ };
256
+
257
+ const parsePayloadObjectKeys = (payloadSource = "") => {
258
+ const trimmed = String(payloadSource || "").trim();
259
+ if (!trimmed || !trimmed.startsWith("{") || !trimmed.endsWith("}")) {
260
+ return null;
261
+ }
262
+
263
+ try {
264
+ const parsed = parseSync("listener-payload.js", `const __payload = (${trimmed});`, {
265
+ sourceType: "module",
266
+ });
267
+ if (Array.isArray(parsed?.errors) && parsed.errors.length > 0) {
268
+ return null;
269
+ }
270
+
271
+ const declaration = parsed?.program?.body?.[0];
272
+ if (
273
+ !declaration
274
+ || declaration.type !== "VariableDeclaration"
275
+ || !Array.isArray(declaration.declarations)
276
+ || declaration.declarations.length === 0
277
+ ) {
278
+ return null;
279
+ }
280
+
281
+ const init = declaration.declarations[0]?.init;
282
+ return getObjectExpressionKeys(init);
283
+ } catch {
284
+ return null;
285
+ }
286
+ };
287
+
288
+ export const runListenerConfigRules = ({ models = [] }) => {
289
+ const diagnostics = [];
290
+
291
+ models.forEach((model) => {
292
+ const viewFilePath = getModelFilePath({ model, fileType: "view" });
293
+ const handlerFunctions = parseNamedExportedFunctions({
294
+ sourceText: model?.handlers?.sourceText || "",
295
+ filePath: model?.handlers?.filePath || model?.files?.handlers || "unknown.handlers.js",
296
+ });
297
+ const actionFunctions = parseNamedExportedFunctions({
298
+ sourceText: model?.store?.sourceText || "",
299
+ filePath: model?.store?.filePath || model?.files?.store || "unknown.store.js",
300
+ });
301
+ const refs = model?.view?.yaml?.refs;
302
+ const invalidRefKeys = collectInvalidRefKeys(refs);
303
+
304
+ if (isObjectRecord(refs)) {
305
+ invalidRefKeys.forEach((refKey) => {
306
+ diagnostics.push({
307
+ code: "RTGL-CHECK-REF-001",
308
+ severity: "error",
309
+ filePath: viewFilePath,
310
+ line: getYamlPathLine(model?.view?.yamlKeyPathLines, ["refs", refKey]),
311
+ message: `${model.componentKey}: invalid ref key '${refKey}'.`,
312
+ });
313
+ });
314
+
315
+ Object.entries(refs).forEach(([refKey, refConfig]) => {
316
+ if (!isObjectRecord(refConfig)) {
317
+ return;
318
+ }
319
+ if (!Object.prototype.hasOwnProperty.call(refConfig, "eventListeners")) {
320
+ return;
321
+ }
322
+ if (isObjectRecord(refConfig.eventListeners)) {
323
+ return;
324
+ }
325
+
326
+ diagnostics.push({
327
+ code: "RTGL-CHECK-LISTENER-009",
328
+ severity: "error",
329
+ filePath: viewFilePath,
330
+ line: getYamlPathLine(model?.view?.yamlKeyPathLines, ["refs", refKey, "eventListeners"]),
331
+ message: `${model.componentKey}: invalid eventListeners config on ref '${refKey}'. Expected an object keyed by event type.`,
332
+ });
333
+ });
334
+ }
335
+
336
+ const hasIdRefMatchers = isObjectRecord(refs)
337
+ ? Object.keys(refs).some((refKey) => isIdRefKeyCandidate(refKey))
338
+ : false;
339
+
340
+ if (hasIdRefMatchers) {
341
+ model.view.selectorBindings.forEach((bindingLine) => {
342
+ const selectorId = getSelectorId(bindingLine.selector);
343
+ if (!selectorId) {
344
+ return;
345
+ }
346
+ if (isDynamicTemplateToken(selectorId)) {
347
+ return;
348
+ }
349
+
350
+ try {
351
+ validateElementIdForRefs(selectorId);
352
+ } catch {
353
+ diagnostics.push({
354
+ code: "RTGL-CHECK-REF-002",
355
+ severity: "error",
356
+ filePath: viewFilePath,
357
+ line: bindingLine.line,
358
+ message: `${model.componentKey}: invalid element id '${selectorId}' for refs matching. Use camelCase ids.`,
359
+ });
360
+ }
361
+ });
362
+ }
363
+
364
+ model.view.refListeners.forEach(({ refKey, eventType, eventConfig, line, optionLines }) => {
365
+ if (invalidRefKeys.has(refKey)) {
366
+ return;
367
+ }
368
+
369
+ const listenerLine = resolveListenerLine({ listenerLine: line, optionLines });
370
+
371
+ if (!isListenerEventConfig(eventConfig)) {
372
+ diagnostics.push({
373
+ code: "RTGL-CHECK-LISTENER-001",
374
+ severity: "error",
375
+ filePath: viewFilePath,
376
+ line: listenerLine,
377
+ message: `${model.componentKey}: invalid event config for event '${eventType}' on ref '${refKey}'.`,
378
+ });
379
+ return;
380
+ }
381
+
382
+ Object.keys(eventConfig).forEach((optionName) => {
383
+ if (KNOWN_LISTENER_OPTIONS.has(optionName)) {
384
+ return;
385
+ }
386
+
387
+ diagnostics.push({
388
+ code: "RTGL-CHECK-LISTENER-008",
389
+ severity: "error",
390
+ filePath: viewFilePath,
391
+ line: resolveListenerLine({
392
+ listenerLine: line,
393
+ optionLines,
394
+ preferredKeys: [optionName],
395
+ }),
396
+ message: `${model.componentKey}: unknown listener option '${optionName}' for event '${eventType}' on ref '${refKey}'.`,
397
+ });
398
+ });
399
+
400
+ const listenerSymbols = getListenerSymbols(eventConfig);
401
+ const symbolEntries = [
402
+ ["handler", listenerSymbols.handler],
403
+ ["action", listenerSymbols.action],
404
+ ];
405
+
406
+ symbolEntries.forEach(([symbolName, symbol]) => {
407
+ if (symbol.isDefined && !symbol.isValid) {
408
+ diagnostics.push({
409
+ code: "RTGL-CHECK-LISTENER-007",
410
+ severity: "error",
411
+ filePath: viewFilePath,
412
+ line: resolveListenerLine({
413
+ listenerLine: line,
414
+ optionLines,
415
+ preferredKeys: [symbolName],
416
+ }),
417
+ message: `${model.componentKey}: invalid '${symbolName}' for event '${eventType}' on ref '${refKey}'. Expected a valid symbol name.`,
418
+ });
419
+ }
420
+ });
421
+
422
+ if (listenerSymbols.handler.isValid && !isValidHandlerSymbol(listenerSymbols.handler.value)) {
423
+ diagnostics.push({
424
+ code: "RTGL-CHECK-HANDLER-001",
425
+ severity: "error",
426
+ filePath: viewFilePath,
427
+ line: resolveListenerLine({
428
+ listenerLine: line,
429
+ optionLines,
430
+ preferredKeys: ["handler"],
431
+ }),
432
+ message: `${model.componentKey}: invalid handler '${listenerSymbols.handler.value}' for event '${eventType}' on ref '${refKey}'. Handler names must start with 'handle'.`,
433
+ });
434
+ }
435
+
436
+ const normalizedEventConfig = normalizeEventConfigForFeValidation({
437
+ eventConfig,
438
+ listenerSymbols,
439
+ });
440
+
441
+ const validationConfig = isObjectRecord(normalizedEventConfig)
442
+ ? { ...normalizedEventConfig }
443
+ : normalizedEventConfig;
444
+ const seenValidationIssues = new Set();
445
+
446
+ for (let iteration = 0; iteration < 16; iteration += 1) {
447
+ try {
448
+ validateFeEventConfig({
449
+ eventType,
450
+ eventConfig: validationConfig,
451
+ refKey,
452
+ });
453
+ break;
454
+ } catch (err) {
455
+ const mappedError = mapFeEventValidationError({
456
+ errMessage: String(err?.message || ""),
457
+ listenerLine: line,
458
+ optionLines,
459
+ model,
460
+ eventType,
461
+ refKey,
462
+ });
463
+
464
+ if (!mappedError) {
465
+ break;
466
+ }
467
+
468
+ const issueKey = `${mappedError.code}:${mappedError.line || 0}:${mappedError.optionName || ""}`;
469
+ if (seenValidationIssues.has(issueKey)) {
470
+ break;
471
+ }
472
+ seenValidationIssues.add(issueKey);
473
+
474
+ diagnostics.push({
475
+ code: mappedError.code,
476
+ severity: "error",
477
+ filePath: viewFilePath,
478
+ line: mappedError.line,
479
+ message: mappedError.message,
480
+ });
481
+
482
+ const didApplyFix = applyValidationFix({
483
+ validationConfig,
484
+ issue: mappedError,
485
+ });
486
+ if (!didApplyFix) {
487
+ break;
488
+ }
489
+ }
490
+ }
491
+
492
+ const payloadObjectKeys = parsePayloadObjectKeys(eventConfig.payload);
493
+ if (!(payloadObjectKeys instanceof Set) || payloadObjectKeys.size === 0) {
494
+ return;
495
+ }
496
+
497
+ const payloadLine = resolveListenerLine({
498
+ listenerLine: line,
499
+ optionLines,
500
+ preferredKeys: ["payload"],
501
+ });
502
+
503
+ const validateSymbolPayloadContract = ({
504
+ symbolName,
505
+ symbolType,
506
+ functionMap,
507
+ }) => {
508
+ if (!symbolName || !(functionMap instanceof Map)) {
509
+ return;
510
+ }
511
+
512
+ const functionMeta = functionMap.get(symbolName);
513
+ if (!functionMeta || functionMeta.secondParam?.kind !== "object" || functionMeta.secondParam?.hasRest) {
514
+ return;
515
+ }
516
+
517
+ const requiredPayloadKeys = new Set(functionMeta.secondParam.objectKeys || []);
518
+ if (requiredPayloadKeys.size === 0) {
519
+ return;
520
+ }
521
+
522
+ const missingKeys = [...requiredPayloadKeys]
523
+ .filter((key) => !payloadObjectKeys.has(key))
524
+ .sort((left, right) => left.localeCompare(right));
525
+ if (missingKeys.length === 0) {
526
+ return;
527
+ }
528
+
529
+ diagnostics.push({
530
+ code: "RTGL-CHECK-CONTRACT-004",
531
+ severity: "error",
532
+ filePath: viewFilePath,
533
+ line: payloadLine,
534
+ message: `${model.componentKey}: listener payload for '${symbolType}' '${symbolName}' on ref '${refKey}' is missing required key(s) [${missingKeys.join(", ")}].`,
535
+ });
536
+ };
537
+
538
+ if (listenerSymbols.handler.isValid && model.handlers.exports.has(listenerSymbols.handler.value)) {
539
+ validateSymbolPayloadContract({
540
+ symbolName: listenerSymbols.handler.value,
541
+ symbolType: "handler",
542
+ functionMap: handlerFunctions,
543
+ });
544
+ }
545
+ if (listenerSymbols.action.isValid && model.store.exports.has(listenerSymbols.action.value)) {
546
+ validateSymbolPayloadContract({
547
+ symbolName: listenerSymbols.action.value,
548
+ symbolType: "action",
549
+ functionMap: actionFunctions,
550
+ });
551
+ }
552
+ });
553
+ });
554
+
555
+ return diagnostics;
556
+ };
@@ -0,0 +1,49 @@
1
+ const LISTENER_SYMBOL_REGEX = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
2
+ const HANDLER_SYMBOL_PREFIX = "handle";
3
+ const MISSING_SYMBOL = () => ({ isDefined: false, isValid: false, value: null });
4
+
5
+ export const isListenerEventConfig = (eventConfig) => {
6
+ return Boolean(eventConfig) && typeof eventConfig === "object" && !Array.isArray(eventConfig);
7
+ };
8
+
9
+ export const getListenerSymbol = ({ eventConfig, symbolName }) => {
10
+ if (!isListenerEventConfig(eventConfig)) {
11
+ return MISSING_SYMBOL();
12
+ }
13
+
14
+ if (!Object.prototype.hasOwnProperty.call(eventConfig, symbolName)) {
15
+ return MISSING_SYMBOL();
16
+ }
17
+
18
+ const value = eventConfig[symbolName];
19
+ if (typeof value !== "string" || !LISTENER_SYMBOL_REGEX.test(value)) {
20
+ return { isDefined: true, isValid: false, value: null };
21
+ }
22
+
23
+ return { isDefined: true, isValid: true, value };
24
+ };
25
+
26
+ export const getListenerSymbols = (eventConfig) => {
27
+ return {
28
+ handler: getListenerSymbol({ eventConfig, symbolName: "handler" }),
29
+ action: getListenerSymbol({ eventConfig, symbolName: "action" }),
30
+ };
31
+ };
32
+
33
+ export const isValidHandlerSymbol = (value) => {
34
+ return typeof value === "string"
35
+ && LISTENER_SYMBOL_REGEX.test(value)
36
+ && value.startsWith(HANDLER_SYMBOL_PREFIX);
37
+ };
38
+
39
+ export const resolveListenerLine = ({ listenerLine, optionLines, preferredKeys = [] }) => {
40
+ for (let index = 0; index < preferredKeys.length; index += 1) {
41
+ const key = preferredKeys[index];
42
+ const keyLine = optionLines?.[key];
43
+ if (Number.isInteger(keyLine)) {
44
+ return keyLine;
45
+ }
46
+ }
47
+
48
+ return Number.isInteger(listenerLine) ? listenerLine : undefined;
49
+ };