@kodus/kodus-graph 0.2.5 → 0.2.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kodus/kodus-graph",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Code graph builder for Kodus code review — parses source code into structural graphs with nodes, edges, and analysis",
5
5
  "type": "module",
6
6
  "bin": {
@@ -74,18 +74,17 @@ export function computeStructuralDiff(
74
74
  } else {
75
75
  const newN = newNodesMap.get(qn)!;
76
76
  const changes: string[] = [];
77
- // Detect real changes vs. pure displacement using content_hash.
78
- // content_hash is per-node (SHA256 of the function/class source text).
79
- // If both have content_hash: definitive comparison — same hash = identical content.
80
- // If either is missing (legacy data): fallback to size heuristic.
81
- if (n.line_start !== newN.line_start || n.line_end !== newN.line_end) {
82
- if (n.content_hash && newN.content_hash) {
83
- if (n.content_hash !== newN.content_hash) changes.push('line_range');
84
- } else {
85
- const oldSize = n.line_end - n.line_start;
86
- const newSize = newN.line_end - newN.line_start;
87
- if (oldSize !== newSize) changes.push('line_range');
88
- }
77
+ // Detect real content changes vs. pure displacement.
78
+ // content_hash = SHA256 of the node's source text (position-independent).
79
+ if (n.content_hash && newN.content_hash) {
80
+ // Definitive: hash comparison catches ALL content changes,
81
+ // even same-line-count edits (e.g. `return 1` `return 2`).
82
+ if (n.content_hash !== newN.content_hash) changes.push('body');
83
+ } else if (n.line_start !== newN.line_start || n.line_end !== newN.line_end) {
84
+ // Fallback (legacy data without content_hash): size heuristic.
85
+ const oldSize = n.line_end - n.line_start;
86
+ const newSize = newN.line_end - newN.line_start;
87
+ if (oldSize !== newSize) changes.push('line_range');
89
88
  }
90
89
  if ((n.params || '') !== (newN.params || '')) changes.push('params');
91
90
  if ((n.return_type || '') !== (newN.return_type || '')) changes.push('return_type');
@@ -118,16 +118,56 @@ export function formatPrompt(output: ContextV2Output): string {
118
118
  lines.push('');
119
119
  }
120
120
 
121
- // Structural diff summary
121
+ // Structural diff
122
122
  const diff = analysis.structural_diff;
123
- if (diff.summary.added > 0 || diff.summary.removed > 0 || diff.summary.modified > 0) {
123
+ const hasNodeChanges = diff.summary.added > 0 || diff.summary.removed > 0 || diff.summary.modified > 0;
124
+ const hasEdgeChanges = diff.edges.added.length > 0 || diff.edges.removed.length > 0;
125
+
126
+ if (hasNodeChanges || hasEdgeChanges) {
124
127
  lines.push('## Structural Changes');
125
128
  lines.push('');
126
- const parts: string[] = [];
127
- if (diff.summary.added > 0) parts.push(`${diff.summary.added} added`);
128
- if (diff.summary.removed > 0) parts.push(`${diff.summary.removed} removed`);
129
- if (diff.summary.modified > 0) parts.push(`${diff.summary.modified} modified`);
130
- lines.push(parts.join(', '));
129
+
130
+ if (hasNodeChanges) {
131
+ const parts: string[] = [];
132
+ if (diff.summary.added > 0) parts.push(`${diff.summary.added} added`);
133
+ if (diff.summary.removed > 0) parts.push(`${diff.summary.removed} removed`);
134
+ if (diff.summary.modified > 0) parts.push(`${diff.summary.modified} modified`);
135
+ lines.push(parts.join(', '));
136
+ }
137
+
138
+ if (diff.nodes.removed.length > 0) {
139
+ lines.push('');
140
+ lines.push('Removed:');
141
+ for (const n of diff.nodes.removed) {
142
+ const name = n.qualified_name.split('::').pop();
143
+ lines.push(` - [${n.kind}] ${name} [${n.file_path}:${n.line_start}]`);
144
+ }
145
+ }
146
+
147
+ if (diff.nodes.modified.length > 0) {
148
+ lines.push('');
149
+ lines.push('Modified:');
150
+ for (const m of diff.nodes.modified) {
151
+ const name = m.qualified_name.split('::').pop();
152
+ lines.push(` - ${name} (${m.changes.join(', ')})`);
153
+ }
154
+ }
155
+
156
+ if (hasEdgeChanges) {
157
+ lines.push('');
158
+ lines.push('Dependency changes:');
159
+ for (const e of diff.edges.added) {
160
+ const src = e.source_qualified.split('::').pop();
161
+ const tgt = e.target_qualified.split('::').pop();
162
+ lines.push(` + ${e.kind}: ${src} → ${tgt}`);
163
+ }
164
+ for (const e of diff.edges.removed) {
165
+ const src = e.source_qualified.split('::').pop();
166
+ const tgt = e.target_qualified.split('::').pop();
167
+ lines.push(` - ${e.kind}: ${src} → ${tgt}`);
168
+ }
169
+ }
170
+
131
171
  lines.push('');
132
172
  }
133
173
 
@@ -247,6 +247,7 @@ export interface RawCallSite {
247
247
  callName: string; // function or method name being called
248
248
  line: number; // line number of the call
249
249
  diField?: string; // if DI pattern (this.field.method), the field name
250
+ resolveInClass?: string; // class to resolve in: current class for self.X(), parent for super().X()
250
251
  }
251
252
 
252
253
  export interface RawCallEdge {
@@ -32,6 +32,6 @@ export function extractCallsFromFile(root: SgRoot, filePath: string, lang: Lang
32
32
  } else if (lang === 'ruby') {
33
33
  extractCallsFromRuby(root, filePath, calls);
34
34
  } else {
35
- extractCallsFromGeneric(root, filePath, calls);
35
+ extractCallsFromGeneric(root, filePath, lang as string, calls);
36
36
  }
37
37
  }
@@ -1,7 +1,7 @@
1
1
  import type { SgNode, SgRoot } from '@ast-grep/napi';
2
2
  import type { RawCallSite, RawGraph } from '../../graph/types';
3
+ import { type CallExtractionConfig, extractCalls } from '../../shared/extract-calls';
3
4
  import { computeContentHash } from '../../shared/file-hash';
4
- import { NOISE } from '../../shared/filters';
5
5
  import { log } from '../../shared/logger';
6
6
  import { LANG_KINDS } from '../languages';
7
7
 
@@ -69,22 +69,64 @@ export function extractGeneric(root: SgRoot, fp: string, lang: string, seen: Set
69
69
  }
70
70
  }
71
71
 
72
+ /** Shared class-finder for languages using class/struct/impl AST kinds. */
73
+ function findEnclosingClassGeneric(node: import('@ast-grep/napi').SgNode): import('@ast-grep/napi').SgNode | null {
74
+ return node.ancestors().find((a) => {
75
+ const k = String(a.kind());
76
+ return k.includes('class') || k.includes('struct') || k.includes('impl');
77
+ }) ?? null;
78
+ }
79
+
80
+ /** Per-language call extraction configs for self/super detection. */
81
+ const GENERIC_CONFIGS: Record<string, CallExtractionConfig> = {
82
+ java: {
83
+ selfPrefixes: ['this.'],
84
+ superPrefixes: ['super.'],
85
+ findEnclosingClass: findEnclosingClassGeneric,
86
+ getParentClass: (classNode) => {
87
+ const sc = classNode.children().find((c) => c.kind() === 'superclass');
88
+ return sc?.children().find((c) => c.kind() === 'type_identifier')?.text();
89
+ },
90
+ },
91
+ csharp: {
92
+ selfPrefixes: ['this.'],
93
+ superPrefixes: ['base.'],
94
+ findEnclosingClass: findEnclosingClassGeneric,
95
+ getParentClass: (classNode) => {
96
+ const bl = classNode.children().find((c) => c.kind() === 'base_list');
97
+ return bl?.children().find((c) => c.kind() === 'identifier' || c.kind() === 'type_identifier')?.text();
98
+ },
99
+ },
100
+ rust: {
101
+ selfPrefixes: ['self.'],
102
+ superPrefixes: [],
103
+ findEnclosingClass: (node) =>
104
+ node.ancestors().find((a) => a.kind() === 'impl_item') ?? null,
105
+ },
106
+ go: {
107
+ selfPrefixes: [],
108
+ superPrefixes: [],
109
+ findEnclosingClass: findEnclosingClassGeneric,
110
+ },
111
+ php: {
112
+ selfPrefixes: [],
113
+ superPrefixes: [],
114
+ findEnclosingClass: findEnclosingClassGeneric,
115
+ },
116
+ };
117
+
118
+ /** Fallback config for unknown languages — no self/super detection. */
119
+ const FALLBACK_CONFIG: CallExtractionConfig = {
120
+ selfPrefixes: [],
121
+ superPrefixes: [],
122
+ findEnclosingClass: findEnclosingClassGeneric,
123
+ };
124
+
72
125
  /**
73
126
  * Extract raw call sites from a generic language AST.
74
- * Direct calls only.
127
+ * Uses per-language config for self/super detection.
75
128
  */
76
- export function extractCallsFromGeneric(root: SgRoot, fp: string, calls: RawCallSite[]): void {
77
- const rootNode = root.root();
78
-
79
- for (const m of rootNode.findAll('$CALLEE($$$ARGS)')) {
80
- const callee = m.getMatch('CALLEE')?.text();
81
- if (!callee) continue;
82
- const callName = callee.includes('.') ? callee.split('.').pop()! : callee;
83
- if (NOISE.has(callName)) continue;
84
- calls.push({
85
- source: fp,
86
- callName,
87
- line: m.range().start.line,
88
- });
89
- }
129
+ export function extractCallsFromGeneric(root: SgRoot, fp: string, lang: string, calls: RawCallSite[]): void {
130
+ const config = GENERIC_CONFIGS[lang] ?? FALLBACK_CONFIG;
131
+ extractCalls(root.root(), fp, config, calls);
90
132
  }
@@ -1,7 +1,7 @@
1
1
  import type { SgNode, SgRoot } from '@ast-grep/napi';
2
2
  import type { RawCallSite, RawGraph } from '../../graph/types';
3
+ import { type CallExtractionConfig, extractCalls } from '../../shared/extract-calls';
3
4
  import { computeContentHash } from '../../shared/file-hash';
4
- import { NOISE } from '../../shared/filters';
5
5
  import { LANG_KINDS } from '../languages';
6
6
 
7
7
  export function extractPython(root: SgRoot, fp: string, seen: Set<string>, graph: RawGraph): void {
@@ -110,22 +110,24 @@ export function extractPython(root: SgRoot, fp: string, seen: Set<string>, graph
110
110
  }
111
111
  }
112
112
 
113
+ /** Python-specific call extraction config for shared extractCalls(). */
114
+ const PYTHON_CALL_CONFIG: CallExtractionConfig = {
115
+ selfPrefixes: ['self.'],
116
+ superPrefixes: ['super().'],
117
+ findEnclosingClass: (node) =>
118
+ node.ancestors().find((a: SgNode) => a.kind() === 'class_definition') ?? null,
119
+ getParentClass: (classNode) => {
120
+ const argList =
121
+ classNode.field('superclasses') ||
122
+ classNode.children().find((c: SgNode) => c.kind() === 'argument_list');
123
+ return argList?.children().find((c: SgNode) => c.kind() === 'identifier')?.text();
124
+ },
125
+ };
126
+
113
127
  /**
114
128
  * Extract raw call sites from a Python AST.
115
- * Direct calls only Python has no DI pattern.
129
+ * Detects self.X() and super().X() to preserve class resolution context.
116
130
  */
117
131
  export function extractCallsFromPython(root: SgRoot, fp: string, calls: RawCallSite[]): void {
118
- const rootNode = root.root();
119
-
120
- for (const m of rootNode.findAll('$CALLEE($$$ARGS)')) {
121
- const callee = m.getMatch('CALLEE')?.text();
122
- if (!callee) continue;
123
- const callName = callee.includes('.') ? callee.split('.').pop()! : callee;
124
- if (NOISE.has(callName)) continue;
125
- calls.push({
126
- source: fp,
127
- callName,
128
- line: m.range().start.line,
129
- });
130
- }
132
+ extractCalls(root.root(), fp, PYTHON_CALL_CONFIG, calls);
131
133
  }
@@ -1,7 +1,7 @@
1
1
  import type { SgNode, SgRoot } from '@ast-grep/napi';
2
2
  import type { RawCallSite, RawGraph } from '../../graph/types';
3
+ import { type CallExtractionConfig, extractCalls } from '../../shared/extract-calls';
3
4
  import { computeContentHash } from '../../shared/file-hash';
4
- import { NOISE } from '../../shared/filters';
5
5
  import { log } from '../../shared/logger';
6
6
  import { LANG_KINDS } from '../languages';
7
7
 
@@ -126,22 +126,22 @@ export function extractRuby(root: SgRoot, fp: string, seen: Set<string>, graph:
126
126
  }
127
127
  }
128
128
 
129
+ /** Ruby-specific call extraction config for shared extractCalls(). */
130
+ function createRubyCallConfig(): CallExtractionConfig {
131
+ const kinds = LANG_KINDS.ruby;
132
+ return {
133
+ selfPrefixes: ['self.'],
134
+ superPrefixes: ['super'],
135
+ findEnclosingClass: (node) =>
136
+ node.ancestors().find((a: SgNode) => a.kind() === kinds.class || a.kind() === kinds.module) ?? null,
137
+ getParentClass: (classNode) => classNode.field('superclass')?.text(),
138
+ };
139
+ }
140
+
129
141
  /**
130
142
  * Extract raw call sites from a Ruby AST.
131
- * Direct calls only Ruby has no DI pattern.
143
+ * Detects self.X() and super() to preserve class resolution context.
132
144
  */
133
145
  export function extractCallsFromRuby(root: SgRoot, fp: string, calls: RawCallSite[]): void {
134
- const rootNode = root.root();
135
-
136
- for (const m of rootNode.findAll('$CALLEE($$$ARGS)')) {
137
- const callee = m.getMatch('CALLEE')?.text();
138
- if (!callee) continue;
139
- const callName = callee.includes('.') ? callee.split('.').pop()! : callee;
140
- if (NOISE.has(callName)) continue;
141
- calls.push({
142
- source: fp,
143
- callName,
144
- line: m.range().start.line,
145
- });
146
- }
146
+ extractCalls(root.root(), fp, createRubyCallConfig(), calls);
147
147
  }
@@ -1,6 +1,7 @@
1
1
  import type { SgNode, SgRoot } from '@ast-grep/napi';
2
2
  import { Lang } from '@ast-grep/napi';
3
3
  import type { RawCallSite, RawGraph } from '../../graph/types';
4
+ import { type CallExtractionConfig, extractCalls } from '../../shared/extract-calls';
4
5
  import { computeContentHash } from '../../shared/file-hash';
5
6
  import { NOISE } from '../../shared/filters';
6
7
  import { LANG_KINDS } from '../languages';
@@ -302,6 +303,27 @@ export function extractTypeScript(
302
303
  }
303
304
  }
304
305
 
306
+ /** TypeScript-specific call extraction config for shared extractCalls(). */
307
+ const TS_CALL_CONFIG: CallExtractionConfig = {
308
+ selfPrefixes: ['this.'],
309
+ superPrefixes: ['super.'],
310
+ findEnclosingClass: (node) => {
311
+ const kinds = LANG_KINDS.typescript;
312
+ return node.ancestors().find(
313
+ (a: SgNode) => a.kind() === kinds.class || a.kind() === kinds.abstractClass,
314
+ ) ?? null;
315
+ },
316
+ getParentClass: (classNode) => {
317
+ const heritage = classNode.children().find((c: SgNode) => c.kind() === 'class_heritage');
318
+ const ext = heritage?.children().find((c: SgNode) => c.kind() === 'extends_clause');
319
+ return ext?.children().find(
320
+ (c: SgNode) => c.kind() === 'identifier' || c.kind() === 'type_identifier' || c.kind() === 'member_expression',
321
+ )?.text();
322
+ },
323
+ // Skip this.field.method — already handled by the DI pattern above
324
+ skipCallee: (callee) => callee.startsWith('this.') && callee.substring(5).includes('.'),
325
+ };
326
+
305
327
  /**
306
328
  * Extract raw call sites from a TypeScript/JavaScript AST.
307
329
  * Finds DI calls (this.field.method) and direct calls ($CALLEE($$$ARGS)).
@@ -323,16 +345,6 @@ export function extractCallsFromTypeScript(root: SgRoot, fp: string, calls: RawC
323
345
  });
324
346
  }
325
347
 
326
- // Direct calls: $CALLEE($$$ARGS)
327
- for (const m of rootNode.findAll('$CALLEE($$$ARGS)')) {
328
- const callee = m.getMatch('CALLEE')?.text();
329
- if (!callee || callee.startsWith('this.')) continue;
330
- const callName = callee.includes('.') ? callee.split('.').pop()! : callee;
331
- if (NOISE.has(callName)) continue;
332
- calls.push({
333
- source: fp,
334
- callName,
335
- line: m.range().start.line,
336
- });
337
- }
348
+ // Direct calls + self/super detection via shared function
349
+ extractCalls(rootNode, fp, TS_CALL_CONFIG, calls);
338
350
  }
@@ -77,6 +77,22 @@ export function resolveAllCalls(
77
77
  }
78
78
  }
79
79
 
80
+ // Class-aware resolution for self.X() and super().X()
81
+ if (call.resolveInClass) {
82
+ const classResolved = resolveInClass(call.callName, fp, call.resolveInClass, symbolTable);
83
+ if (classResolved) {
84
+ callEdges.push({
85
+ source: fp,
86
+ target: classResolved.target,
87
+ callName: call.callName,
88
+ line: call.line,
89
+ confidence: classResolved.confidence,
90
+ });
91
+ stats[classResolved.strategy]++;
92
+ continue;
93
+ }
94
+ }
95
+
80
96
  // Name-based cascade fallback
81
97
  const resolved = resolveByName(call.callName, fp, symbolTable, importMap);
82
98
  if (resolved) {
@@ -94,6 +110,26 @@ export function resolveAllCalls(
94
110
  return { callEdges, stats };
95
111
  }
96
112
 
113
+ // ── Class-aware resolution (self./super.) ──
114
+
115
+ function resolveInClass(
116
+ callName: string,
117
+ currentFile: string,
118
+ className: string,
119
+ symbolTable: SymbolTable,
120
+ ): ResolveResult | null {
121
+ // Try same-file class method first (self.method() or super().method())
122
+ const inFile = symbolTable.lookupInFile(currentFile, callName, className);
123
+ if (inFile) return { target: inFile, confidence: 0.9, strategy: 'same' };
124
+
125
+ // Class might be in another file (imported parent class for super())
126
+ const candidates = symbolTable.lookupGlobal(callName);
127
+ const match = candidates.find((q) => q.includes(`::${className}.${callName}`));
128
+ if (match) return { target: match, confidence: 0.85, strategy: 'import' };
129
+
130
+ return null;
131
+ }
132
+
97
133
  // ── DI resolution ──
98
134
 
99
135
  function resolveDICall(
@@ -9,6 +9,7 @@
9
9
  export interface SymbolTable {
10
10
  add(file: string, name: string, qualified: string): void;
11
11
  lookupExact(file: string, name: string): string | null;
12
+ lookupInFile(file: string, name: string, className: string): string | null;
12
13
  isUnique(name: string): boolean;
13
14
  lookupGlobal(name: string): string[];
14
15
  readonly size: number;
@@ -16,20 +17,31 @@ export interface SymbolTable {
16
17
  }
17
18
 
18
19
  export function createSymbolTable(): SymbolTable {
19
- const byFile = new Map<string, Map<string, string>>();
20
+ const byFile = new Map<string, Map<string, string[]>>();
20
21
  const byName = new Map<string, string[]>();
21
22
 
22
23
  return {
23
24
  add(file, name, qualified) {
24
25
  if (!byFile.has(file)) byFile.set(file, new Map());
25
- byFile.get(file)!.set(name, qualified);
26
+ const fileMap = byFile.get(file)!;
27
+ if (!fileMap.has(name)) fileMap.set(name, []);
28
+ fileMap.get(name)!.push(qualified);
26
29
 
27
30
  if (!byName.has(name)) byName.set(name, []);
28
31
  byName.get(name)!.push(qualified);
29
32
  },
30
33
 
31
34
  lookupExact(file, name) {
32
- return byFile.get(file)?.get(name) ?? null;
35
+ const candidates = byFile.get(file)?.get(name);
36
+ if (!candidates || candidates.length === 0) return null;
37
+ // Only return if unambiguous within this file
38
+ return candidates.length === 1 ? candidates[0] : null;
39
+ },
40
+
41
+ lookupInFile(file, name, className) {
42
+ const candidates = byFile.get(file)?.get(name);
43
+ if (!candidates || candidates.length === 0) return null;
44
+ return candidates.find((q) => q.includes(`::${className}.${name}`)) ?? null;
33
45
  },
34
46
 
35
47
  isUnique(name) {
@@ -42,7 +54,9 @@ export function createSymbolTable(): SymbolTable {
42
54
 
43
55
  get size() {
44
56
  let count = 0;
45
- for (const m of byFile.values()) count += m.size;
57
+ for (const m of byFile.values()) {
58
+ for (const arr of m.values()) count += arr.length;
59
+ }
46
60
  return count;
47
61
  },
48
62
 
@@ -0,0 +1,75 @@
1
+ import type { SgNode } from '@ast-grep/napi';
2
+ import type { RawCallSite } from '../graph/types';
3
+ import { NOISE } from './filters';
4
+
5
+ /**
6
+ * Language-specific configuration for call extraction.
7
+ * Each language provides its self/super patterns and how to find class context in the AST.
8
+ */
9
+ export interface CallExtractionConfig {
10
+ /** Prefixes indicating a self-reference (e.g., 'self.', 'this.') */
11
+ selfPrefixes: string[];
12
+ /** Prefixes indicating a super-reference (e.g., 'super().', 'super.', 'base.') */
13
+ superPrefixes: string[];
14
+ /** Find the enclosing class/module/impl node from a call site */
15
+ findEnclosingClass: (node: SgNode) => SgNode | null;
16
+ /** Extract the parent class name from a class node (for super resolution) */
17
+ getParentClass?: (classNode: SgNode) => string | undefined;
18
+ /** Skip this callee entirely (e.g., TS skips this.field.method — handled by DI) */
19
+ skipCallee?: (callee: string) => boolean;
20
+ }
21
+
22
+ /**
23
+ * Shared call extraction for all languages.
24
+ *
25
+ * Parses `$CALLEE($$$ARGS)` pattern, detects self/super references
26
+ * based on language config, and populates resolveInClass for
27
+ * class-aware resolution downstream.
28
+ */
29
+ export function extractCalls(
30
+ rootNode: SgNode,
31
+ fp: string,
32
+ config: CallExtractionConfig,
33
+ calls: RawCallSite[],
34
+ ): void {
35
+ for (const m of rootNode.findAll('$CALLEE($$$ARGS)')) {
36
+ const callee = m.getMatch('CALLEE')?.text();
37
+ if (!callee) continue;
38
+ if (config.skipCallee?.(callee)) continue;
39
+
40
+ const callName = callee.includes('.') ? callee.split('.').pop()! : callee;
41
+ if (NOISE.has(callName)) continue;
42
+
43
+ let resolveInClass: string | undefined;
44
+
45
+ // Check self-reference: callee must be exactly `prefix + methodName` (no further chaining)
46
+ for (const prefix of config.selfPrefixes) {
47
+ if (!callee.startsWith(prefix)) continue;
48
+ const rest = callee.substring(prefix.length);
49
+ if (rest.includes('.')) break; // chained access (e.g., this.field.method) — not a self call
50
+ const classNode = config.findEnclosingClass(m);
51
+ resolveInClass = classNode?.field('name')?.text();
52
+ break;
53
+ }
54
+
55
+ // Check super-reference if no self match
56
+ if (!resolveInClass) {
57
+ for (const prefix of config.superPrefixes) {
58
+ const matches = callee === prefix || (callee.startsWith(prefix) && !callee.substring(prefix.length).includes('.'));
59
+ if (!matches) continue;
60
+ const classNode = config.findEnclosingClass(m);
61
+ if (classNode && config.getParentClass) {
62
+ resolveInClass = config.getParentClass(classNode);
63
+ }
64
+ break;
65
+ }
66
+ }
67
+
68
+ calls.push({
69
+ source: fp,
70
+ callName,
71
+ line: m.range().start.line,
72
+ ...(resolveInClass ? { resolveInClass } : {}),
73
+ });
74
+ }
75
+ }