@gnapi/cotester 1.2.10 → 1.2.12
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/analyzer.d.ts +17 -0
- package/dist/analyzer.js +184 -15
- package/dist/analyzer.js.map +1 -1
- package/dist/generator.js +438 -85
- package/dist/generator.js.map +1 -1
- package/dist/interfaceShapeResolver.js +9 -0
- package/dist/interfaceShapeResolver.js.map +1 -1
- package/dist/mockDataEngine.js +5 -3
- package/dist/mockDataEngine.js.map +1 -1
- package/dist/mockGenerator.js +39 -11
- package/dist/mockGenerator.js.map +1 -1
- package/dist/ormMockGenerator.js +22 -20
- package/dist/ormMockGenerator.js.map +1 -1
- package/dist/scenarioEngine.d.ts +22 -0
- package/dist/scenarioEngine.js +55 -40
- package/dist/scenarioEngine.js.map +1 -1
- package/dist/sharedMockRegistry.js +1 -1
- package/dist/sharedMockRegistry.js.map +1 -1
- package/package.json +1 -1
package/dist/generator.js
CHANGED
|
@@ -37,6 +37,7 @@ exports.VERSION_MARKER = exports.CURRENT_FILE_VERSION = exports.TESTGEN_MARKER =
|
|
|
37
37
|
exports.generateTestFile = generateTestFile;
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
39
|
const path = __importStar(require("path"));
|
|
40
|
+
const ts_morph_1 = require("ts-morph");
|
|
40
41
|
const formatter_1 = require("./formatter");
|
|
41
42
|
const utils_1 = require("./utils");
|
|
42
43
|
const ormMockGenerator_1 = require("./ormMockGenerator");
|
|
@@ -391,6 +392,24 @@ function buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, proje
|
|
|
391
392
|
lines.push(adapter.mockModule(depImportPath, dep.importedNames));
|
|
392
393
|
lines.push("");
|
|
393
394
|
}
|
|
395
|
+
// ── Auto-mock @nestjs/swagger to prevent decorator crashes ─────
|
|
396
|
+
// NestJS Swagger decorators call into metadata reflection at import time,
|
|
397
|
+
// which fails in a pure-Jest context. Mock the entire module with no-op
|
|
398
|
+
// decorator factories.
|
|
399
|
+
if (srcFilePath.includes(".controller.") ||
|
|
400
|
+
srcFilePath.includes(".dto.") ||
|
|
401
|
+
srcFilePath.includes("swagger") ||
|
|
402
|
+
allDeps.some(d => d.modulePath.includes("swagger") || d.modulePath.includes(".dto") || d.modulePath.includes(".controller"))) {
|
|
403
|
+
lines.push(`jest.mock('@nestjs/swagger', () => {`);
|
|
404
|
+
lines.push(` const noop = () => (_target: any, _key?: any, _desc?: any) => _desc ?? _target;`);
|
|
405
|
+
lines.push(` const noopClass = () => (target: any) => target;`);
|
|
406
|
+
lines.push(` return new Proxy({}, { get: (_t, prop) => {`);
|
|
407
|
+
lines.push(` if (prop === '__esModule') return true;`);
|
|
408
|
+
lines.push(` return (...args: any[]) => args[0]?.constructor === Function || typeof args[0] === 'function' ? args[0] : noop();`);
|
|
409
|
+
lines.push(` }});`);
|
|
410
|
+
lines.push(`});`);
|
|
411
|
+
lines.push(``);
|
|
412
|
+
}
|
|
394
413
|
// ── Import the source module ────────────────────────────────────
|
|
395
414
|
const importPath = computeRelativeImport(srcFilePath, testFilePath);
|
|
396
415
|
const importNames = collectImportNames(scenarios);
|
|
@@ -450,11 +469,45 @@ function buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, proje
|
|
|
450
469
|
// ─── Class describe block with constructor DI ────────────────────────────────
|
|
451
470
|
function buildClassDescribe(className, methods, mockResult, tableThreshold = 0, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
|
|
452
471
|
const lines = [];
|
|
472
|
+
const isAbstract = methods[0]?.isAbstractClass ?? false;
|
|
473
|
+
const abstractMethods = methods[0]?.abstractMethods ?? [];
|
|
474
|
+
const testClassName = isAbstract ? `Test${className}` : className;
|
|
475
|
+
// For abstract classes, generate a concrete test subclass
|
|
476
|
+
if (isAbstract) {
|
|
477
|
+
lines.push(`// Concrete subclass to test protected methods of abstract ${className}`);
|
|
478
|
+
lines.push(`class ${testClassName} extends ${className} {`);
|
|
479
|
+
// Stub all abstract methods with minimal implementations
|
|
480
|
+
for (const am of abstractMethods) {
|
|
481
|
+
const paramList = am.params.map(p => `_${p.name}: ${p.type}`).join(", ");
|
|
482
|
+
const returnDefault = buildAbstractMethodDefault(am.returnType, am.isAsync);
|
|
483
|
+
if (am.isAsync) {
|
|
484
|
+
lines.push(` async ${am.name}(${paramList}): ${am.returnType} { return ${returnDefault}; }`);
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
lines.push(` ${am.name}(${paramList}): ${am.returnType} { return ${returnDefault}; }`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
// Expose protected methods via public wrappers
|
|
491
|
+
const protectedMethods = methods.filter(m => !m.isStatic && !abstractMethods.some(am => am.name === m.functionName));
|
|
492
|
+
for (const pm of protectedMethods) {
|
|
493
|
+
const paramList = pm.parameters.map(p => `${p.name}: ${p.type}`).join(", ");
|
|
494
|
+
const argList = pm.parameters.map(p => p.name).join(", ");
|
|
495
|
+
const retType = pm.returnType;
|
|
496
|
+
if (pm.isAsync) {
|
|
497
|
+
lines.push(` async call${capitalize(pm.functionName)}(${paramList}): ${retType} { return this.${pm.functionName}(${argList}); }`);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
lines.push(` call${capitalize(pm.functionName)}(${paramList}): ${retType} { return this.${pm.functionName}(${argList}); }`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
lines.push("}");
|
|
504
|
+
lines.push("");
|
|
505
|
+
}
|
|
453
506
|
lines.push(`describe(${JSON.stringify(className)}, () => {`);
|
|
454
507
|
// Determine if the class has any instance methods (non-static) before declaring `instance`
|
|
455
508
|
const hasInstanceMethods = methods.some((m) => !m.isStatic);
|
|
456
509
|
if (hasInstanceMethods) {
|
|
457
|
-
lines.push(` let instance: ${
|
|
510
|
+
lines.push(` let instance: ${testClassName};`);
|
|
458
511
|
}
|
|
459
512
|
// Generate mock objects for constructor dependencies
|
|
460
513
|
// E-3: declare as `let` at describe scope, assign fresh instances in beforeEach
|
|
@@ -479,6 +532,14 @@ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0,
|
|
|
479
532
|
}
|
|
480
533
|
}
|
|
481
534
|
}
|
|
535
|
+
// For constructor-injected dependencies whose methods weren't found via
|
|
536
|
+
// the body-text dependency scan (because the class name doesn't appear in
|
|
537
|
+
// method bodies — only `this.<paramName>.method()` does), resolve the
|
|
538
|
+
// actual public methods from the injected class using ts-morph.
|
|
539
|
+
const sourceFilePath = methods[0]?.sourceFilePath;
|
|
540
|
+
if (sourceFilePath && ctorParams.length > 0) {
|
|
541
|
+
resolveCtorDepMethods(ctorParams, depMethodsByType, sourceFilePath);
|
|
542
|
+
}
|
|
482
543
|
for (const param of ctorParams) {
|
|
483
544
|
const mockName = `mock${capitalize(param.name)}`;
|
|
484
545
|
letDeclarations.push(` let ${mockName}: any;`);
|
|
@@ -494,8 +555,15 @@ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0,
|
|
|
494
555
|
}
|
|
495
556
|
else {
|
|
496
557
|
const fn = adapter.mockFn();
|
|
497
|
-
// Check if we have actual dependency metadata for this param's type
|
|
498
|
-
|
|
558
|
+
// Check if we have actual dependency metadata for this param's type.
|
|
559
|
+
// param.type may be a fully-qualified import path like
|
|
560
|
+
// import("../metadata-sync/metadata.repository").MetadataRepository
|
|
561
|
+
// so we also try matching by the short class name extracted from the end.
|
|
562
|
+
let knownMethods = depMethodsByType.get(param.type);
|
|
563
|
+
if (!knownMethods || knownMethods.size === 0) {
|
|
564
|
+
const shortType = param.type.replace(/^import\([^)]*\)\./, "").replace(/<.*>$/, "").trim();
|
|
565
|
+
knownMethods = depMethodsByType.get(shortType);
|
|
566
|
+
}
|
|
499
567
|
if (knownMethods && knownMethods.size > 0) {
|
|
500
568
|
// Use actual method names from AST analysis
|
|
501
569
|
const mockProps = Array.from(knownMethods).map(m => {
|
|
@@ -510,7 +578,11 @@ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0,
|
|
|
510
578
|
const typeLower = param.type.toLowerCase();
|
|
511
579
|
const asyncFn = `${fn}.mockResolvedValue(null)`;
|
|
512
580
|
const asyncIdFn = `${fn}.mockResolvedValue({ id: 1 })`;
|
|
513
|
-
if (typeLower.includes("
|
|
581
|
+
if (typeLower.includes("configservice") || typeLower.includes("config_service")) {
|
|
582
|
+
// NestJS ConfigService mock — must come before generic "service" match
|
|
583
|
+
beforeEachAssignments.push(` ${mockName} = { get: ${fn}.mockReturnValue('test'), getOrThrow: ${fn}.mockReturnValue('test') } as any;`);
|
|
584
|
+
}
|
|
585
|
+
else if (typeLower.includes("repository") || typeLower.includes("repo")) {
|
|
514
586
|
beforeEachAssignments.push(` ${mockName} = { find: ${fn}.mockResolvedValue([]), findOne: ${asyncFn}, findById: ${asyncFn}, ` +
|
|
515
587
|
`save: ${asyncIdFn}, create: ${asyncIdFn}, update: ${asyncIdFn}, delete: ${asyncIdFn}, ` +
|
|
516
588
|
`createQueryBuilder: ${fn}.mockReturnValue({ where: ${fn}.mockReturnThis(), getOne: ${asyncFn}, getMany: ${fn}.mockResolvedValue([]) }) } as any;`);
|
|
@@ -529,13 +601,8 @@ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0,
|
|
|
529
601
|
beforeEachAssignments.push(` ${mockName} = { send: ${asyncFn}, emit: ${fn}, publish: ${asyncFn}, add: ${asyncFn} } as any;`);
|
|
530
602
|
}
|
|
531
603
|
else if (typeLower.includes("database") || typeLower.includes("nodepgdatabase") || typeLower.includes("drizzle") || typeLower.includes("pgtransaction")) {
|
|
532
|
-
// Drizzle ORM database/transaction mock
|
|
533
|
-
|
|
534
|
-
beforeEachAssignments.push(` ${mockName} = { select: ${fn}.mockReturnValue({ from: ${chain}, where: ${chain}, leftJoin: ${chain}, orderBy: ${chain}, limit: ${chain}, then: ${fn}.mockResolvedValue([]) }),` +
|
|
535
|
-
` insert: ${fn}.mockReturnValue({ values: ${fn}.mockReturnValue({ onConflictDoNothing: ${fn}.mockReturnValue({ returning: ${fn}.mockResolvedValue([{ id: 1 }]) }), returning: ${fn}.mockResolvedValue([{ id: 1 }]) }) }),` +
|
|
536
|
-
` update: ${fn}.mockReturnValue({ set: ${fn}.mockReturnValue({ where: ${fn}.mockReturnValue({ returning: ${fn}.mockResolvedValue([{ id: 1 }]) }) }) }),` +
|
|
537
|
-
` delete: ${fn}.mockReturnValue({ where: ${fn}.mockResolvedValue([]) }),` +
|
|
538
|
-
` execute: ${asyncFn}, query: ${asyncFn}, transaction: ${fn}.mockImplementation((cb: any) => cb(${mockName})) } as any;`);
|
|
604
|
+
// Drizzle ORM database/transaction mock — Proxy-based self-referencing chain
|
|
605
|
+
beforeEachAssignments.push(` ${mockName} = new Proxy({} as any, { get: (_t, prop) => { if (prop === 'then' || prop === 'catch') return undefined; if (typeof prop === 'string' && !_t[prop]) { if (['select','insert','update','delete','execute','query'].includes(prop)) _t[prop] = ${fn}.mockImplementation(() => new Proxy({} as any, { get: (_c, m) => { if (m === 'then') return (cb: any) => Promise.resolve([]).then(cb); if (typeof m === 'string' && !_c[m]) _c[m] = ${fn}.mockReturnValue(_c); return _c[m]; } })); else if (prop === 'transaction') _t[prop] = ${fn}.mockImplementation((cb: any) => cb(${mockName})); else _t[prop] = ${fn}; } return _t[prop]; } });`);
|
|
539
606
|
}
|
|
540
607
|
else if (typeLower.includes("clickhouse") || typeLower.includes("client")) {
|
|
541
608
|
// ClickHouse / generic DB client mock
|
|
@@ -557,10 +624,24 @@ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0,
|
|
|
557
624
|
lines.push(decl);
|
|
558
625
|
}
|
|
559
626
|
}
|
|
627
|
+
// Collect env var properties from all methods (they share the same class)
|
|
628
|
+
const envVarProps = methods[0]?.envVarProperties ?? [];
|
|
629
|
+
if (envVarProps.length > 0) {
|
|
630
|
+
lines.push("");
|
|
631
|
+
lines.push(" let originalEnv: NodeJS.ProcessEnv;");
|
|
632
|
+
}
|
|
560
633
|
lines.push("");
|
|
561
634
|
lines.push(" beforeEach(() => {");
|
|
562
635
|
// FA-2: clearAllMocks in beforeEach (keeps implementations, clears call history)
|
|
563
636
|
lines.push(` ${adapter.clearAllMocks()}`);
|
|
637
|
+
// Set up env vars before creating the instance (class properties read env at construction time)
|
|
638
|
+
if (envVarProps.length > 0) {
|
|
639
|
+
lines.push(" originalEnv = { ...process.env };");
|
|
640
|
+
for (const evp of envVarProps) {
|
|
641
|
+
const val = evp.defaultValue ?? "test-value";
|
|
642
|
+
lines.push(` process.env['${evp.envVarName}'] = '${val}';`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
564
645
|
// E-3: recreate fresh mock objects so mutations don't leak between tests
|
|
565
646
|
for (const assignment of beforeEachAssignments) {
|
|
566
647
|
lines.push(assignment);
|
|
@@ -570,10 +651,10 @@ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0,
|
|
|
570
651
|
const args = ctorParams
|
|
571
652
|
.map((p) => `mock${capitalize(p.name)}`)
|
|
572
653
|
.join(", ");
|
|
573
|
-
lines.push(` instance = new ${
|
|
654
|
+
lines.push(` instance = new ${testClassName}(${args});`);
|
|
574
655
|
}
|
|
575
656
|
else {
|
|
576
|
-
lines.push(` instance = new ${
|
|
657
|
+
lines.push(` instance = new ${testClassName}();`);
|
|
577
658
|
}
|
|
578
659
|
}
|
|
579
660
|
lines.push(" });");
|
|
@@ -581,8 +662,9 @@ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0,
|
|
|
581
662
|
lines.push(" afterEach(() => {");
|
|
582
663
|
// FA-2: resetAllMocks in afterEach (resets implementations too)
|
|
583
664
|
lines.push(` ${adapter.resetAllMocks()}`);
|
|
584
|
-
|
|
585
|
-
|
|
665
|
+
if (envVarProps.length > 0) {
|
|
666
|
+
lines.push(" process.env = originalEnv;");
|
|
667
|
+
}
|
|
586
668
|
lines.push(" });");
|
|
587
669
|
// T-2: always emit afterAll (not only when ctorParams.length > 0)
|
|
588
670
|
lines.push("");
|
|
@@ -650,7 +732,8 @@ function buildFunctionDescribe(fn, mockResult, tableThreshold = 0, adapter = (0,
|
|
|
650
732
|
lines.push(" });");
|
|
651
733
|
lines.push("");
|
|
652
734
|
}
|
|
653
|
-
const
|
|
735
|
+
const mockKey = fn.className ? `${fn.className}_${fn.functionName}` : fn.functionName;
|
|
736
|
+
const constName = mockResult?.constantNames.get(mockKey);
|
|
654
737
|
const { tableCases, individualCases } = partitionCases(fn.cases, fn.parameters.length, tableThreshold);
|
|
655
738
|
if (tableCases.length > 0) {
|
|
656
739
|
lines.push(indentBlock(buildTableBlock(fn, tableCases, snapshotForComplexTypes), 1));
|
|
@@ -668,7 +751,8 @@ function buildMethodDescribe(fn, mockResult, tableThreshold = 0, adapter = (0, f
|
|
|
668
751
|
const lines = [];
|
|
669
752
|
lines.push(`// @testgen-sig:${buildSignatureFingerprint(fn)}`);
|
|
670
753
|
lines.push(`describe(${JSON.stringify(fn.functionName)}, () => {`);
|
|
671
|
-
const
|
|
754
|
+
const mockKey = fn.className ? `${fn.className}_${fn.functionName}` : fn.functionName;
|
|
755
|
+
const constName = mockResult?.constantNames.get(mockKey);
|
|
672
756
|
const { tableCases, individualCases } = partitionCases(fn.cases, fn.parameters.length, tableThreshold);
|
|
673
757
|
if (tableCases.length > 0) {
|
|
674
758
|
lines.push(indentBlock(buildTableBlock(fn, tableCases, snapshotForComplexTypes), 1));
|
|
@@ -728,7 +812,7 @@ function buildTableBlock(scenario, tableCases, snapshotForComplexTypes = false)
|
|
|
728
812
|
const isStatic = !!scenario.isStatic;
|
|
729
813
|
const caller = isStatic
|
|
730
814
|
? `${scenario.className}.${fnName}`
|
|
731
|
-
: isMethod ? `instance.${fnName}` : fnName;
|
|
815
|
+
: isMethod ? `instance.${scenario.isAbstractClass ? `call${capitalize(fnName)}` : fnName}` : fnName;
|
|
732
816
|
const safeParamNames = paramNames.map(safeName);
|
|
733
817
|
if (isAsync) {
|
|
734
818
|
lines.push(` const result = await ${caller}(${safeParamNames.join(", ")});`);
|
|
@@ -752,7 +836,7 @@ function buildTestBlock(scenario, testCase, mockConstName, adapter = (0, framewo
|
|
|
752
836
|
case "throws":
|
|
753
837
|
return buildThrowsBlock(scenario, testCase, mockConstName);
|
|
754
838
|
case "rejects":
|
|
755
|
-
return buildRejectsBlock(scenario, testCase, mockConstName);
|
|
839
|
+
return buildRejectsBlock(scenario, testCase, mockConstName, adapter);
|
|
756
840
|
default:
|
|
757
841
|
return buildStandardBlock(scenario, testCase, mockConstName, adapter, snapshotForComplexTypes);
|
|
758
842
|
}
|
|
@@ -820,7 +904,7 @@ function buildStandardBlock(scenario, testCase, mockConstName, adapter = (0, fra
|
|
|
820
904
|
const argsStr = testCase.paramNames.map(n => `${safeName(n)} as any`).join(", ");
|
|
821
905
|
const caller = isStatic
|
|
822
906
|
? `${scenario.className}.${fnName}`
|
|
823
|
-
: isMethod ? `instance.${fnName}` : fnName;
|
|
907
|
+
: isMethod ? `instance.${scenario.isAbstractClass ? `call${capitalize(fnName)}` : fnName}` : fnName;
|
|
824
908
|
const callExpr = `${caller}(${argsStr})`;
|
|
825
909
|
if (isAsync) {
|
|
826
910
|
lines.push(` const result = await ${callExpr};`);
|
|
@@ -832,6 +916,7 @@ function buildStandardBlock(scenario, testCase, mockConstName, adapter = (0, fra
|
|
|
832
916
|
lines.push("");
|
|
833
917
|
// ── Assert ──────────────────────────────────────────────────────
|
|
834
918
|
lines.push(" // Assert");
|
|
919
|
+
const assertStartIdx = lines.length;
|
|
835
920
|
if (isDependencyTest) {
|
|
836
921
|
// Verify only functions that the analyzer confirmed are called (have meta)
|
|
837
922
|
// This avoids asserting on imports the function never uses
|
|
@@ -882,23 +967,57 @@ function buildStandardBlock(scenario, testCase, mockConstName, adapter = (0, fra
|
|
|
882
967
|
for (const e of expectLines) {
|
|
883
968
|
lines.push(` ${e}`);
|
|
884
969
|
}
|
|
885
|
-
//
|
|
970
|
+
// Add dependency-call verification for happy-path (valid input) tests
|
|
886
971
|
if (!isDependencyTest && scenario.dependencies.length > 0) {
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
.
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
972
|
+
const isHappyPath = testCase.label.includes("valid") || testCase.label.includes("typical");
|
|
973
|
+
const isClassMethod = !!scenario.className;
|
|
974
|
+
const ctorParams = scenario.constructorParams ?? [];
|
|
975
|
+
if (isHappyPath) {
|
|
976
|
+
lines.push("");
|
|
977
|
+
for (const dep of scenario.dependencies) {
|
|
978
|
+
let mockPrefix = "";
|
|
979
|
+
let skipDep = false;
|
|
980
|
+
if (isClassMethod && ctorParams.length > 0) {
|
|
981
|
+
const depTypeName = dep.importedNames.find(n => /^[A-Z]/.test(n)) ?? dep.localName;
|
|
982
|
+
const matchingParam = ctorParams.find((p) => p.type === depTypeName || p.type.includes(depTypeName));
|
|
983
|
+
if (matchingParam) {
|
|
984
|
+
mockPrefix = `mock${capitalize(matchingParam.name)}.`;
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
987
|
+
skipDep = true;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (skipDep)
|
|
991
|
+
continue;
|
|
992
|
+
const metaList = dep.importedFunctionMeta.length > 0
|
|
993
|
+
? dep.importedFunctionMeta
|
|
994
|
+
: dep.importedNames
|
|
995
|
+
.filter(n => n !== "default" && !/^[A-Z]/.test(n))
|
|
996
|
+
.map(n => ({ name: n, returnType: "unknown", isAsync: false, params: [] }));
|
|
997
|
+
for (const meta of metaList) {
|
|
998
|
+
if (isClassMethod && /^[A-Z]/.test(meta.name) && dep.importedFunctionMeta.length === 0)
|
|
999
|
+
continue;
|
|
1000
|
+
const expectTarget = `${mockPrefix}${meta.name}`;
|
|
1001
|
+
const depParams = meta.params ?? [];
|
|
1002
|
+
if (depParams.length > 0) {
|
|
1003
|
+
const callWithArgs = buildCalledWithArgs(depParams, testCase.paramNames, testCase.argLiterals);
|
|
1004
|
+
lines.push(` expect(${expectTarget}).toHaveBeenCalledWith(${callWithArgs});`);
|
|
1005
|
+
}
|
|
1006
|
+
else {
|
|
1007
|
+
lines.push(` expect(${expectTarget}).toHaveBeenCalled();`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
// Fallback: if no assertions were generated, add a meaningful default
|
|
1015
|
+
if (lines.length === assertStartIdx) {
|
|
1016
|
+
if (isAsync) {
|
|
1017
|
+
lines.push(" expect(result).toBeUndefined();");
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
lines.push(" expect(result).toBeDefined();");
|
|
902
1021
|
}
|
|
903
1022
|
}
|
|
904
1023
|
lines.push("});");
|
|
@@ -935,22 +1054,66 @@ function buildThrowsBlock(scenario, testCase, mockConstName) {
|
|
|
935
1054
|
const argsStr = testCase.paramNames.map(n => `${safeName(n)} as any`).join(", ");
|
|
936
1055
|
const caller = isStatic
|
|
937
1056
|
? `${scenario.className}.${fnName}`
|
|
938
|
-
: isMethod ? `instance.${fnName}` : fnName;
|
|
939
|
-
|
|
940
|
-
|
|
1057
|
+
: isMethod ? `instance.${scenario.isAbstractClass ? `call${capitalize(fnName)}` : fnName}` : fnName;
|
|
1058
|
+
// Use specific exception type from scenario label (e.g. "throws BadRequestException")
|
|
1059
|
+
const throwsExType = extractExceptionFromLabel(testCase.label, "throws");
|
|
1060
|
+
const errorMatcher = throwsExType || "Error";
|
|
1061
|
+
lines.push(` expect(() => ${caller}(${argsStr})).toThrow(${errorMatcher});`);
|
|
941
1062
|
lines.push("});");
|
|
942
1063
|
return lines.join("\n");
|
|
943
1064
|
}
|
|
944
1065
|
// ─── Rejects test block (async rejection) ────────────────────────────────────
|
|
945
|
-
function buildRejectsBlock(scenario, testCase, mockConstName) {
|
|
1066
|
+
function buildRejectsBlock(scenario, testCase, mockConstName, adapter = (0, frameworkAdapter_1.createAdapter)("jest")) {
|
|
946
1067
|
const lines = [];
|
|
947
1068
|
const fnName = scenario.functionName;
|
|
948
1069
|
const isMethod = !!scenario.className;
|
|
949
1070
|
const isStatic = !!scenario.isStatic;
|
|
1071
|
+
const isDepFailure = testCase.label === "propagates error when dependency fails";
|
|
950
1072
|
lines.push(`test(${JSON.stringify(testCase.label)}, async () => {`);
|
|
951
1073
|
// ── Arrange ──────────────────────────────────────────────────────
|
|
952
1074
|
const caseKey = camelCase(testCase.label);
|
|
953
|
-
if (
|
|
1075
|
+
if (isDepFailure && scenario.dependencies.length > 0) {
|
|
1076
|
+
// Set up a dependency to reject, triggering the error path
|
|
1077
|
+
lines.push(" // Arrange — force a dependency to reject");
|
|
1078
|
+
const isClassMethod = !!scenario.className;
|
|
1079
|
+
const ctorParams = scenario.constructorParams ?? [];
|
|
1080
|
+
// Find the first async dependency to make it fail
|
|
1081
|
+
let arranged = false;
|
|
1082
|
+
for (const dep of scenario.dependencies) {
|
|
1083
|
+
if (arranged)
|
|
1084
|
+
break;
|
|
1085
|
+
let mockPrefix = "";
|
|
1086
|
+
if (isClassMethod && ctorParams.length > 0) {
|
|
1087
|
+
const depTypeName = dep.importedNames.find(n => /^[A-Z]/.test(n)) ?? dep.localName;
|
|
1088
|
+
const matchingParam = ctorParams.find((p) => p.type === depTypeName || p.type.includes(depTypeName));
|
|
1089
|
+
if (matchingParam) {
|
|
1090
|
+
mockPrefix = `mock${capitalize(matchingParam.name)}.`;
|
|
1091
|
+
}
|
|
1092
|
+
else {
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
for (const meta of dep.importedFunctionMeta) {
|
|
1097
|
+
if (meta.isAsync) {
|
|
1098
|
+
lines.push(` const depError = new Error('dependency failure');`);
|
|
1099
|
+
lines.push(` ${mockPrefix}${meta.name}.mockRejectedValueOnce(depError);`);
|
|
1100
|
+
arranged = true;
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
if (!arranged) {
|
|
1106
|
+
lines.push(` // No async dependency found — add a mockRejectedValueOnce call for the relevant dep`);
|
|
1107
|
+
}
|
|
1108
|
+
// Declare input params
|
|
1109
|
+
if (testCase.paramNames.length > 0 && testCase.argLiterals.length > 0) {
|
|
1110
|
+
for (let i = 0; i < testCase.paramNames.length; i++) {
|
|
1111
|
+
lines.push(` const ${safeName(testCase.paramNames[i])} = ${testCase.argLiterals[i] ?? "undefined"};`);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
lines.push("");
|
|
1115
|
+
}
|
|
1116
|
+
else if (testCase.paramNames.length > 0 && mockConstName) {
|
|
954
1117
|
lines.push(" // Arrange");
|
|
955
1118
|
const mockRef = `mocks.${mockConstName}.${caseKey}`;
|
|
956
1119
|
for (const name of testCase.paramNames) {
|
|
@@ -972,12 +1135,35 @@ function buildRejectsBlock(scenario, testCase, mockConstName) {
|
|
|
972
1135
|
const argsStr = testCase.paramNames.map(n => `${safeName(n)} as any`).join(", ");
|
|
973
1136
|
const caller = isStatic
|
|
974
1137
|
? `${scenario.className}.${fnName}`
|
|
975
|
-
: isMethod ? `instance.${fnName}` : fnName;
|
|
976
|
-
|
|
977
|
-
|
|
1138
|
+
: isMethod ? `instance.${scenario.isAbstractClass ? `call${capitalize(fnName)}` : fnName}` : fnName;
|
|
1139
|
+
// Use specific exception type from scenario label (e.g. "rejects with NotFoundException")
|
|
1140
|
+
const rejectsExType = extractExceptionFromLabel(testCase.label, "rejects with");
|
|
1141
|
+
if (isDepFailure) {
|
|
1142
|
+
lines.push(` await expect(${caller}(${argsStr})).rejects.toThrow('dependency failure');`);
|
|
1143
|
+
}
|
|
1144
|
+
else if (rejectsExType) {
|
|
1145
|
+
lines.push(` await expect(${caller}(${argsStr})).rejects.toThrow(${rejectsExType});`);
|
|
1146
|
+
}
|
|
1147
|
+
else {
|
|
1148
|
+
lines.push(` await expect(${caller}(${argsStr})).rejects.toThrow();`);
|
|
1149
|
+
}
|
|
978
1150
|
lines.push("});");
|
|
979
1151
|
return lines.join("\n");
|
|
980
1152
|
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Extract an exception class name from a scenario label.
|
|
1155
|
+
* e.g. "throws BadRequestException" → "BadRequestException"
|
|
1156
|
+
* "rejects with NotFoundException" → "NotFoundException"
|
|
1157
|
+
*/
|
|
1158
|
+
function extractExceptionFromLabel(label, prefix) {
|
|
1159
|
+
if (!label.startsWith(prefix))
|
|
1160
|
+
return null;
|
|
1161
|
+
const rest = label.slice(prefix.length).trim();
|
|
1162
|
+
// Must be a PascalCase identifier (exception class name)
|
|
1163
|
+
if (/^[A-Z][a-zA-Z0-9]*$/.test(rest))
|
|
1164
|
+
return rest;
|
|
1165
|
+
return null;
|
|
1166
|
+
}
|
|
981
1167
|
// ─── Mock return value builder ────────────────────────────────────────────────
|
|
982
1168
|
/**
|
|
983
1169
|
* Given a ts-morph return type string (e.g. "Promise<User>" or "string[]"),
|
|
@@ -1012,6 +1198,27 @@ function buildMockReturnValue(returnType) {
|
|
|
1012
1198
|
// Object / interface / class type — generate a minimal shape
|
|
1013
1199
|
return "{ id: 1 }";
|
|
1014
1200
|
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Generate a minimal default return value for abstract method stubs.
|
|
1203
|
+
* Used when generating concrete subclasses for abstract class testing.
|
|
1204
|
+
*/
|
|
1205
|
+
function buildAbstractMethodDefault(returnType, isAsync) {
|
|
1206
|
+
const raw = returnType.trim();
|
|
1207
|
+
// Unwrap Promise<T> for async methods
|
|
1208
|
+
const promiseMatch = raw.match(/^Promise<(.+)>$/i);
|
|
1209
|
+
const inner = (promiseMatch ? promiseMatch[1] : raw).trim().toLowerCase();
|
|
1210
|
+
if (inner === "void" || inner === "undefined")
|
|
1211
|
+
return isAsync ? "undefined as any" : "undefined as any";
|
|
1212
|
+
if (inner === "boolean")
|
|
1213
|
+
return "true";
|
|
1214
|
+
if (inner === "number")
|
|
1215
|
+
return "0";
|
|
1216
|
+
if (inner === "string")
|
|
1217
|
+
return "''";
|
|
1218
|
+
if (inner.endsWith("[]") || inner.startsWith("array<"))
|
|
1219
|
+
return "[] as any";
|
|
1220
|
+
return "{} as any";
|
|
1221
|
+
}
|
|
1015
1222
|
// ─── Expect stubs based on return type ───────────────────────────────────────
|
|
1016
1223
|
/** Primitive/built-in type names that do NOT warrant snapshot assertions. */
|
|
1017
1224
|
const SNAPSHOT_SKIP_TYPES = new Set([
|
|
@@ -1032,52 +1239,67 @@ function buildExpectStubs(returnType, scenarioLabel, snapshotForComplexTypes = f
|
|
|
1032
1239
|
const t = returnType.trim().toLowerCase();
|
|
1033
1240
|
const unwrapped = unwrapType(t);
|
|
1034
1241
|
const stubs = [];
|
|
1035
|
-
|
|
1242
|
+
// Detect nullable return types (T | null) — guard/empty-input scenarios should expect null
|
|
1243
|
+
const isNullable = unwrapped.includes("| null") || unwrapped.includes("null |");
|
|
1244
|
+
const isGuardCase = scenarioLabel.includes("invalid") || scenarioLabel.includes("empty") || scenarioLabel.includes("guard") || scenarioLabel.includes("is 0");
|
|
1245
|
+
if (isNullable && isGuardCase) {
|
|
1246
|
+
stubs.push("expect(result).toBeNull();");
|
|
1247
|
+
return stubs;
|
|
1248
|
+
}
|
|
1249
|
+
// Handle null / void / undefined early — no mock return value needed
|
|
1250
|
+
if (unwrapped === "null") {
|
|
1251
|
+
stubs.push("expect(result).toBeNull();");
|
|
1252
|
+
return stubs;
|
|
1253
|
+
}
|
|
1254
|
+
if (unwrapped === "void" || unwrapped === "undefined") {
|
|
1255
|
+
stubs.push("expect(result).toBeUndefined();");
|
|
1256
|
+
return stubs;
|
|
1257
|
+
}
|
|
1258
|
+
// Derive the concrete expected value from the return type (mirrors mockReturnValue / mockResolvedValue)
|
|
1259
|
+
const expectedValue = buildMockReturnValue(returnType);
|
|
1036
1260
|
if (unwrapped === "number") {
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
if (scenarioLabel.includes("zero"))
|
|
1041
|
-
stubs.push("
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1261
|
+
if (scenarioLabel.includes("negative")) {
|
|
1262
|
+
stubs.push("expect(result).toBeLessThan(0);");
|
|
1263
|
+
}
|
|
1264
|
+
else if (scenarioLabel.includes("zero")) {
|
|
1265
|
+
stubs.push("expect(result).toBe(0);");
|
|
1266
|
+
}
|
|
1267
|
+
else {
|
|
1268
|
+
stubs.push(`expect(result).toBe(${expectedValue});`);
|
|
1269
|
+
}
|
|
1045
1270
|
}
|
|
1046
1271
|
else if (unwrapped === "string") {
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
stubs.push(
|
|
1052
|
-
|
|
1272
|
+
if (scenarioLabel.includes("empty")) {
|
|
1273
|
+
stubs.push("expect(result).toBe('');");
|
|
1274
|
+
}
|
|
1275
|
+
else {
|
|
1276
|
+
stubs.push(`expect(result).toBe(${expectedValue});`);
|
|
1277
|
+
}
|
|
1053
1278
|
}
|
|
1054
1279
|
else if (unwrapped === "boolean") {
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
if (scenarioLabel.includes("falsy"))
|
|
1059
|
-
stubs.push("
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
stubs.push("expect(result).toBeNull();");
|
|
1065
|
-
}
|
|
1066
|
-
else if (unwrapped === "void" || unwrapped === "undefined") {
|
|
1067
|
-
stubs.length = 0;
|
|
1068
|
-
stubs.push("expect(result).toBeUndefined();");
|
|
1280
|
+
if (scenarioLabel.includes("truthy")) {
|
|
1281
|
+
stubs.push("expect(result).toBe(true);");
|
|
1282
|
+
}
|
|
1283
|
+
else if (scenarioLabel.includes("falsy")) {
|
|
1284
|
+
stubs.push("expect(result).toBe(false);");
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
stubs.push(`expect(result).toBe(${expectedValue});`);
|
|
1288
|
+
}
|
|
1069
1289
|
}
|
|
1070
1290
|
else if (unwrapped.endsWith("[]") || unwrapped.startsWith("array<")) {
|
|
1071
1291
|
stubs.push("expect(Array.isArray(result)).toBe(true);");
|
|
1072
|
-
if (scenarioLabel.includes("empty"))
|
|
1073
|
-
stubs.push("
|
|
1074
|
-
|
|
1292
|
+
if (scenarioLabel.includes("empty")) {
|
|
1293
|
+
stubs.push("expect(result).toHaveLength(0);");
|
|
1294
|
+
}
|
|
1295
|
+
stubs.push(`expect(result).toEqual(${expectedValue});`);
|
|
1075
1296
|
}
|
|
1076
1297
|
else if (/ApiResponseDto/i.test(returnType)) {
|
|
1077
1298
|
// Controller methods wrapped in ApiResponseDto<T>
|
|
1078
|
-
stubs.push("expect(result).
|
|
1079
|
-
stubs.push("expect
|
|
1080
|
-
stubs.push("
|
|
1299
|
+
stubs.push("expect(result).toEqual(expect.objectContaining({");
|
|
1300
|
+
stubs.push(" success: expect.any(Boolean),");
|
|
1301
|
+
stubs.push(" data: expect.anything(),");
|
|
1302
|
+
stubs.push("}));");
|
|
1081
1303
|
}
|
|
1082
1304
|
else {
|
|
1083
1305
|
// Try to parse as an inline object type: { id: string; status: string; ... }
|
|
@@ -1088,14 +1310,9 @@ function buildExpectStubs(returnType, scenarioLabel, snapshotForComplexTypes = f
|
|
|
1088
1310
|
stubs.push(` ${prop.name}: ${tsTypeToJestMatcher(prop.tsType)},`);
|
|
1089
1311
|
}
|
|
1090
1312
|
stubs.push("});");
|
|
1091
|
-
stubs.push("// TODO: assert exact values, e.g. expect(result.id).toBe(expectedId)");
|
|
1092
1313
|
}
|
|
1093
1314
|
else {
|
|
1094
|
-
stubs.push(
|
|
1095
|
-
if (snapshotForComplexTypes && isComplexReturnType(unwrapped)) {
|
|
1096
|
-
stubs.push("expect(result).toMatchSnapshot(); // Update snapshot after first run");
|
|
1097
|
-
}
|
|
1098
|
-
stubs.push("// TODO: add domain assertion — e.g. expect(result).toEqual(expected) or toHaveProperty('key', value)");
|
|
1315
|
+
stubs.push(`expect(result).toEqual(${expectedValue});`);
|
|
1099
1316
|
}
|
|
1100
1317
|
}
|
|
1101
1318
|
return stubs;
|
|
@@ -1245,6 +1462,9 @@ function computeRelativeImport(targetFilePath, fromFilePath) {
|
|
|
1245
1462
|
if (!rel.startsWith("."))
|
|
1246
1463
|
rel = "./" + rel;
|
|
1247
1464
|
const basename = path.basename(targetFilePath, ".ts");
|
|
1465
|
+
// Avoid double-slash when same directory (rel === "./")
|
|
1466
|
+
if (rel === ".")
|
|
1467
|
+
return `./${basename}`;
|
|
1248
1468
|
return `${rel}/${basename}`;
|
|
1249
1469
|
}
|
|
1250
1470
|
// ─── Utility helpers ─────────────────────────────────────────────────────────
|
|
@@ -1271,4 +1491,137 @@ function safeName(name) {
|
|
|
1271
1491
|
function capitalize(str) {
|
|
1272
1492
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
1273
1493
|
}
|
|
1494
|
+
// ─── Constructor dependency resolution ───────────────────────────────────────
|
|
1495
|
+
/** Names to skip when resolving class methods — lifecycle hooks, internals, built-in prototypes */
|
|
1496
|
+
const SKIP_METHOD_NAMES = new Set([
|
|
1497
|
+
// Object prototype
|
|
1498
|
+
"constructor", "toString", "valueOf", "hasOwnProperty",
|
|
1499
|
+
"isPrototypeOf", "propertyIsEnumerable", "toLocaleString",
|
|
1500
|
+
// NestJS lifecycle hooks
|
|
1501
|
+
"onModuleInit", "onModuleDestroy", "onApplicationBootstrap",
|
|
1502
|
+
"onApplicationShutdown", "beforeApplicationShutdown",
|
|
1503
|
+
// String prototype methods
|
|
1504
|
+
"charAt", "charCodeAt", "codePointAt", "concat", "indexOf",
|
|
1505
|
+
"lastIndexOf", "localeCompare", "match", "matchAll", "replace",
|
|
1506
|
+
"replaceAll", "search", "slice", "split", "substring", "substr",
|
|
1507
|
+
"toLowerCase", "toLocaleLowerCase", "toUpperCase", "toLocaleUpperCase",
|
|
1508
|
+
"trim", "trimStart", "trimEnd", "trimLeft", "trimRight",
|
|
1509
|
+
"padStart", "padEnd", "repeat", "normalize", "includes",
|
|
1510
|
+
"startsWith", "endsWith", "at", "anchor", "big", "blink",
|
|
1511
|
+
"bold", "fixed", "fontcolor", "fontsize", "italics", "link",
|
|
1512
|
+
"small", "strike", "sub", "sup",
|
|
1513
|
+
// Array prototype methods
|
|
1514
|
+
"push", "pop", "shift", "unshift", "splice", "sort", "reverse",
|
|
1515
|
+
"map", "filter", "reduce", "reduceRight", "forEach", "some",
|
|
1516
|
+
"every", "find", "findIndex", "findLast", "findLastIndex",
|
|
1517
|
+
"flat", "flatMap", "fill", "copyWithin", "entries", "keys",
|
|
1518
|
+
"values", "join",
|
|
1519
|
+
// Number / Symbol / common built-ins
|
|
1520
|
+
"toFixed", "toPrecision", "toExponential",
|
|
1521
|
+
"length", "size",
|
|
1522
|
+
]);
|
|
1523
|
+
/**
|
|
1524
|
+
* For constructor-injected dependencies, resolve their actual public method
|
|
1525
|
+
* signatures by reading the dependency class via ts-morph. This fills in
|
|
1526
|
+
* `depMethodsByType` for params whose methods weren't found through the
|
|
1527
|
+
* body-text dependency scan (which misses `this.<param>.method()` patterns).
|
|
1528
|
+
*/
|
|
1529
|
+
function resolveCtorDepMethods(ctorParams, depMethodsByType, sourceFilePath) {
|
|
1530
|
+
// Lazily create a ts-morph project only if needed
|
|
1531
|
+
let project = null;
|
|
1532
|
+
for (const param of ctorParams) {
|
|
1533
|
+
// Extract short type name from potentially fully-qualified type
|
|
1534
|
+
const shortType = param.type
|
|
1535
|
+
.replace(/^import\([^)]*\)\./, "")
|
|
1536
|
+
.replace(/<.*>$/, "")
|
|
1537
|
+
.trim();
|
|
1538
|
+
// Skip if we already have methods for this type (from dependency scan)
|
|
1539
|
+
if (depMethodsByType.has(shortType) && depMethodsByType.get(shortType).size > 0)
|
|
1540
|
+
continue;
|
|
1541
|
+
if (depMethodsByType.has(param.type) && depMethodsByType.get(param.type).size > 0)
|
|
1542
|
+
continue;
|
|
1543
|
+
// Skip primitives, built-in types, and ORM/logger patterns (handled elsewhere)
|
|
1544
|
+
const lower = shortType.toLowerCase();
|
|
1545
|
+
if (/^(string|number|boolean|void|any|never|unknown|undefined|null|object)$/.test(lower))
|
|
1546
|
+
continue;
|
|
1547
|
+
if (/nodepgdatabase|pgtransaction|drizzle|logger|configservice|eventemitter/i.test(lower))
|
|
1548
|
+
continue;
|
|
1549
|
+
try {
|
|
1550
|
+
if (!project) {
|
|
1551
|
+
const tsConfig = findTsConfig(sourceFilePath);
|
|
1552
|
+
project = new ts_morph_1.Project({ tsConfigFilePath: tsConfig });
|
|
1553
|
+
}
|
|
1554
|
+
// Find the source file of the dependency by resolving the import.
|
|
1555
|
+
// Normalize path separators — ts-morph uses forward slashes internally.
|
|
1556
|
+
const normalizedPath = sourceFilePath.replace(/\\/g, "/");
|
|
1557
|
+
let sourceFile = project.getSourceFile(normalizedPath);
|
|
1558
|
+
if (!sourceFile) {
|
|
1559
|
+
sourceFile = project.getSourceFile(sourceFilePath);
|
|
1560
|
+
}
|
|
1561
|
+
if (!sourceFile) {
|
|
1562
|
+
// If still not found, try adding it explicitly
|
|
1563
|
+
try {
|
|
1564
|
+
sourceFile = project.addSourceFileAtPath(sourceFilePath);
|
|
1565
|
+
}
|
|
1566
|
+
catch { /* ignore */ }
|
|
1567
|
+
}
|
|
1568
|
+
if (!sourceFile)
|
|
1569
|
+
continue;
|
|
1570
|
+
// Look for the import declaration that brings in this type
|
|
1571
|
+
const ctorImportDecl = sourceFile.getImportDeclarations().find(decl => {
|
|
1572
|
+
const namedImps = decl.getNamedImports();
|
|
1573
|
+
return namedImps.some(ni => ni.getName() === shortType);
|
|
1574
|
+
});
|
|
1575
|
+
if (!ctorImportDecl)
|
|
1576
|
+
continue;
|
|
1577
|
+
const ctorNamedImport = ctorImportDecl.getNamedImports().find(ni => ni.getName() === shortType);
|
|
1578
|
+
if (!ctorNamedImport)
|
|
1579
|
+
continue;
|
|
1580
|
+
let type = ctorNamedImport.getNameNode().getType();
|
|
1581
|
+
// If the type is `typeof ClassName` (the constructor), get the instance type
|
|
1582
|
+
// by looking at the construct signature's return type.
|
|
1583
|
+
const constructSigs = type.getConstructSignatures();
|
|
1584
|
+
if (constructSigs.length > 0) {
|
|
1585
|
+
type = constructSigs[0].getReturnType();
|
|
1586
|
+
}
|
|
1587
|
+
const props = type.getProperties();
|
|
1588
|
+
const methodSet = new Set();
|
|
1589
|
+
for (const prop of props) {
|
|
1590
|
+
const methodName = prop.getName();
|
|
1591
|
+
if (methodName.startsWith("_") || methodName.startsWith("#"))
|
|
1592
|
+
continue;
|
|
1593
|
+
if (SKIP_METHOD_NAMES.has(methodName))
|
|
1594
|
+
continue;
|
|
1595
|
+
const propType = prop.getTypeAtLocation(ctorNamedImport);
|
|
1596
|
+
const callSigs = propType.getCallSignatures();
|
|
1597
|
+
if (callSigs.length === 0)
|
|
1598
|
+
continue;
|
|
1599
|
+
const sig = callSigs[0];
|
|
1600
|
+
const returnTypeText = sig.getReturnType().getText();
|
|
1601
|
+
const isAsync = /^Promise</i.test(returnTypeText);
|
|
1602
|
+
methodSet.add({ name: methodName, isAsync });
|
|
1603
|
+
}
|
|
1604
|
+
if (methodSet.size > 0) {
|
|
1605
|
+
depMethodsByType.set(shortType, methodSet);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
catch {
|
|
1609
|
+
// Silently continue — AST resolution can fail for external deps
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Walk up from sourceFilePath to find the nearest tsconfig.json.
|
|
1615
|
+
*/
|
|
1616
|
+
function findTsConfig(sourceFilePath) {
|
|
1617
|
+
let dir = path.dirname(sourceFilePath);
|
|
1618
|
+
while (dir !== path.dirname(dir)) {
|
|
1619
|
+
const candidate = path.join(dir, "tsconfig.json");
|
|
1620
|
+
if (fs.existsSync(candidate))
|
|
1621
|
+
return candidate;
|
|
1622
|
+
dir = path.dirname(dir);
|
|
1623
|
+
}
|
|
1624
|
+
// Fallback — let ts-morph use its own defaults
|
|
1625
|
+
return path.join(path.dirname(sourceFilePath), "tsconfig.json");
|
|
1626
|
+
}
|
|
1274
1627
|
//# sourceMappingURL=generator.js.map
|