@atlashub/smartstack-mcp 1.17.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +3858 -403
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -82,10 +82,10 @@ import { stat, mkdir, readFile, writeFile, cp, rm } from "fs/promises";
|
|
|
82
82
|
import path from "path";
|
|
83
83
|
import { glob } from "glob";
|
|
84
84
|
var FileSystemError = class extends Error {
|
|
85
|
-
constructor(message, operation,
|
|
85
|
+
constructor(message, operation, path27, cause) {
|
|
86
86
|
super(message);
|
|
87
87
|
this.operation = operation;
|
|
88
|
-
this.path =
|
|
88
|
+
this.path = path27;
|
|
89
89
|
this.cause = cause;
|
|
90
90
|
this.name = "FileSystemError";
|
|
91
91
|
}
|
|
@@ -189,6 +189,288 @@ async function findFiles(pattern, options = {}) {
|
|
|
189
189
|
return files;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// src/types/index.ts
|
|
193
|
+
import { z } from "zod";
|
|
194
|
+
var ProjectConfigSchema = z.object({
|
|
195
|
+
projectType: z.enum(["client", "platform"]).default("platform"),
|
|
196
|
+
dbContext: z.enum(["core", "extensions"]).default("core"),
|
|
197
|
+
baseNamespace: z.string().optional(),
|
|
198
|
+
smartStackVersion: z.string().optional(),
|
|
199
|
+
initialized: z.string().optional()
|
|
200
|
+
});
|
|
201
|
+
var SmartStackConfigSchema = z.object({
|
|
202
|
+
projectPath: z.string(),
|
|
203
|
+
apiUrl: z.string().url().optional(),
|
|
204
|
+
apiEnabled: z.boolean().default(true)
|
|
205
|
+
});
|
|
206
|
+
var ConventionsConfigSchema = z.object({
|
|
207
|
+
schemas: z.object({
|
|
208
|
+
platform: z.string().default("core"),
|
|
209
|
+
extensions: z.string().default("extensions")
|
|
210
|
+
}),
|
|
211
|
+
tablePrefixes: z.array(z.string()).default([
|
|
212
|
+
"auth_",
|
|
213
|
+
"nav_",
|
|
214
|
+
"usr_",
|
|
215
|
+
"ai_",
|
|
216
|
+
"cfg_",
|
|
217
|
+
"wkf_",
|
|
218
|
+
"support_",
|
|
219
|
+
"entra_",
|
|
220
|
+
"ref_",
|
|
221
|
+
"loc_",
|
|
222
|
+
"lic_"
|
|
223
|
+
]),
|
|
224
|
+
scopeTypes: z.array(z.string()).default(["Core", "Extension", "Partner", "Community"]),
|
|
225
|
+
migrationFormat: z.string().default("{context}_v{version}_{sequence}_{Description}"),
|
|
226
|
+
namespaces: z.object({
|
|
227
|
+
domain: z.string(),
|
|
228
|
+
application: z.string(),
|
|
229
|
+
infrastructure: z.string(),
|
|
230
|
+
api: z.string()
|
|
231
|
+
}),
|
|
232
|
+
servicePattern: z.object({
|
|
233
|
+
interface: z.string().default("I{Name}Service"),
|
|
234
|
+
implementation: z.string().default("{Name}Service")
|
|
235
|
+
})
|
|
236
|
+
});
|
|
237
|
+
var EfCoreContextSchema = z.object({
|
|
238
|
+
name: z.string(),
|
|
239
|
+
projectPath: z.string(),
|
|
240
|
+
migrationsFolder: z.string().default("Migrations")
|
|
241
|
+
});
|
|
242
|
+
var EfCoreConfigSchema = z.object({
|
|
243
|
+
contexts: z.array(EfCoreContextSchema),
|
|
244
|
+
validation: z.object({
|
|
245
|
+
checkModelSnapshot: z.boolean().default(true),
|
|
246
|
+
checkMigrationOrder: z.boolean().default(true),
|
|
247
|
+
requireBuildSuccess: z.boolean().default(true)
|
|
248
|
+
})
|
|
249
|
+
});
|
|
250
|
+
var ScaffoldingConfigSchema = z.object({
|
|
251
|
+
outputPath: z.string(),
|
|
252
|
+
templates: z.object({
|
|
253
|
+
service: z.string(),
|
|
254
|
+
entity: z.string(),
|
|
255
|
+
controller: z.string(),
|
|
256
|
+
component: z.string()
|
|
257
|
+
})
|
|
258
|
+
});
|
|
259
|
+
var ConfigSchema = z.object({
|
|
260
|
+
version: z.string(),
|
|
261
|
+
smartstack: SmartStackConfigSchema,
|
|
262
|
+
conventions: ConventionsConfigSchema,
|
|
263
|
+
efcore: EfCoreConfigSchema,
|
|
264
|
+
scaffolding: ScaffoldingConfigSchema,
|
|
265
|
+
// Loaded from .smartstack/config.json if present
|
|
266
|
+
projectConfig: ProjectConfigSchema.optional(),
|
|
267
|
+
// Resolved DbContext to use (from projectConfig or default)
|
|
268
|
+
defaultDbContext: z.enum(["core", "extensions"]).default("core")
|
|
269
|
+
});
|
|
270
|
+
var ValidateConventionsInputSchema = z.object({
|
|
271
|
+
path: z.string().optional().describe("Project path to validate (default: SmartStack.app path)"),
|
|
272
|
+
checks: z.array(z.enum(["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "hierarchies", "protected-actions", "all"])).default(["all"]).describe("Types of checks to perform")
|
|
273
|
+
});
|
|
274
|
+
var CheckMigrationsInputSchema = z.object({
|
|
275
|
+
projectPath: z.string().optional().describe("EF Core project path"),
|
|
276
|
+
branch: z.string().optional().describe("Git branch to check (default: current)"),
|
|
277
|
+
compareBranch: z.string().optional().describe("Branch to compare against")
|
|
278
|
+
});
|
|
279
|
+
var EntityPropertySchema = z.object({
|
|
280
|
+
name: z.string().describe("Property name (PascalCase)"),
|
|
281
|
+
type: z.string().describe("C# type (string, int, Guid, DateTime, etc.)"),
|
|
282
|
+
required: z.boolean().optional().describe("If true, property is required"),
|
|
283
|
+
maxLength: z.number().optional().describe("Max length for string properties")
|
|
284
|
+
});
|
|
285
|
+
var ScaffoldExtensionInputSchema = z.object({
|
|
286
|
+
type: z.enum(["feature", "service", "entity", "controller", "component", "test", "dto", "validator", "repository"]).describe('Type of extension to scaffold. Use "feature" for full-stack generation.'),
|
|
287
|
+
name: z.string().describe('Name of the extension (e.g., "UserProfile", "Order")'),
|
|
288
|
+
options: z.object({
|
|
289
|
+
namespace: z.string().optional().describe("Custom namespace"),
|
|
290
|
+
baseEntity: z.string().optional().describe("Base entity to extend (for entity type)"),
|
|
291
|
+
methods: z.array(z.string()).optional().describe("Methods to generate (for service type)"),
|
|
292
|
+
outputPath: z.string().optional().describe("Custom output path"),
|
|
293
|
+
isSystemEntity: z.boolean().optional().describe("If true, creates a system entity without TenantId"),
|
|
294
|
+
tablePrefix: z.string().optional().describe('Domain prefix for table name (e.g., "auth_", "nav_", "cfg_")'),
|
|
295
|
+
schema: z.enum(["core", "extensions"]).optional().describe("Database schema (default: core)"),
|
|
296
|
+
dryRun: z.boolean().optional().describe("If true, preview generated code without writing files"),
|
|
297
|
+
skipService: z.boolean().optional().describe("For feature type: skip service generation"),
|
|
298
|
+
skipController: z.boolean().optional().describe("For feature type: skip controller generation"),
|
|
299
|
+
skipComponent: z.boolean().optional().describe("For feature type: skip React component generation"),
|
|
300
|
+
clientExtension: z.boolean().optional().describe("If true, use extensions schema for client-specific code"),
|
|
301
|
+
withTests: z.boolean().optional().describe("For feature type: also generate unit tests"),
|
|
302
|
+
withDtos: z.boolean().optional().describe("For feature type: generate DTOs (Create, Update, Response)"),
|
|
303
|
+
withValidation: z.boolean().optional().describe("For feature type: generate FluentValidation validators"),
|
|
304
|
+
withRepository: z.boolean().optional().describe("For feature type: generate repository pattern"),
|
|
305
|
+
entityProperties: z.array(EntityPropertySchema).optional().describe("Entity properties for DTO/Validator generation"),
|
|
306
|
+
navRoute: z.string().optional().describe('Navigation route path for controller (e.g., "platform.administration.users"). Required for controllers.'),
|
|
307
|
+
navRouteSuffix: z.string().optional().describe('Optional suffix for NavRoute (e.g., "dashboard" for sub-resources)'),
|
|
308
|
+
withHierarchyFunction: z.boolean().optional().describe("For entity type with self-reference (ParentId): generate TVF SQL script for hierarchy traversal"),
|
|
309
|
+
hierarchyDirection: z.enum(["ancestors", "descendants", "both"]).optional().describe("Direction for hierarchy traversal function (default: both)")
|
|
310
|
+
}).optional()
|
|
311
|
+
});
|
|
312
|
+
var ApiDocsInputSchema = z.object({
|
|
313
|
+
endpoint: z.string().optional().describe('Filter by endpoint path (e.g., "/api/users")'),
|
|
314
|
+
format: z.enum(["markdown", "json", "openapi"]).default("markdown").describe("Output format"),
|
|
315
|
+
controller: z.string().optional().describe("Filter by controller name")
|
|
316
|
+
});
|
|
317
|
+
var SuggestMigrationInputSchema = z.object({
|
|
318
|
+
description: z.string().describe('Description of what the migration does (e.g., "Add User Profiles", "Create Orders Table")'),
|
|
319
|
+
context: z.enum(["core", "extensions"]).optional().describe("DbContext name (default: core)"),
|
|
320
|
+
version: z.string().optional().describe('Semver version (e.g., "1.0.0", "1.2.0"). If not provided, uses latest from existing migrations.')
|
|
321
|
+
});
|
|
322
|
+
var GeneratePermissionsInputSchema = z.object({
|
|
323
|
+
navRoute: z.string().optional().describe('NavRoute path (e.g., "platform.administration.entra"). If not provided, scans all controllers.'),
|
|
324
|
+
actions: z.array(z.string()).optional().describe("Custom actions to generate (default: read, create, update, delete)"),
|
|
325
|
+
includeStandardActions: z.boolean().default(true).describe("Include standard CRUD actions (read, create, update, delete)"),
|
|
326
|
+
generateMigration: z.boolean().default(true).describe("Generate EF Core migration to seed permissions in database"),
|
|
327
|
+
dryRun: z.boolean().default(false).describe("Preview without writing files or creating migration")
|
|
328
|
+
});
|
|
329
|
+
var TestTypeSchema = z.enum(["unit", "integration", "security", "e2e"]);
|
|
330
|
+
var TestTargetSchema = z.enum(["entity", "service", "controller", "validator", "repository", "all"]);
|
|
331
|
+
var ScaffoldTestsInputSchema = z.object({
|
|
332
|
+
target: TestTargetSchema.describe("Type of component to test"),
|
|
333
|
+
name: z.string().min(1).describe('Component name (PascalCase, e.g., "User", "Order")'),
|
|
334
|
+
testTypes: z.array(TestTypeSchema).default(["unit"]).describe("Types of tests to generate"),
|
|
335
|
+
options: z.object({
|
|
336
|
+
includeEdgeCases: z.boolean().default(true).describe("Include edge case tests"),
|
|
337
|
+
includeTenantIsolation: z.boolean().default(true).describe("Include tenant isolation tests"),
|
|
338
|
+
includeSoftDelete: z.boolean().default(true).describe("Include soft delete tests"),
|
|
339
|
+
includeAudit: z.boolean().default(true).describe("Include audit trail tests"),
|
|
340
|
+
includeValidation: z.boolean().default(true).describe("Include validation tests"),
|
|
341
|
+
includeAuthorization: z.boolean().default(false).describe("Include authorization tests"),
|
|
342
|
+
includePerformance: z.boolean().default(false).describe("Include performance tests"),
|
|
343
|
+
entityProperties: z.array(EntityPropertySchema).optional().describe("Entity properties for test generation"),
|
|
344
|
+
isSystemEntity: z.boolean().default(false).describe("If true, entity has no TenantId"),
|
|
345
|
+
dryRun: z.boolean().default(false).describe("Preview without writing files")
|
|
346
|
+
}).optional()
|
|
347
|
+
});
|
|
348
|
+
var AnalyzeTestCoverageInputSchema = z.object({
|
|
349
|
+
path: z.string().optional().describe("Project path to analyze"),
|
|
350
|
+
scope: z.enum(["entity", "service", "controller", "all"]).default("all").describe("Scope of analysis"),
|
|
351
|
+
outputFormat: z.enum(["summary", "detailed", "json"]).default("summary").describe("Output format"),
|
|
352
|
+
includeRecommendations: z.boolean().default(true).describe("Include recommendations for missing tests")
|
|
353
|
+
});
|
|
354
|
+
var ValidateTestConventionsInputSchema = z.object({
|
|
355
|
+
path: z.string().optional().describe("Project path to validate"),
|
|
356
|
+
checks: z.array(z.enum(["naming", "structure", "patterns", "assertions", "mocking", "all"])).default(["all"]).describe("Types of convention checks to perform"),
|
|
357
|
+
autoFix: z.boolean().default(false).describe("Automatically fix minor issues")
|
|
358
|
+
});
|
|
359
|
+
var SuggestTestScenariosInputSchema = z.object({
|
|
360
|
+
target: z.enum(["entity", "service", "controller", "file"]).describe("Type of target to analyze"),
|
|
361
|
+
name: z.string().min(1).describe("Component name or file path"),
|
|
362
|
+
depth: z.enum(["basic", "comprehensive", "security-focused"]).default("comprehensive").describe("Depth of analysis")
|
|
363
|
+
});
|
|
364
|
+
var SecurityCheckSchema = z.enum([
|
|
365
|
+
"hardcoded-secrets",
|
|
366
|
+
"sql-injection",
|
|
367
|
+
"tenant-isolation",
|
|
368
|
+
"authorization",
|
|
369
|
+
"dangerous-functions",
|
|
370
|
+
"input-validation",
|
|
371
|
+
"xss",
|
|
372
|
+
"csrf",
|
|
373
|
+
"logging-sensitive",
|
|
374
|
+
"all"
|
|
375
|
+
]);
|
|
376
|
+
var ValidateSecurityInputSchema = z.object({
|
|
377
|
+
path: z.string().optional().describe("Project path to validate (default: SmartStack.app path)"),
|
|
378
|
+
checks: z.array(SecurityCheckSchema).default(["all"]).describe("Security checks to run"),
|
|
379
|
+
severity: z.enum(["blocking", "all"]).optional().describe("Filter results by severity")
|
|
380
|
+
});
|
|
381
|
+
var QualityMetricSchema = z.enum([
|
|
382
|
+
"cognitive-complexity",
|
|
383
|
+
"cyclomatic-complexity",
|
|
384
|
+
"function-size",
|
|
385
|
+
"nesting-depth",
|
|
386
|
+
"parameter-count",
|
|
387
|
+
"code-duplication",
|
|
388
|
+
"file-size",
|
|
389
|
+
"all"
|
|
390
|
+
]);
|
|
391
|
+
var AnalyzeCodeQualityInputSchema = z.object({
|
|
392
|
+
path: z.string().optional().describe("Project path to analyze (default: SmartStack.app path)"),
|
|
393
|
+
metrics: z.array(QualityMetricSchema).default(["all"]).describe("Metrics to analyze"),
|
|
394
|
+
threshold: z.enum(["strict", "normal", "lenient"]).default("normal").describe("Threshold level for violations"),
|
|
395
|
+
scope: z.enum(["changed", "all"]).default("all").describe("Analyze only changed files or all")
|
|
396
|
+
});
|
|
397
|
+
var ScaffoldApiClientInputSchema = z.object({
|
|
398
|
+
path: z.string().optional().describe("Path to SmartStack project root (defaults to configured project path)"),
|
|
399
|
+
navRoute: z.string().min(1).describe('NavRoute path (e.g., "platform.administration.users")'),
|
|
400
|
+
name: z.string().min(1).describe('Entity name in PascalCase (e.g., "User", "Order")'),
|
|
401
|
+
methods: z.array(z.enum(["getAll", "getById", "create", "update", "delete", "search", "export"])).default(["getAll", "getById", "create", "update", "delete"]).describe("API methods to generate"),
|
|
402
|
+
options: z.object({
|
|
403
|
+
outputPath: z.string().optional().describe("Custom output path for generated files"),
|
|
404
|
+
includeTypes: z.boolean().default(true).describe("Generate TypeScript types"),
|
|
405
|
+
includeHook: z.boolean().default(true).describe("Generate React Query hook"),
|
|
406
|
+
dryRun: z.boolean().default(false).describe("Preview without writing files")
|
|
407
|
+
}).optional()
|
|
408
|
+
});
|
|
409
|
+
var ScaffoldRoutesInputSchema = z.object({
|
|
410
|
+
path: z.string().optional().describe("Path to SmartStack project root (defaults to configured project path)"),
|
|
411
|
+
source: z.enum(["controllers", "navigation", "manual"]).default("controllers").describe("Source for route discovery: controllers (scan NavRoute attributes), navigation (from DB), manual (from config)"),
|
|
412
|
+
scope: z.enum(["all", "platform", "business", "extensions"]).default("all").describe("Scope of routes to generate"),
|
|
413
|
+
options: z.object({
|
|
414
|
+
outputPath: z.string().optional().describe("Custom output path"),
|
|
415
|
+
includeLayouts: z.boolean().default(true).describe("Generate layout components"),
|
|
416
|
+
includeGuards: z.boolean().default(true).describe("Include route guards for permissions"),
|
|
417
|
+
generateRegistry: z.boolean().default(true).describe("Generate navRoutes.generated.ts"),
|
|
418
|
+
dryRun: z.boolean().default(false).describe("Preview without writing files")
|
|
419
|
+
}).optional()
|
|
420
|
+
});
|
|
421
|
+
var ValidateFrontendRoutesInputSchema = z.object({
|
|
422
|
+
path: z.string().optional().describe("Path to SmartStack project root (defaults to configured project path)"),
|
|
423
|
+
scope: z.enum(["api-clients", "routes", "registry", "all"]).default("all").describe("Scope of validation"),
|
|
424
|
+
options: z.object({
|
|
425
|
+
fix: z.boolean().default(false).describe("Auto-fix minor issues"),
|
|
426
|
+
strict: z.boolean().default(false).describe("Fail on warnings")
|
|
427
|
+
}).optional()
|
|
428
|
+
});
|
|
429
|
+
var SlotDefinitionSchema = z.object({
|
|
430
|
+
name: z.string().describe('Slot name (e.g., "users.form.fields.after")'),
|
|
431
|
+
location: z.string().describe("Where the slot is located in the page"),
|
|
432
|
+
contextProps: z.array(z.string()).optional().describe("Props passed to slot content")
|
|
433
|
+
});
|
|
434
|
+
var ScaffoldFrontendExtensionInputSchema = z.object({
|
|
435
|
+
type: z.enum(["infrastructure", "page-slots", "extension-example", "all"]).describe("Type of extension to scaffold: infrastructure (types, contexts), page-slots (add slots to a page), extension-example (client usage example), all"),
|
|
436
|
+
target: z.string().optional().describe('Target page or component (e.g., "UsersPage", "UserDetailPage")'),
|
|
437
|
+
options: z.object({
|
|
438
|
+
slots: z.array(SlotDefinitionSchema).optional().describe("Slot definitions to add"),
|
|
439
|
+
outputPath: z.string().optional().describe("Custom output path"),
|
|
440
|
+
dryRun: z.boolean().default(false).describe("Preview without writing files"),
|
|
441
|
+
overwrite: z.boolean().default(false).describe("Overwrite existing files")
|
|
442
|
+
}).optional()
|
|
443
|
+
});
|
|
444
|
+
var AnalyzeExtensionPointsInputSchema = z.object({
|
|
445
|
+
path: z.string().optional().describe("Path to SmartStack web project"),
|
|
446
|
+
target: z.enum(["pages", "components", "forms", "tables", "all"]).default("all").describe("Type of components to analyze"),
|
|
447
|
+
filter: z.string().optional().describe('Filter by file name pattern (e.g., "User*")')
|
|
448
|
+
});
|
|
449
|
+
var AnalyzeHierarchyPatternsInputSchema = z.object({
|
|
450
|
+
path: z.string().optional().describe("Project path to analyze (default: SmartStack.app path)"),
|
|
451
|
+
entityFilter: z.string().optional().describe('Filter entities by name pattern (e.g., "User*", "*Group*")'),
|
|
452
|
+
includeRecommendations: z.boolean().default(true).describe("Include pattern recommendations for each hierarchy"),
|
|
453
|
+
outputFormat: z.enum(["summary", "detailed", "json"]).default("detailed").describe("Output format")
|
|
454
|
+
});
|
|
455
|
+
var ReviewCodeCheckSchema = z.enum([
|
|
456
|
+
"security",
|
|
457
|
+
"architecture",
|
|
458
|
+
"hardcoded-values",
|
|
459
|
+
"tests",
|
|
460
|
+
"ai-hallucinations",
|
|
461
|
+
"performance",
|
|
462
|
+
"dead-code",
|
|
463
|
+
"i18n",
|
|
464
|
+
"accessibility",
|
|
465
|
+
"all"
|
|
466
|
+
]);
|
|
467
|
+
var ReviewCodeInputSchema = z.object({
|
|
468
|
+
path: z.string().optional().describe("Project path to review (default: SmartStack.app path)"),
|
|
469
|
+
scope: z.enum(["all", "changed", "staged"]).default("all").describe("Files to review: all, only changed (git diff), or staged files"),
|
|
470
|
+
checks: z.array(ReviewCodeCheckSchema).default(["all"]).describe("Categories of checks to run"),
|
|
471
|
+
severity: z.enum(["blocking", "critical", "warning", "all"]).default("all").describe("Filter results by minimum severity")
|
|
472
|
+
});
|
|
473
|
+
|
|
192
474
|
// src/utils/dotnet.ts
|
|
193
475
|
import { exec } from "child_process";
|
|
194
476
|
import { promisify } from "util";
|
|
@@ -373,7 +655,11 @@ var defaultConfig = {
|
|
|
373
655
|
controller: "templates/controller.cs.hbs",
|
|
374
656
|
component: "templates/component.tsx.hbs"
|
|
375
657
|
}
|
|
376
|
-
}
|
|
658
|
+
},
|
|
659
|
+
// Will be loaded from .smartstack/config.json if present
|
|
660
|
+
projectConfig: void 0,
|
|
661
|
+
// Default to 'core', will be overridden by projectConfig.dbContext if present
|
|
662
|
+
defaultDbContext: "core"
|
|
377
663
|
};
|
|
378
664
|
var cachedConfig = null;
|
|
379
665
|
function resolveProjectPath(configPath) {
|
|
@@ -408,12 +694,33 @@ async function getConfig() {
|
|
|
408
694
|
cachedConfig.smartstack.projectPath = resolveProjectPath(
|
|
409
695
|
cachedConfig.smartstack.projectPath
|
|
410
696
|
);
|
|
411
|
-
const
|
|
412
|
-
|
|
697
|
+
const smartstackConfigPath = path3.join(
|
|
698
|
+
cachedConfig.smartstack.projectPath,
|
|
699
|
+
".smartstack",
|
|
700
|
+
"config.json"
|
|
701
|
+
);
|
|
702
|
+
if (await fileExists(smartstackConfigPath)) {
|
|
413
703
|
try {
|
|
414
|
-
const
|
|
415
|
-
const
|
|
416
|
-
|
|
704
|
+
const projectConfigRaw = await readJson(smartstackConfigPath);
|
|
705
|
+
const projectConfig = ProjectConfigSchema.parse(projectConfigRaw);
|
|
706
|
+
cachedConfig.projectConfig = projectConfig;
|
|
707
|
+
cachedConfig.defaultDbContext = projectConfig.dbContext;
|
|
708
|
+
logger.info("Project config loaded from .smartstack/config.json", {
|
|
709
|
+
projectType: projectConfig.projectType,
|
|
710
|
+
dbContext: projectConfig.dbContext
|
|
711
|
+
});
|
|
712
|
+
} catch (error) {
|
|
713
|
+
logger.warn("Failed to load .smartstack/config.json, using defaults", { error });
|
|
714
|
+
}
|
|
715
|
+
} else {
|
|
716
|
+
logger.debug("No .smartstack/config.json found, assuming platform project (core context)");
|
|
717
|
+
}
|
|
718
|
+
const namespacesEmpty = !cachedConfig.conventions.namespaces.domain || !cachedConfig.conventions.namespaces.application || !cachedConfig.conventions.namespaces.infrastructure || !cachedConfig.conventions.namespaces.api;
|
|
719
|
+
if (namespacesEmpty && cachedConfig.smartstack.projectPath) {
|
|
720
|
+
try {
|
|
721
|
+
const csprojFiles = await findCsprojFiles(cachedConfig.smartstack.projectPath);
|
|
722
|
+
const detected = await detectNamespaces(csprojFiles);
|
|
723
|
+
if (detected) {
|
|
417
724
|
if (!cachedConfig.conventions.namespaces.domain) {
|
|
418
725
|
cachedConfig.conventions.namespaces.domain = detected.domain;
|
|
419
726
|
}
|
|
@@ -489,251 +796,6 @@ function mergeConfig(base, override) {
|
|
|
489
796
|
};
|
|
490
797
|
}
|
|
491
798
|
|
|
492
|
-
// src/types/index.ts
|
|
493
|
-
import { z } from "zod";
|
|
494
|
-
var SmartStackConfigSchema = z.object({
|
|
495
|
-
projectPath: z.string(),
|
|
496
|
-
apiUrl: z.string().url().optional(),
|
|
497
|
-
apiEnabled: z.boolean().default(true)
|
|
498
|
-
});
|
|
499
|
-
var ConventionsConfigSchema = z.object({
|
|
500
|
-
schemas: z.object({
|
|
501
|
-
platform: z.string().default("core"),
|
|
502
|
-
extensions: z.string().default("extensions")
|
|
503
|
-
}),
|
|
504
|
-
tablePrefixes: z.array(z.string()).default([
|
|
505
|
-
"auth_",
|
|
506
|
-
"nav_",
|
|
507
|
-
"usr_",
|
|
508
|
-
"ai_",
|
|
509
|
-
"cfg_",
|
|
510
|
-
"wkf_",
|
|
511
|
-
"support_",
|
|
512
|
-
"entra_",
|
|
513
|
-
"ref_",
|
|
514
|
-
"loc_",
|
|
515
|
-
"lic_"
|
|
516
|
-
]),
|
|
517
|
-
scopeTypes: z.array(z.string()).default(["Core", "Extension", "Partner", "Community"]),
|
|
518
|
-
migrationFormat: z.string().default("{context}_v{version}_{sequence}_{Description}"),
|
|
519
|
-
namespaces: z.object({
|
|
520
|
-
domain: z.string(),
|
|
521
|
-
application: z.string(),
|
|
522
|
-
infrastructure: z.string(),
|
|
523
|
-
api: z.string()
|
|
524
|
-
}),
|
|
525
|
-
servicePattern: z.object({
|
|
526
|
-
interface: z.string().default("I{Name}Service"),
|
|
527
|
-
implementation: z.string().default("{Name}Service")
|
|
528
|
-
})
|
|
529
|
-
});
|
|
530
|
-
var EfCoreContextSchema = z.object({
|
|
531
|
-
name: z.string(),
|
|
532
|
-
projectPath: z.string(),
|
|
533
|
-
migrationsFolder: z.string().default("Migrations")
|
|
534
|
-
});
|
|
535
|
-
var EfCoreConfigSchema = z.object({
|
|
536
|
-
contexts: z.array(EfCoreContextSchema),
|
|
537
|
-
validation: z.object({
|
|
538
|
-
checkModelSnapshot: z.boolean().default(true),
|
|
539
|
-
checkMigrationOrder: z.boolean().default(true),
|
|
540
|
-
requireBuildSuccess: z.boolean().default(true)
|
|
541
|
-
})
|
|
542
|
-
});
|
|
543
|
-
var ScaffoldingConfigSchema = z.object({
|
|
544
|
-
outputPath: z.string(),
|
|
545
|
-
templates: z.object({
|
|
546
|
-
service: z.string(),
|
|
547
|
-
entity: z.string(),
|
|
548
|
-
controller: z.string(),
|
|
549
|
-
component: z.string()
|
|
550
|
-
})
|
|
551
|
-
});
|
|
552
|
-
var ConfigSchema = z.object({
|
|
553
|
-
version: z.string(),
|
|
554
|
-
smartstack: SmartStackConfigSchema,
|
|
555
|
-
conventions: ConventionsConfigSchema,
|
|
556
|
-
efcore: EfCoreConfigSchema,
|
|
557
|
-
scaffolding: ScaffoldingConfigSchema
|
|
558
|
-
});
|
|
559
|
-
var ValidateConventionsInputSchema = z.object({
|
|
560
|
-
path: z.string().optional().describe("Project path to validate (default: SmartStack.app path)"),
|
|
561
|
-
checks: z.array(z.enum(["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "all"])).default(["all"]).describe("Types of checks to perform")
|
|
562
|
-
});
|
|
563
|
-
var CheckMigrationsInputSchema = z.object({
|
|
564
|
-
projectPath: z.string().optional().describe("EF Core project path"),
|
|
565
|
-
branch: z.string().optional().describe("Git branch to check (default: current)"),
|
|
566
|
-
compareBranch: z.string().optional().describe("Branch to compare against")
|
|
567
|
-
});
|
|
568
|
-
var EntityPropertySchema = z.object({
|
|
569
|
-
name: z.string().describe("Property name (PascalCase)"),
|
|
570
|
-
type: z.string().describe("C# type (string, int, Guid, DateTime, etc.)"),
|
|
571
|
-
required: z.boolean().optional().describe("If true, property is required"),
|
|
572
|
-
maxLength: z.number().optional().describe("Max length for string properties")
|
|
573
|
-
});
|
|
574
|
-
var ScaffoldExtensionInputSchema = z.object({
|
|
575
|
-
type: z.enum(["feature", "service", "entity", "controller", "component", "test", "dto", "validator", "repository"]).describe('Type of extension to scaffold. Use "feature" for full-stack generation.'),
|
|
576
|
-
name: z.string().describe('Name of the extension (e.g., "UserProfile", "Order")'),
|
|
577
|
-
options: z.object({
|
|
578
|
-
namespace: z.string().optional().describe("Custom namespace"),
|
|
579
|
-
baseEntity: z.string().optional().describe("Base entity to extend (for entity type)"),
|
|
580
|
-
methods: z.array(z.string()).optional().describe("Methods to generate (for service type)"),
|
|
581
|
-
outputPath: z.string().optional().describe("Custom output path"),
|
|
582
|
-
isSystemEntity: z.boolean().optional().describe("If true, creates a system entity without TenantId"),
|
|
583
|
-
tablePrefix: z.string().optional().describe('Domain prefix for table name (e.g., "auth_", "nav_", "cfg_")'),
|
|
584
|
-
schema: z.enum(["core", "extensions"]).optional().describe("Database schema (default: core)"),
|
|
585
|
-
dryRun: z.boolean().optional().describe("If true, preview generated code without writing files"),
|
|
586
|
-
skipService: z.boolean().optional().describe("For feature type: skip service generation"),
|
|
587
|
-
skipController: z.boolean().optional().describe("For feature type: skip controller generation"),
|
|
588
|
-
skipComponent: z.boolean().optional().describe("For feature type: skip React component generation"),
|
|
589
|
-
clientExtension: z.boolean().optional().describe("If true, use extensions schema for client-specific code"),
|
|
590
|
-
withTests: z.boolean().optional().describe("For feature type: also generate unit tests"),
|
|
591
|
-
withDtos: z.boolean().optional().describe("For feature type: generate DTOs (Create, Update, Response)"),
|
|
592
|
-
withValidation: z.boolean().optional().describe("For feature type: generate FluentValidation validators"),
|
|
593
|
-
withRepository: z.boolean().optional().describe("For feature type: generate repository pattern"),
|
|
594
|
-
entityProperties: z.array(EntityPropertySchema).optional().describe("Entity properties for DTO/Validator generation"),
|
|
595
|
-
navRoute: z.string().optional().describe('Navigation route path for controller (e.g., "platform.administration.users"). Required for controllers.'),
|
|
596
|
-
navRouteSuffix: z.string().optional().describe('Optional suffix for NavRoute (e.g., "dashboard" for sub-resources)')
|
|
597
|
-
}).optional()
|
|
598
|
-
});
|
|
599
|
-
var ApiDocsInputSchema = z.object({
|
|
600
|
-
endpoint: z.string().optional().describe('Filter by endpoint path (e.g., "/api/users")'),
|
|
601
|
-
format: z.enum(["markdown", "json", "openapi"]).default("markdown").describe("Output format"),
|
|
602
|
-
controller: z.string().optional().describe("Filter by controller name")
|
|
603
|
-
});
|
|
604
|
-
var SuggestMigrationInputSchema = z.object({
|
|
605
|
-
description: z.string().describe('Description of what the migration does (e.g., "Add User Profiles", "Create Orders Table")'),
|
|
606
|
-
context: z.enum(["core", "extensions"]).optional().describe("DbContext name (default: core)"),
|
|
607
|
-
version: z.string().optional().describe('Semver version (e.g., "1.0.0", "1.2.0"). If not provided, uses latest from existing migrations.')
|
|
608
|
-
});
|
|
609
|
-
var GeneratePermissionsInputSchema = z.object({
|
|
610
|
-
navRoute: z.string().optional().describe('NavRoute path (e.g., "platform.administration.entra"). If not provided, scans all controllers.'),
|
|
611
|
-
actions: z.array(z.string()).optional().describe("Custom actions to generate (default: read, create, update, delete)"),
|
|
612
|
-
includeStandardActions: z.boolean().default(true).describe("Include standard CRUD actions (read, create, update, delete)"),
|
|
613
|
-
generateMigration: z.boolean().default(true).describe("Generate EF Core migration to seed permissions in database"),
|
|
614
|
-
dryRun: z.boolean().default(false).describe("Preview without writing files or creating migration")
|
|
615
|
-
});
|
|
616
|
-
var TestTypeSchema = z.enum(["unit", "integration", "security", "e2e"]);
|
|
617
|
-
var TestTargetSchema = z.enum(["entity", "service", "controller", "validator", "repository", "all"]);
|
|
618
|
-
var ScaffoldTestsInputSchema = z.object({
|
|
619
|
-
target: TestTargetSchema.describe("Type of component to test"),
|
|
620
|
-
name: z.string().min(1).describe('Component name (PascalCase, e.g., "User", "Order")'),
|
|
621
|
-
testTypes: z.array(TestTypeSchema).default(["unit"]).describe("Types of tests to generate"),
|
|
622
|
-
options: z.object({
|
|
623
|
-
includeEdgeCases: z.boolean().default(true).describe("Include edge case tests"),
|
|
624
|
-
includeTenantIsolation: z.boolean().default(true).describe("Include tenant isolation tests"),
|
|
625
|
-
includeSoftDelete: z.boolean().default(true).describe("Include soft delete tests"),
|
|
626
|
-
includeAudit: z.boolean().default(true).describe("Include audit trail tests"),
|
|
627
|
-
includeValidation: z.boolean().default(true).describe("Include validation tests"),
|
|
628
|
-
includeAuthorization: z.boolean().default(false).describe("Include authorization tests"),
|
|
629
|
-
includePerformance: z.boolean().default(false).describe("Include performance tests"),
|
|
630
|
-
entityProperties: z.array(EntityPropertySchema).optional().describe("Entity properties for test generation"),
|
|
631
|
-
isSystemEntity: z.boolean().default(false).describe("If true, entity has no TenantId"),
|
|
632
|
-
dryRun: z.boolean().default(false).describe("Preview without writing files")
|
|
633
|
-
}).optional()
|
|
634
|
-
});
|
|
635
|
-
var AnalyzeTestCoverageInputSchema = z.object({
|
|
636
|
-
path: z.string().optional().describe("Project path to analyze"),
|
|
637
|
-
scope: z.enum(["entity", "service", "controller", "all"]).default("all").describe("Scope of analysis"),
|
|
638
|
-
outputFormat: z.enum(["summary", "detailed", "json"]).default("summary").describe("Output format"),
|
|
639
|
-
includeRecommendations: z.boolean().default(true).describe("Include recommendations for missing tests")
|
|
640
|
-
});
|
|
641
|
-
var ValidateTestConventionsInputSchema = z.object({
|
|
642
|
-
path: z.string().optional().describe("Project path to validate"),
|
|
643
|
-
checks: z.array(z.enum(["naming", "structure", "patterns", "assertions", "mocking", "all"])).default(["all"]).describe("Types of convention checks to perform"),
|
|
644
|
-
autoFix: z.boolean().default(false).describe("Automatically fix minor issues")
|
|
645
|
-
});
|
|
646
|
-
var SuggestTestScenariosInputSchema = z.object({
|
|
647
|
-
target: z.enum(["entity", "service", "controller", "file"]).describe("Type of target to analyze"),
|
|
648
|
-
name: z.string().min(1).describe("Component name or file path"),
|
|
649
|
-
depth: z.enum(["basic", "comprehensive", "security-focused"]).default("comprehensive").describe("Depth of analysis")
|
|
650
|
-
});
|
|
651
|
-
var SecurityCheckSchema = z.enum([
|
|
652
|
-
"hardcoded-secrets",
|
|
653
|
-
"sql-injection",
|
|
654
|
-
"tenant-isolation",
|
|
655
|
-
"authorization",
|
|
656
|
-
"dangerous-functions",
|
|
657
|
-
"input-validation",
|
|
658
|
-
"xss",
|
|
659
|
-
"csrf",
|
|
660
|
-
"logging-sensitive",
|
|
661
|
-
"all"
|
|
662
|
-
]);
|
|
663
|
-
var ValidateSecurityInputSchema = z.object({
|
|
664
|
-
path: z.string().optional().describe("Project path to validate (default: SmartStack.app path)"),
|
|
665
|
-
checks: z.array(SecurityCheckSchema).default(["all"]).describe("Security checks to run"),
|
|
666
|
-
severity: z.enum(["blocking", "all"]).optional().describe("Filter results by severity")
|
|
667
|
-
});
|
|
668
|
-
var QualityMetricSchema = z.enum([
|
|
669
|
-
"cognitive-complexity",
|
|
670
|
-
"cyclomatic-complexity",
|
|
671
|
-
"function-size",
|
|
672
|
-
"nesting-depth",
|
|
673
|
-
"parameter-count",
|
|
674
|
-
"code-duplication",
|
|
675
|
-
"file-size",
|
|
676
|
-
"all"
|
|
677
|
-
]);
|
|
678
|
-
var AnalyzeCodeQualityInputSchema = z.object({
|
|
679
|
-
path: z.string().optional().describe("Project path to analyze (default: SmartStack.app path)"),
|
|
680
|
-
metrics: z.array(QualityMetricSchema).default(["all"]).describe("Metrics to analyze"),
|
|
681
|
-
threshold: z.enum(["strict", "normal", "lenient"]).default("normal").describe("Threshold level for violations"),
|
|
682
|
-
scope: z.enum(["changed", "all"]).default("all").describe("Analyze only changed files or all")
|
|
683
|
-
});
|
|
684
|
-
var ScaffoldApiClientInputSchema = z.object({
|
|
685
|
-
path: z.string().optional().describe("Path to SmartStack project root (defaults to configured project path)"),
|
|
686
|
-
navRoute: z.string().min(1).describe('NavRoute path (e.g., "platform.administration.users")'),
|
|
687
|
-
name: z.string().min(1).describe('Entity name in PascalCase (e.g., "User", "Order")'),
|
|
688
|
-
methods: z.array(z.enum(["getAll", "getById", "create", "update", "delete", "search", "export"])).default(["getAll", "getById", "create", "update", "delete"]).describe("API methods to generate"),
|
|
689
|
-
options: z.object({
|
|
690
|
-
outputPath: z.string().optional().describe("Custom output path for generated files"),
|
|
691
|
-
includeTypes: z.boolean().default(true).describe("Generate TypeScript types"),
|
|
692
|
-
includeHook: z.boolean().default(true).describe("Generate React Query hook"),
|
|
693
|
-
dryRun: z.boolean().default(false).describe("Preview without writing files")
|
|
694
|
-
}).optional()
|
|
695
|
-
});
|
|
696
|
-
var ScaffoldRoutesInputSchema = z.object({
|
|
697
|
-
path: z.string().optional().describe("Path to SmartStack project root (defaults to configured project path)"),
|
|
698
|
-
source: z.enum(["controllers", "navigation", "manual"]).default("controllers").describe("Source for route discovery: controllers (scan NavRoute attributes), navigation (from DB), manual (from config)"),
|
|
699
|
-
scope: z.enum(["all", "platform", "business", "extensions"]).default("all").describe("Scope of routes to generate"),
|
|
700
|
-
options: z.object({
|
|
701
|
-
outputPath: z.string().optional().describe("Custom output path"),
|
|
702
|
-
includeLayouts: z.boolean().default(true).describe("Generate layout components"),
|
|
703
|
-
includeGuards: z.boolean().default(true).describe("Include route guards for permissions"),
|
|
704
|
-
generateRegistry: z.boolean().default(true).describe("Generate navRoutes.generated.ts"),
|
|
705
|
-
dryRun: z.boolean().default(false).describe("Preview without writing files")
|
|
706
|
-
}).optional()
|
|
707
|
-
});
|
|
708
|
-
var ValidateFrontendRoutesInputSchema = z.object({
|
|
709
|
-
path: z.string().optional().describe("Path to SmartStack project root (defaults to configured project path)"),
|
|
710
|
-
scope: z.enum(["api-clients", "routes", "registry", "all"]).default("all").describe("Scope of validation"),
|
|
711
|
-
options: z.object({
|
|
712
|
-
fix: z.boolean().default(false).describe("Auto-fix minor issues"),
|
|
713
|
-
strict: z.boolean().default(false).describe("Fail on warnings")
|
|
714
|
-
}).optional()
|
|
715
|
-
});
|
|
716
|
-
var SlotDefinitionSchema = z.object({
|
|
717
|
-
name: z.string().describe('Slot name (e.g., "users.form.fields.after")'),
|
|
718
|
-
location: z.string().describe("Where the slot is located in the page"),
|
|
719
|
-
contextProps: z.array(z.string()).optional().describe("Props passed to slot content")
|
|
720
|
-
});
|
|
721
|
-
var ScaffoldFrontendExtensionInputSchema = z.object({
|
|
722
|
-
type: z.enum(["infrastructure", "page-slots", "extension-example", "all"]).describe("Type of extension to scaffold: infrastructure (types, contexts), page-slots (add slots to a page), extension-example (client usage example), all"),
|
|
723
|
-
target: z.string().optional().describe('Target page or component (e.g., "UsersPage", "UserDetailPage")'),
|
|
724
|
-
options: z.object({
|
|
725
|
-
slots: z.array(SlotDefinitionSchema).optional().describe("Slot definitions to add"),
|
|
726
|
-
outputPath: z.string().optional().describe("Custom output path"),
|
|
727
|
-
dryRun: z.boolean().default(false).describe("Preview without writing files"),
|
|
728
|
-
overwrite: z.boolean().default(false).describe("Overwrite existing files")
|
|
729
|
-
}).optional()
|
|
730
|
-
});
|
|
731
|
-
var AnalyzeExtensionPointsInputSchema = z.object({
|
|
732
|
-
path: z.string().optional().describe("Path to SmartStack web project"),
|
|
733
|
-
target: z.enum(["pages", "components", "forms", "tables", "all"]).default("all").describe("Type of components to analyze"),
|
|
734
|
-
filter: z.string().optional().describe('Filter by file name pattern (e.g., "User*")')
|
|
735
|
-
});
|
|
736
|
-
|
|
737
799
|
// src/lib/detector.ts
|
|
738
800
|
import path5 from "path";
|
|
739
801
|
|
|
@@ -778,10 +840,19 @@ async function getCurrentBranch(cwd) {
|
|
|
778
840
|
return null;
|
|
779
841
|
}
|
|
780
842
|
}
|
|
781
|
-
async function
|
|
843
|
+
async function getChangedFiles(cwd) {
|
|
782
844
|
try {
|
|
783
|
-
await git(
|
|
784
|
-
return
|
|
845
|
+
const status = await git("status --porcelain", cwd);
|
|
846
|
+
if (!status) return [];
|
|
847
|
+
return status.split("\n").map((line) => line.substring(3).trim()).filter(Boolean);
|
|
848
|
+
} catch {
|
|
849
|
+
return [];
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
async function branchExists(branch, cwd) {
|
|
853
|
+
try {
|
|
854
|
+
await git(`rev-parse --verify ${branch}`, cwd);
|
|
855
|
+
return true;
|
|
785
856
|
} catch {
|
|
786
857
|
return false;
|
|
787
858
|
}
|
|
@@ -801,6 +872,15 @@ async function getDiff(fromBranch, toBranch, filePath, cwd) {
|
|
|
801
872
|
return "";
|
|
802
873
|
}
|
|
803
874
|
}
|
|
875
|
+
async function getStagedFiles(cwd) {
|
|
876
|
+
try {
|
|
877
|
+
const status = await git("diff --cached --name-only", cwd);
|
|
878
|
+
if (!status) return [];
|
|
879
|
+
return status.split("\n").filter(Boolean);
|
|
880
|
+
} catch {
|
|
881
|
+
return [];
|
|
882
|
+
}
|
|
883
|
+
}
|
|
804
884
|
|
|
805
885
|
// src/lib/detector.ts
|
|
806
886
|
import { glob as glob2 } from "glob";
|
|
@@ -947,7 +1027,7 @@ var validateConventionsTool = {
|
|
|
947
1027
|
type: "array",
|
|
948
1028
|
items: {
|
|
949
1029
|
type: "string",
|
|
950
|
-
enum: ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "all"]
|
|
1030
|
+
enum: ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "hierarchies", "protected-actions", "all"]
|
|
951
1031
|
},
|
|
952
1032
|
description: "Types of checks to perform",
|
|
953
1033
|
default: ["all"]
|
|
@@ -958,7 +1038,7 @@ var validateConventionsTool = {
|
|
|
958
1038
|
async function handleValidateConventions(args, config) {
|
|
959
1039
|
const input = ValidateConventionsInputSchema.parse(args);
|
|
960
1040
|
const projectPath = input.path || config.smartstack.projectPath;
|
|
961
|
-
const checks = input.checks.includes("all") ? ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs"] : input.checks;
|
|
1041
|
+
const checks = input.checks.includes("all") ? ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "hierarchies", "protected-actions"] : input.checks;
|
|
962
1042
|
logger.info("Validating conventions", { projectPath, checks });
|
|
963
1043
|
const result = {
|
|
964
1044
|
valid: true,
|
|
@@ -994,6 +1074,12 @@ async function handleValidateConventions(args, config) {
|
|
|
994
1074
|
if (checks.includes("tabs")) {
|
|
995
1075
|
await validateTabs(structure, config, result);
|
|
996
1076
|
}
|
|
1077
|
+
if (checks.includes("hierarchies")) {
|
|
1078
|
+
await validateHierarchies(structure, config, result);
|
|
1079
|
+
}
|
|
1080
|
+
if (checks.includes("protected-actions")) {
|
|
1081
|
+
await validateProtectedActions(structure, config, result);
|
|
1082
|
+
}
|
|
997
1083
|
result.valid = result.errors.length === 0;
|
|
998
1084
|
result.summary = generateSummary(result, checks);
|
|
999
1085
|
return formatResult(result);
|
|
@@ -1511,6 +1597,7 @@ async function validateTabs(structure, _config, result) {
|
|
|
1511
1597
|
const pageFiles = await findFiles("**/pages/**/*.tsx", { cwd: structure.web });
|
|
1512
1598
|
let tabPagesCount = 0;
|
|
1513
1599
|
let hookUsageCount = 0;
|
|
1600
|
+
let lazyLoadingCount = 0;
|
|
1514
1601
|
for (const file of pageFiles) {
|
|
1515
1602
|
const content = await readText(file);
|
|
1516
1603
|
const fileName = path6.basename(file);
|
|
@@ -1540,14 +1627,326 @@ async function validateTabs(structure, _config, result) {
|
|
|
1540
1627
|
suggestion: "Use useTabNavigation hook to sync tab state with URL: const { activeTab, setActiveTab } = useTabNavigation(defaultTab, VALID_TABS)"
|
|
1541
1628
|
});
|
|
1542
1629
|
}
|
|
1630
|
+
const lazyLoadingPatterns = detectTabLazyLoading(content);
|
|
1631
|
+
if (lazyLoadingPatterns.hasLazyLoading) {
|
|
1632
|
+
lazyLoadingCount++;
|
|
1633
|
+
} else if (lazyLoadingPatterns.hasDataFetching) {
|
|
1634
|
+
result.warnings.push({
|
|
1635
|
+
type: "warning",
|
|
1636
|
+
category: "tabs",
|
|
1637
|
+
message: `Page "${fileName}" has tabs with data fetching but no lazy loading pattern detected`,
|
|
1638
|
+
file: path6.relative(structure.root, file),
|
|
1639
|
+
suggestion: 'Use lazy loading for tab data: useQuery([...], fetchFn, { enabled: activeTab === "tabName" }) or React.lazy() for tab components'
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1543
1642
|
}
|
|
1544
1643
|
}
|
|
1545
1644
|
if (tabPagesCount > 0) {
|
|
1645
|
+
const summaryParts = [
|
|
1646
|
+
`${hookUsageCount}/${tabPagesCount} use useTabNavigation hook`
|
|
1647
|
+
];
|
|
1648
|
+
if (lazyLoadingCount > 0 || tabPagesCount > hookUsageCount) {
|
|
1649
|
+
summaryParts.push(`${lazyLoadingCount}/${tabPagesCount} use lazy loading`);
|
|
1650
|
+
}
|
|
1546
1651
|
result.warnings.push({
|
|
1547
1652
|
type: "warning",
|
|
1548
1653
|
category: "tabs",
|
|
1549
|
-
message: `Tab summary: ${
|
|
1654
|
+
message: `Tab summary: ${summaryParts.join(", ")}`
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
function detectTabLazyLoading(content) {
|
|
1659
|
+
const patterns = [];
|
|
1660
|
+
let hasLazyLoading = false;
|
|
1661
|
+
let hasDataFetching = false;
|
|
1662
|
+
const hasUseQuery = /useQuery\s*\(/.test(content);
|
|
1663
|
+
const hasEnabledTabCondition = /enabled\s*:\s*(?:activeTab|tab|currentTab)\s*===\s*['"][^'"]+['"]/.test(content);
|
|
1664
|
+
if (hasUseQuery && hasEnabledTabCondition) {
|
|
1665
|
+
hasLazyLoading = true;
|
|
1666
|
+
patterns.push("useQuery with enabled condition");
|
|
1667
|
+
}
|
|
1668
|
+
const reactLazyPattern = /(?:React\.)?lazy\s*\(\s*\(\)\s*=>\s*import\s*\(/;
|
|
1669
|
+
if (reactLazyPattern.test(content)) {
|
|
1670
|
+
hasLazyLoading = true;
|
|
1671
|
+
patterns.push("React.lazy() imports");
|
|
1672
|
+
}
|
|
1673
|
+
const suspensePattern = /<Suspense\s+[^>]*fallback\s*=/;
|
|
1674
|
+
if (suspensePattern.test(content)) {
|
|
1675
|
+
hasLazyLoading = true;
|
|
1676
|
+
patterns.push("Suspense with fallback");
|
|
1677
|
+
}
|
|
1678
|
+
const conditionalRenderPattern = /(?:activeTab|tab|currentTab)\s*===\s*['"][^'"]+['"]\s*(?:&&|\?)\s*<[A-Z]/;
|
|
1679
|
+
if (conditionalRenderPattern.test(content)) {
|
|
1680
|
+
hasLazyLoading = true;
|
|
1681
|
+
patterns.push("Conditional tab rendering");
|
|
1682
|
+
}
|
|
1683
|
+
const useSWRConditionalPattern = /useSWR\s*\(\s*(?:activeTab|tab|currentTab)\s*===\s*['"][^'"]+['"]\s*\?/;
|
|
1684
|
+
if (useSWRConditionalPattern.test(content)) {
|
|
1685
|
+
hasLazyLoading = true;
|
|
1686
|
+
patterns.push("useSWR with conditional key");
|
|
1687
|
+
}
|
|
1688
|
+
const dataFetchingPatterns = [
|
|
1689
|
+
/useQuery\s*\(/,
|
|
1690
|
+
/useSWR\s*\(/,
|
|
1691
|
+
/useMutation\s*\(/,
|
|
1692
|
+
/fetch\s*\(/,
|
|
1693
|
+
/axios\./,
|
|
1694
|
+
/useEffect\s*\([^)]*\{[^}]*(?:fetch|axios|api\.)/s
|
|
1695
|
+
];
|
|
1696
|
+
for (const pattern of dataFetchingPatterns) {
|
|
1697
|
+
if (pattern.test(content)) {
|
|
1698
|
+
hasDataFetching = true;
|
|
1699
|
+
break;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
return {
|
|
1703
|
+
hasLazyLoading,
|
|
1704
|
+
hasDataFetching,
|
|
1705
|
+
patterns
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
async function validateHierarchies(structure, _config, result) {
|
|
1709
|
+
if (!structure.domain) {
|
|
1710
|
+
result.warnings.push({
|
|
1711
|
+
type: "warning",
|
|
1712
|
+
category: "hierarchies",
|
|
1713
|
+
message: "Domain project not found, skipping hierarchy validation"
|
|
1714
|
+
});
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
const entityFiles = await findFiles("**/*.cs", { cwd: structure.domain });
|
|
1718
|
+
const hierarchies = [];
|
|
1719
|
+
const entityNames = /* @__PURE__ */ new Set();
|
|
1720
|
+
for (const file of entityFiles) {
|
|
1721
|
+
const content = await readText(file);
|
|
1722
|
+
const fileName = path6.basename(file, ".cs");
|
|
1723
|
+
if (fileName.endsWith("Dto") || fileName.endsWith("Command") || fileName.endsWith("Query") || fileName.endsWith("Handler") || fileName.endsWith("Validator") || fileName.endsWith("Exception") || fileName.startsWith("I")) {
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
const classMatch = content.match(
|
|
1727
|
+
/public\s+(?:class|record)\s+(\w+)(?:\s*:\s*([^{]+))?/
|
|
1728
|
+
);
|
|
1729
|
+
if (!classMatch) continue;
|
|
1730
|
+
const entityName = classMatch[1];
|
|
1731
|
+
const inheritance = classMatch[2]?.trim() || "";
|
|
1732
|
+
const hasBaseEntity = inheritance.includes("BaseEntity");
|
|
1733
|
+
const hasSystemEntity = inheritance.includes("SystemEntity");
|
|
1734
|
+
if (!hasBaseEntity && !hasSystemEntity) continue;
|
|
1735
|
+
entityNames.add(entityName);
|
|
1736
|
+
const hasITenantEntity = inheritance.includes("ITenantEntity");
|
|
1737
|
+
const selfRefMatch = content.match(
|
|
1738
|
+
new RegExp(
|
|
1739
|
+
`public\\s+(?:Guid\\??|${entityName}\\??)\\s+(Parent(?:Id)?|Parent${entityName}(?:Id)?)\\s*\\{`,
|
|
1740
|
+
"i"
|
|
1741
|
+
)
|
|
1742
|
+
);
|
|
1743
|
+
const childCollections = [];
|
|
1744
|
+
const collectionMatches = content.matchAll(
|
|
1745
|
+
/public\s+(?:virtual\s+)?ICollection<(\w+)>\s+(\w+)\s*\{/g
|
|
1746
|
+
);
|
|
1747
|
+
for (const match of collectionMatches) {
|
|
1748
|
+
childCollections.push(match[1]);
|
|
1749
|
+
}
|
|
1750
|
+
const fkMatches = content.matchAll(/public\s+Guid\s+(\w+Id)\s*\{[^}]+\}/g);
|
|
1751
|
+
let parentEntity;
|
|
1752
|
+
for (const match of fkMatches) {
|
|
1753
|
+
const propName = match[1];
|
|
1754
|
+
if (propName.toLowerCase().includes("parent")) continue;
|
|
1755
|
+
if (propName === "TenantId") continue;
|
|
1756
|
+
const refEntity = propName.replace(/Id$/, "");
|
|
1757
|
+
if (refEntity !== entityName) {
|
|
1758
|
+
parentEntity = refEntity;
|
|
1759
|
+
break;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
if (selfRefMatch || childCollections.length > 0 || parentEntity) {
|
|
1763
|
+
hierarchies.push({
|
|
1764
|
+
name: entityName,
|
|
1765
|
+
file: path6.relative(structure.root, file),
|
|
1766
|
+
isSelfReferencing: !!selfRefMatch,
|
|
1767
|
+
selfRefField: selfRefMatch ? selfRefMatch[1] : void 0,
|
|
1768
|
+
parentEntity,
|
|
1769
|
+
isTenantAware: hasITenantEntity,
|
|
1770
|
+
childCollections,
|
|
1771
|
+
depth: 0
|
|
1772
|
+
// Will be calculated later
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
let selfRefCount = 0;
|
|
1777
|
+
let parentChildCount = 0;
|
|
1778
|
+
let deepHierarchyCount = 0;
|
|
1779
|
+
for (const h of hierarchies) {
|
|
1780
|
+
if (h.isSelfReferencing) {
|
|
1781
|
+
selfRefCount++;
|
|
1782
|
+
result.warnings.push({
|
|
1783
|
+
type: "warning",
|
|
1784
|
+
category: "hierarchies",
|
|
1785
|
+
message: `Entity "${h.name}" is self-referencing via "${h.selfRefField}"`,
|
|
1786
|
+
file: h.file,
|
|
1787
|
+
suggestion: `Consider using TVF with CTE for efficient hierarchy traversal. Ensure index on ${h.selfRefField}.`
|
|
1788
|
+
});
|
|
1789
|
+
if (h.name.toLowerCase().includes("group") || h.name.toLowerCase().includes("permission") || h.name.toLowerCase().includes("role")) {
|
|
1790
|
+
result.warnings.push({
|
|
1791
|
+
type: "warning",
|
|
1792
|
+
category: "hierarchies",
|
|
1793
|
+
message: `Permission/Group entity "${h.name}" detected with self-reference`,
|
|
1794
|
+
file: h.file,
|
|
1795
|
+
suggestion: `CRITICAL: Use fn_Get${h.name}Hierarchy TVF for recursive permission resolution. Run analyze_hierarchy_patterns for SQL template.`
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
if (h.parentEntity) {
|
|
1800
|
+
parentChildCount++;
|
|
1801
|
+
if (!entityNames.has(h.parentEntity)) {
|
|
1802
|
+
result.errors.push({
|
|
1803
|
+
type: "error",
|
|
1804
|
+
category: "hierarchies",
|
|
1805
|
+
message: `Entity "${h.name}" references non-existent parent "${h.parentEntity}"`,
|
|
1806
|
+
file: h.file,
|
|
1807
|
+
suggestion: `Create the ${h.parentEntity} entity or fix the foreign key reference.`
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
if (h.parentEntity && entityNames.has(h.parentEntity)) {
|
|
1812
|
+
const parentInfo = hierarchies.find((x) => x.name === h.parentEntity);
|
|
1813
|
+
if (parentInfo && parentInfo.isTenantAware && !h.isTenantAware) {
|
|
1814
|
+
result.errors.push({
|
|
1815
|
+
type: "error",
|
|
1816
|
+
category: "hierarchies",
|
|
1817
|
+
message: `Entity "${h.name}" has tenant-aware parent "${h.parentEntity}" but is not tenant-aware itself`,
|
|
1818
|
+
file: h.file,
|
|
1819
|
+
suggestion: `Add ITenantEntity interface to ${h.name} for consistent tenant isolation.`
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
if (h.childCollections.length > 0) {
|
|
1824
|
+
for (const child of h.childCollections) {
|
|
1825
|
+
const childInfo = hierarchies.find((x) => x.name === child);
|
|
1826
|
+
if (childInfo && childInfo.childCollections.length > 0) {
|
|
1827
|
+
deepHierarchyCount++;
|
|
1828
|
+
result.warnings.push({
|
|
1829
|
+
type: "warning",
|
|
1830
|
+
category: "hierarchies",
|
|
1831
|
+
message: `Deep hierarchy detected: ${h.name} \u2192 ${child} \u2192 ...`,
|
|
1832
|
+
file: h.file,
|
|
1833
|
+
suggestion: `Consider TVF for efficient traversal. Run analyze_hierarchy_patterns for recommendations.`
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
if (hierarchies.length > 0) {
|
|
1840
|
+
result.warnings.push({
|
|
1841
|
+
type: "warning",
|
|
1842
|
+
category: "hierarchies",
|
|
1843
|
+
message: `Hierarchy summary: ${selfRefCount} self-referencing, ${parentChildCount} parent-child, ${deepHierarchyCount} deep hierarchies detected`
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
async function validateProtectedActions(structure, _config, result) {
|
|
1848
|
+
if (!structure.web) {
|
|
1849
|
+
result.warnings.push({
|
|
1850
|
+
type: "warning",
|
|
1851
|
+
category: "protected-actions",
|
|
1852
|
+
message: "Web project not found, skipping protected actions validation"
|
|
1853
|
+
});
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
const pageFiles = await findFiles("**/pages/**/*.tsx", { cwd: structure.web });
|
|
1857
|
+
const componentFiles = await findFiles("**/components/**/*.tsx", { cwd: structure.web });
|
|
1858
|
+
const allFiles = [...pageFiles, ...componentFiles];
|
|
1859
|
+
const actionPatterns = {
|
|
1860
|
+
create: ["create", "add", "new", "cr\xE9er", "ajouter", "nouveau"],
|
|
1861
|
+
update: ["edit", "update", "modify", "modifier", "\xE9diter", "save", "enregistrer"],
|
|
1862
|
+
delete: ["delete", "remove", "supprimer", "retirer"],
|
|
1863
|
+
execute: ["execute", "run", "sync", "ex\xE9cuter", "lancer", "synchroniser"]
|
|
1864
|
+
};
|
|
1865
|
+
let protectedButtonCount = 0;
|
|
1866
|
+
let hasPermissionCount = 0;
|
|
1867
|
+
let unprotectedCount = 0;
|
|
1868
|
+
const unprotectedButtons = [];
|
|
1869
|
+
for (const file of allFiles) {
|
|
1870
|
+
const content = await readText(file);
|
|
1871
|
+
const lines = content.split("\n");
|
|
1872
|
+
const usesProtectedButton = content.includes("<ProtectedButton");
|
|
1873
|
+
if (usesProtectedButton) {
|
|
1874
|
+
protectedButtonCount++;
|
|
1875
|
+
continue;
|
|
1876
|
+
}
|
|
1877
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1878
|
+
const line = lines[i];
|
|
1879
|
+
const trimmed = line.trim();
|
|
1880
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
const buttonMatch = /<(?:button|Button)[^>]*>([^<]*)<\/|onClick\s*=\s*\{[^}]*handle(Create|Delete|Edit|Update|Remove|Save|Add|Sync|Execute)/i.exec(line);
|
|
1884
|
+
if (buttonMatch) {
|
|
1885
|
+
const buttonContent = buttonMatch[1]?.toLowerCase() || "";
|
|
1886
|
+
const handlerAction = buttonMatch[2]?.toLowerCase() || "";
|
|
1887
|
+
let detectedAction = null;
|
|
1888
|
+
for (const [action, keywords] of Object.entries(actionPatterns)) {
|
|
1889
|
+
if (keywords.some((kw) => buttonContent.includes(kw) || handlerAction.includes(kw))) {
|
|
1890
|
+
detectedAction = action;
|
|
1891
|
+
break;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
if (detectedAction) {
|
|
1895
|
+
const startLine = Math.max(0, i - 5);
|
|
1896
|
+
const endLine = Math.min(lines.length - 1, i + 5);
|
|
1897
|
+
const context = lines.slice(startLine, endLine + 1).join("\n");
|
|
1898
|
+
const hasPermissionCheck = context.includes("hasPermission") || context.includes("canCreate") || context.includes("canEdit") || context.includes("canUpdate") || context.includes("canDelete") || context.includes("canExecute") || context.includes("disabled={!can");
|
|
1899
|
+
if (!hasPermissionCheck) {
|
|
1900
|
+
unprotectedCount++;
|
|
1901
|
+
unprotectedButtons.push({
|
|
1902
|
+
file: path6.relative(structure.root, file),
|
|
1903
|
+
line: i + 1,
|
|
1904
|
+
action: detectedAction,
|
|
1905
|
+
code: line.trim().substring(0, 80)
|
|
1906
|
+
});
|
|
1907
|
+
} else {
|
|
1908
|
+
hasPermissionCount++;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
const reportedButtons = unprotectedButtons.slice(0, 10);
|
|
1915
|
+
for (const btn of reportedButtons) {
|
|
1916
|
+
result.warnings.push({
|
|
1917
|
+
type: "warning",
|
|
1918
|
+
category: "protected-actions",
|
|
1919
|
+
message: `Button with "${btn.action}" action may be missing permission check`,
|
|
1920
|
+
file: btn.file,
|
|
1921
|
+
line: btn.line,
|
|
1922
|
+
suggestion: `Use <ProtectedButton action="${btn.action}" navRoute="..."> or add hasPermission check`
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
if (unprotectedButtons.length > 10) {
|
|
1926
|
+
result.warnings.push({
|
|
1927
|
+
type: "warning",
|
|
1928
|
+
category: "protected-actions",
|
|
1929
|
+
message: `... and ${unprotectedButtons.length - 10} more unprotected buttons`
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
const totalActionButtons = protectedButtonCount + hasPermissionCount + unprotectedCount;
|
|
1933
|
+
if (totalActionButtons > 0) {
|
|
1934
|
+
const protectedPercentage = Math.round(
|
|
1935
|
+
(protectedButtonCount + hasPermissionCount) / totalActionButtons * 100
|
|
1936
|
+
);
|
|
1937
|
+
result.warnings.push({
|
|
1938
|
+
type: "warning",
|
|
1939
|
+
category: "protected-actions",
|
|
1940
|
+
message: `Protected actions summary: ${protectedButtonCount} use ProtectedButton, ${hasPermissionCount} use hasPermission, ${unprotectedCount} potentially unprotected (${protectedPercentage}% protected)`
|
|
1550
1941
|
});
|
|
1942
|
+
if (protectedPercentage < 80 && totalActionButtons > 5) {
|
|
1943
|
+
result.errors.push({
|
|
1944
|
+
type: "error",
|
|
1945
|
+
category: "protected-actions",
|
|
1946
|
+
message: `Less than 80% of action buttons are protected (${protectedPercentage}%)`,
|
|
1947
|
+
suggestion: "Migrate action buttons to use <ProtectedButton> component for consistent permission checking"
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1551
1950
|
}
|
|
1552
1951
|
}
|
|
1553
1952
|
function generateSummary(result, checks) {
|
|
@@ -2011,6 +2410,15 @@ var scaffoldExtensionTool = {
|
|
|
2011
2410
|
navRouteSuffix: {
|
|
2012
2411
|
type: "string",
|
|
2013
2412
|
description: 'Optional suffix for NavRoute (e.g., "dashboard" for sub-resources)'
|
|
2413
|
+
},
|
|
2414
|
+
withHierarchyFunction: {
|
|
2415
|
+
type: "boolean",
|
|
2416
|
+
description: "For entity type with self-reference (ParentId): generate TVF SQL script for hierarchy traversal"
|
|
2417
|
+
},
|
|
2418
|
+
hierarchyDirection: {
|
|
2419
|
+
type: "string",
|
|
2420
|
+
enum: ["ancestors", "descendants", "both"],
|
|
2421
|
+
description: "Direction for hierarchy traversal function (default: both)"
|
|
2014
2422
|
}
|
|
2015
2423
|
}
|
|
2016
2424
|
}
|
|
@@ -2155,10 +2563,10 @@ async function scaffoldFeature(name, options, structure, config, result, dryRun
|
|
|
2155
2563
|
result.instructions.push("---");
|
|
2156
2564
|
result.instructions.push(`## Summary: Generated ${generated.join(" + ")}`);
|
|
2157
2565
|
result.instructions.push("");
|
|
2158
|
-
const schema = options?.schema || (isClientExtension ? "extensions" :
|
|
2566
|
+
const schema = options?.schema || (isClientExtension ? "extensions" : config.defaultDbContext);
|
|
2159
2567
|
const dbContextName = schema === "extensions" ? "ExtensionsDbContext" : "CoreDbContext";
|
|
2160
2568
|
const dbContextInterface = schema === "extensions" ? "IExtensionsDbContext" : "ICoreDbContext";
|
|
2161
|
-
const migrationPrefix = schema
|
|
2569
|
+
const migrationPrefix = schema;
|
|
2162
2570
|
result.instructions.push("### Next Steps:");
|
|
2163
2571
|
result.instructions.push(`1. Add DbSet to ${dbContextInterface} and ${dbContextName}: \`public DbSet<${name}> ${name}s => Set<${name}>();\``);
|
|
2164
2572
|
if (withRepository) {
|
|
@@ -2500,7 +2908,7 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
|
|
|
2500
2908
|
result.files.push({ path: configFilePath, content: configContent, type: "created" });
|
|
2501
2909
|
const dbContextName = schema === "extensions" ? "ExtensionsDbContext" : "CoreDbContext";
|
|
2502
2910
|
const dbContextInterface = schema === "extensions" ? "IExtensionsDbContext" : "ICoreDbContext";
|
|
2503
|
-
const migrationPrefix = schema
|
|
2911
|
+
const migrationPrefix = schema;
|
|
2504
2912
|
result.instructions.push(`Add DbSet to ${dbContextInterface}:`);
|
|
2505
2913
|
result.instructions.push(`public DbSet<${name}> ${name}s => Set<${name}>();`);
|
|
2506
2914
|
result.instructions.push("");
|
|
@@ -2520,6 +2928,11 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
|
|
|
2520
2928
|
result.instructions.push("### Seed Data");
|
|
2521
2929
|
await scaffoldSeedData(name, options, structure, config, result, dryRun);
|
|
2522
2930
|
}
|
|
2931
|
+
if (options?.withHierarchyFunction) {
|
|
2932
|
+
result.instructions.push("");
|
|
2933
|
+
result.instructions.push("### Hierarchy Function (TVF)");
|
|
2934
|
+
await scaffoldHierarchyFunction(name, options, structure, config, result, dryRun);
|
|
2935
|
+
}
|
|
2523
2936
|
}
|
|
2524
2937
|
async function scaffoldSeedData(name, options, structure, config, result, dryRun = false) {
|
|
2525
2938
|
const tablePrefix = options?.tablePrefix || "ref_";
|
|
@@ -2629,6 +3042,180 @@ public static class {{name}}SeedData
|
|
|
2629
3042
|
result.instructions.push("3. Implement GetSeedData() returning object[]");
|
|
2630
3043
|
result.instructions.push("4. Reference other SeedData IDs for foreign keys");
|
|
2631
3044
|
}
|
|
3045
|
+
async function scaffoldHierarchyFunction(name, options, structure, config, result, dryRun = false) {
|
|
3046
|
+
const tablePrefix = options?.tablePrefix || "ref_";
|
|
3047
|
+
const schema = options?.schema || config.conventions.schemas.platform;
|
|
3048
|
+
const tableName = `${tablePrefix}${name}s`;
|
|
3049
|
+
const tvfTemplate = `-- ============================================================================
|
|
3050
|
+
-- Table-Valued Function: fn_Get{{name}}Hierarchy
|
|
3051
|
+
-- Purpose: Recursively traverse {{name}} hierarchy (ancestors/descendants)
|
|
3052
|
+
-- Generated by: SmartStack MCP scaffold_extension
|
|
3053
|
+
-- ============================================================================
|
|
3054
|
+
|
|
3055
|
+
-- Drop existing function if it exists
|
|
3056
|
+
IF OBJECT_ID('[{{schema}}].[fn_Get{{name}}Hierarchy]', 'IF') IS NOT NULL
|
|
3057
|
+
DROP FUNCTION [{{schema}}].[fn_Get{{name}}Hierarchy];
|
|
3058
|
+
GO
|
|
3059
|
+
|
|
3060
|
+
CREATE FUNCTION [{{schema}}].[fn_Get{{name}}Hierarchy]
|
|
3061
|
+
(
|
|
3062
|
+
@{{nameLower}}Id UNIQUEIDENTIFIER,
|
|
3063
|
+
@Direction VARCHAR(11) = 'descendants' -- 'ancestors', 'descendants', or 'both'
|
|
3064
|
+
)
|
|
3065
|
+
RETURNS TABLE
|
|
3066
|
+
AS
|
|
3067
|
+
RETURN
|
|
3068
|
+
(
|
|
3069
|
+
WITH HierarchyCTE AS (
|
|
3070
|
+
-- ========================================
|
|
3071
|
+
-- Base case: starting node
|
|
3072
|
+
-- ========================================
|
|
3073
|
+
SELECT
|
|
3074
|
+
Id,
|
|
3075
|
+
ParentId,
|
|
3076
|
+
Code,
|
|
3077
|
+
TenantId,
|
|
3078
|
+
0 AS [Level],
|
|
3079
|
+
CAST(Id AS NVARCHAR(MAX)) AS [Path],
|
|
3080
|
+
'self' AS Direction
|
|
3081
|
+
FROM [{{schema}}].[{{tableName}}]
|
|
3082
|
+
WHERE Id = @{{nameLower}}Id
|
|
3083
|
+
AND IsDeleted = 0
|
|
3084
|
+
|
|
3085
|
+
UNION ALL
|
|
3086
|
+
|
|
3087
|
+
-- ========================================
|
|
3088
|
+
-- Recursive: ancestors (climb up the tree)
|
|
3089
|
+
-- ========================================
|
|
3090
|
+
SELECT
|
|
3091
|
+
parent.Id,
|
|
3092
|
+
parent.ParentId,
|
|
3093
|
+
parent.Code,
|
|
3094
|
+
parent.TenantId,
|
|
3095
|
+
h.[Level] - 1,
|
|
3096
|
+
CAST(parent.Id AS NVARCHAR(MAX)) + '/' + h.[Path],
|
|
3097
|
+
'ancestor' AS Direction
|
|
3098
|
+
FROM [{{schema}}].[{{tableName}}] parent
|
|
3099
|
+
INNER JOIN HierarchyCTE h ON parent.Id = h.ParentId
|
|
3100
|
+
WHERE parent.IsDeleted = 0
|
|
3101
|
+
AND @Direction IN ('ancestors', 'both')
|
|
3102
|
+
AND h.[Level] > -50 -- Prevent infinite recursion
|
|
3103
|
+
|
|
3104
|
+
UNION ALL
|
|
3105
|
+
|
|
3106
|
+
-- ========================================
|
|
3107
|
+
-- Recursive: descendants (go down the tree)
|
|
3108
|
+
-- ========================================
|
|
3109
|
+
SELECT
|
|
3110
|
+
child.Id,
|
|
3111
|
+
child.ParentId,
|
|
3112
|
+
child.Code,
|
|
3113
|
+
child.TenantId,
|
|
3114
|
+
h.[Level] + 1,
|
|
3115
|
+
h.[Path] + '/' + CAST(child.Id AS NVARCHAR(MAX)),
|
|
3116
|
+
'descendant' AS Direction
|
|
3117
|
+
FROM [{{schema}}].[{{tableName}}] child
|
|
3118
|
+
INNER JOIN HierarchyCTE h ON child.ParentId = h.Id
|
|
3119
|
+
WHERE child.IsDeleted = 0
|
|
3120
|
+
AND @Direction IN ('descendants', 'both')
|
|
3121
|
+
AND h.[Level] < 50 -- Prevent infinite recursion
|
|
3122
|
+
)
|
|
3123
|
+
SELECT
|
|
3124
|
+
Id,
|
|
3125
|
+
ParentId,
|
|
3126
|
+
Code,
|
|
3127
|
+
TenantId,
|
|
3128
|
+
[Level],
|
|
3129
|
+
[Path],
|
|
3130
|
+
Direction,
|
|
3131
|
+
ABS([Level]) AS Depth -- Absolute depth from starting node
|
|
3132
|
+
FROM HierarchyCTE
|
|
3133
|
+
WHERE (
|
|
3134
|
+
@Direction = 'both'
|
|
3135
|
+
OR (@Direction = 'ancestors' AND Direction IN ('self', 'ancestor'))
|
|
3136
|
+
OR (@Direction = 'descendants' AND Direction IN ('self', 'descendant'))
|
|
3137
|
+
)
|
|
3138
|
+
);
|
|
3139
|
+
GO
|
|
3140
|
+
|
|
3141
|
+
-- ============================================================================
|
|
3142
|
+
-- Usage Examples
|
|
3143
|
+
-- ============================================================================
|
|
3144
|
+
|
|
3145
|
+
-- Get all descendants of a {{name}}
|
|
3146
|
+
-- SELECT * FROM [{{schema}}].[fn_Get{{name}}Hierarchy](@id, 'descendants') ORDER BY [Level];
|
|
3147
|
+
|
|
3148
|
+
-- Get all ancestors of a {{name}}
|
|
3149
|
+
-- SELECT * FROM [{{schema}}].[fn_Get{{name}}Hierarchy](@id, 'ancestors') ORDER BY [Level] DESC;
|
|
3150
|
+
|
|
3151
|
+
-- Get full hierarchy (ancestors + descendants)
|
|
3152
|
+
-- SELECT * FROM [{{schema}}].[fn_Get{{name}}Hierarchy](@id, 'both') ORDER BY [Level];
|
|
3153
|
+
|
|
3154
|
+
-- Get all {{name}}s at a specific level
|
|
3155
|
+
-- SELECT * FROM [{{schema}}].[fn_Get{{name}}Hierarchy](@rootId, 'descendants') WHERE [Level] = 2;
|
|
3156
|
+
|
|
3157
|
+
-- Check if user belongs to a specific {{name}} hierarchy
|
|
3158
|
+
-- SELECT CASE WHEN EXISTS (
|
|
3159
|
+
-- SELECT 1 FROM [{{schema}}].[fn_Get{{name}}Hierarchy](@userId{{name}}Id, 'ancestors')
|
|
3160
|
+
-- WHERE Id = @target{{name}}Id
|
|
3161
|
+
-- ) THEN 1 ELSE 0 END AS BelongsToHierarchy;
|
|
3162
|
+
`;
|
|
3163
|
+
const context = {
|
|
3164
|
+
name,
|
|
3165
|
+
nameLower: name.charAt(0).toLowerCase() + name.slice(1),
|
|
3166
|
+
tableName,
|
|
3167
|
+
schema
|
|
3168
|
+
};
|
|
3169
|
+
const tvfContent = Handlebars.compile(tvfTemplate)(context);
|
|
3170
|
+
const projectRoot = config.smartstack.projectPath;
|
|
3171
|
+
const infraPath = structure.infrastructure || path8.join(projectRoot, "Infrastructure");
|
|
3172
|
+
const scriptsPath = path8.join(infraPath, "Persistence", "Scripts", "Functions");
|
|
3173
|
+
const tvfFilePath = path8.join(scriptsPath, `fn_Get${name}Hierarchy.sql`);
|
|
3174
|
+
validatePathSecurity(tvfFilePath, projectRoot);
|
|
3175
|
+
if (!dryRun) {
|
|
3176
|
+
await ensureDirectory(scriptsPath);
|
|
3177
|
+
await writeText(tvfFilePath, tvfContent);
|
|
3178
|
+
}
|
|
3179
|
+
result.files.push({ path: tvfFilePath, content: tvfContent, type: "created" });
|
|
3180
|
+
result.instructions.push("TVF hierarchy function generated!");
|
|
3181
|
+
result.instructions.push("");
|
|
3182
|
+
result.instructions.push("**Next Steps:**");
|
|
3183
|
+
result.instructions.push(`1. Run the SQL script to create the function in your database`);
|
|
3184
|
+
result.instructions.push(`2. Add a method to I${name}Repository for hierarchy queries:`);
|
|
3185
|
+
result.instructions.push("```csharp");
|
|
3186
|
+
result.instructions.push(`Task<IReadOnlyList<${name}HierarchyDto>> GetHierarchyAsync(`);
|
|
3187
|
+
result.instructions.push(` Guid ${name.toLowerCase()}Id,`);
|
|
3188
|
+
result.instructions.push(` HierarchyDirection direction,`);
|
|
3189
|
+
result.instructions.push(` CancellationToken ct = default);`);
|
|
3190
|
+
result.instructions.push("```");
|
|
3191
|
+
result.instructions.push("");
|
|
3192
|
+
result.instructions.push(`3. Create ${name}HierarchyDto:`);
|
|
3193
|
+
result.instructions.push("```csharp");
|
|
3194
|
+
result.instructions.push(`public record ${name}HierarchyDto(`);
|
|
3195
|
+
result.instructions.push(" Guid Id,");
|
|
3196
|
+
result.instructions.push(" Guid? ParentId,");
|
|
3197
|
+
result.instructions.push(" string Code,");
|
|
3198
|
+
result.instructions.push(" Guid TenantId,");
|
|
3199
|
+
result.instructions.push(" int Level,");
|
|
3200
|
+
result.instructions.push(" string Path,");
|
|
3201
|
+
result.instructions.push(" string Direction,");
|
|
3202
|
+
result.instructions.push(" int Depth);");
|
|
3203
|
+
result.instructions.push("```");
|
|
3204
|
+
result.instructions.push("");
|
|
3205
|
+
result.instructions.push("4. Implement using raw SQL in repository:");
|
|
3206
|
+
result.instructions.push("```csharp");
|
|
3207
|
+
result.instructions.push(`var sql = "SELECT * FROM [${schema}].[fn_Get${name}Hierarchy]({0}, {1})";`);
|
|
3208
|
+
result.instructions.push(`return await _context.Database`);
|
|
3209
|
+
result.instructions.push(` .SqlQueryRaw<${name}HierarchyDto>(sql, id, direction.ToString().ToLower())`);
|
|
3210
|
+
result.instructions.push(` .ToListAsync(ct);`);
|
|
3211
|
+
result.instructions.push("```");
|
|
3212
|
+
result.instructions.push("");
|
|
3213
|
+
result.instructions.push(`**Pattern Recommendation**: TVF with CTE is optimal for:`);
|
|
3214
|
+
result.instructions.push("- Permission/role hierarchies (recursive permission check)");
|
|
3215
|
+
result.instructions.push("- Organization charts (reporting structure)");
|
|
3216
|
+
result.instructions.push("- Category trees (nested categories)");
|
|
3217
|
+
result.instructions.push("- Group memberships (user belongs to parent groups)");
|
|
3218
|
+
}
|
|
2632
3219
|
async function scaffoldController(name, options, structure, config, result, dryRun = false) {
|
|
2633
3220
|
const namespace = options?.namespace || `${config.conventions.namespaces.api}.Controllers`;
|
|
2634
3221
|
const navRoute = options?.navRoute;
|
|
@@ -3858,7 +4445,7 @@ var suggestMigrationTool = {
|
|
|
3858
4445
|
context: {
|
|
3859
4446
|
type: "string",
|
|
3860
4447
|
enum: ["core", "extensions"],
|
|
3861
|
-
description:
|
|
4448
|
+
description: 'DbContext name (default: auto-detected from .smartstack/config.json, or "core" for platform projects)'
|
|
3862
4449
|
},
|
|
3863
4450
|
version: {
|
|
3864
4451
|
type: "string",
|
|
@@ -3870,12 +4457,12 @@ var suggestMigrationTool = {
|
|
|
3870
4457
|
};
|
|
3871
4458
|
var SuggestMigrationInputSchema2 = z2.object({
|
|
3872
4459
|
description: z2.string().describe("Description of what the migration does"),
|
|
3873
|
-
context: z2.enum(["core", "extensions"]).optional().describe("DbContext name (default:
|
|
4460
|
+
context: z2.enum(["core", "extensions"]).optional().describe("DbContext name (default: auto-detected from project config)"),
|
|
3874
4461
|
version: z2.string().optional().describe('Semver version (e.g., "1.0.0")')
|
|
3875
4462
|
});
|
|
3876
4463
|
async function handleSuggestMigration(args, config) {
|
|
3877
4464
|
const input = SuggestMigrationInputSchema2.parse(args);
|
|
3878
|
-
const context = input.context || "core";
|
|
4465
|
+
const context = input.context || config.defaultDbContext || "core";
|
|
3879
4466
|
logger.info("Suggesting migration name", { description: input.description, context });
|
|
3880
4467
|
const structure = await findSmartStackStructure(config.smartstack.projectPath);
|
|
3881
4468
|
const existingMigrations = await findExistingMigrations(structure, config, context);
|
|
@@ -3898,7 +4485,8 @@ async function handleSuggestMigration(args, config) {
|
|
|
3898
4485
|
const sequenceStr = sequence.toString().padStart(3, "0");
|
|
3899
4486
|
const migrationName = `${context}_v${version}_${sequenceStr}_${pascalDescription}`;
|
|
3900
4487
|
const dbContextName = context === "core" ? "CoreDbContext" : "ExtensionsDbContext";
|
|
3901
|
-
const
|
|
4488
|
+
const outputPath = context === "extensions" ? "Persistence/Migrations/Extensions" : "Persistence/Migrations";
|
|
4489
|
+
const command = `dotnet ef migrations add ${migrationName} --context ${dbContextName} --project ../SmartStack.Infrastructure -o ${outputPath}`;
|
|
3902
4490
|
const lines = [];
|
|
3903
4491
|
lines.push("# Migration Name Suggestion");
|
|
3904
4492
|
lines.push("");
|
|
@@ -10587,141 +11175,2864 @@ function calculateMetrics(functions, fileMetrics, thresholds) {
|
|
|
10587
11175
|
fileSize: createStat(fileSizes, thresholds.fileSize)
|
|
10588
11176
|
};
|
|
10589
11177
|
}
|
|
10590
|
-
function identifyHotspots(functions, fileMetrics, thresholds) {
|
|
10591
|
-
const hotspots = [];
|
|
10592
|
-
for (const func of functions) {
|
|
10593
|
-
const issues = [];
|
|
10594
|
-
if (func.cognitiveComplexity > thresholds.cognitiveComplexity) {
|
|
10595
|
-
issues.push(`Cognitive complexity: ${func.cognitiveComplexity} (threshold: ${thresholds.cognitiveComplexity})`);
|
|
11178
|
+
function identifyHotspots(functions, fileMetrics, thresholds) {
|
|
11179
|
+
const hotspots = [];
|
|
11180
|
+
for (const func of functions) {
|
|
11181
|
+
const issues = [];
|
|
11182
|
+
if (func.cognitiveComplexity > thresholds.cognitiveComplexity) {
|
|
11183
|
+
issues.push(`Cognitive complexity: ${func.cognitiveComplexity} (threshold: ${thresholds.cognitiveComplexity})`);
|
|
11184
|
+
}
|
|
11185
|
+
if (func.cyclomaticComplexity > thresholds.cyclomaticComplexity) {
|
|
11186
|
+
issues.push(`Cyclomatic complexity: ${func.cyclomaticComplexity} (threshold: ${thresholds.cyclomaticComplexity})`);
|
|
11187
|
+
}
|
|
11188
|
+
if (func.lineCount > thresholds.functionSize) {
|
|
11189
|
+
issues.push(`Lines: ${func.lineCount} (threshold: ${thresholds.functionSize})`);
|
|
11190
|
+
}
|
|
11191
|
+
if (func.maxNestingDepth > thresholds.nestingDepth) {
|
|
11192
|
+
issues.push(`Nesting depth: ${func.maxNestingDepth} (threshold: ${thresholds.nestingDepth})`);
|
|
11193
|
+
}
|
|
11194
|
+
if (issues.length > 0) {
|
|
11195
|
+
const severity = issues.length >= 3 ? "high" : issues.length === 2 ? "medium" : "low";
|
|
11196
|
+
hotspots.push({
|
|
11197
|
+
file: func.file,
|
|
11198
|
+
function: func.name,
|
|
11199
|
+
issues,
|
|
11200
|
+
severity,
|
|
11201
|
+
metrics: {
|
|
11202
|
+
cognitiveComplexity: func.cognitiveComplexity,
|
|
11203
|
+
cyclomaticComplexity: func.cyclomaticComplexity,
|
|
11204
|
+
lineCount: func.lineCount,
|
|
11205
|
+
nestingDepth: func.maxNestingDepth
|
|
11206
|
+
}
|
|
11207
|
+
});
|
|
11208
|
+
}
|
|
11209
|
+
}
|
|
11210
|
+
for (const [file, metrics] of fileMetrics) {
|
|
11211
|
+
if (metrics.lineCount > thresholds.fileSize) {
|
|
11212
|
+
hotspots.push({
|
|
11213
|
+
file,
|
|
11214
|
+
issues: [`File size: ${metrics.lineCount} lines (threshold: ${thresholds.fileSize})`],
|
|
11215
|
+
severity: "medium",
|
|
11216
|
+
metrics: {
|
|
11217
|
+
lineCount: metrics.lineCount
|
|
11218
|
+
}
|
|
11219
|
+
});
|
|
11220
|
+
}
|
|
11221
|
+
}
|
|
11222
|
+
const severityOrder = { high: 0, medium: 1, low: 2 };
|
|
11223
|
+
hotspots.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
11224
|
+
return hotspots.slice(0, 20);
|
|
11225
|
+
}
|
|
11226
|
+
function calculateSummary(functions, fileMetrics, metrics, hotspots) {
|
|
11227
|
+
const totalViolations = metrics.cognitiveComplexity.violations + metrics.cyclomaticComplexity.violations + metrics.functionSize.violations + metrics.nestingDepth.violations + metrics.fileSize.violations;
|
|
11228
|
+
const totalFunctions = functions.length;
|
|
11229
|
+
const totalFiles = fileMetrics.size;
|
|
11230
|
+
const violationRate = totalFunctions > 0 ? totalViolations / totalFunctions : 0;
|
|
11231
|
+
const score = Math.max(0, Math.round(100 - violationRate * 100));
|
|
11232
|
+
let grade;
|
|
11233
|
+
if (score >= 90) grade = "A";
|
|
11234
|
+
else if (score >= 80) grade = "B";
|
|
11235
|
+
else if (score >= 70) grade = "C";
|
|
11236
|
+
else if (score >= 60) grade = "D";
|
|
11237
|
+
else grade = "F";
|
|
11238
|
+
return {
|
|
11239
|
+
score,
|
|
11240
|
+
grade,
|
|
11241
|
+
filesAnalyzed: totalFiles,
|
|
11242
|
+
functionsAnalyzed: totalFunctions,
|
|
11243
|
+
issuesFound: hotspots.length
|
|
11244
|
+
};
|
|
11245
|
+
}
|
|
11246
|
+
function isExcludedPath(filePath) {
|
|
11247
|
+
const exclusions = [
|
|
11248
|
+
/[/\\]bin[/\\]/,
|
|
11249
|
+
/[/\\]obj[/\\]/,
|
|
11250
|
+
/[/\\]node_modules[/\\]/,
|
|
11251
|
+
/[/\\]Migrations[/\\]/,
|
|
11252
|
+
/\.test\./,
|
|
11253
|
+
/\.spec\./,
|
|
11254
|
+
/Tests[/\\]/,
|
|
11255
|
+
/\.d\.ts$/,
|
|
11256
|
+
/\.min\./
|
|
11257
|
+
];
|
|
11258
|
+
return exclusions.some((pattern) => pattern.test(filePath));
|
|
11259
|
+
}
|
|
11260
|
+
function getLineNumber2(content, index) {
|
|
11261
|
+
const normalizedContent = content.substring(0, index).replace(/\r\n/g, "\n");
|
|
11262
|
+
return normalizedContent.split("\n").length;
|
|
11263
|
+
}
|
|
11264
|
+
function formatQualityReport(result, thresholds) {
|
|
11265
|
+
const lines = [];
|
|
11266
|
+
lines.push("# Code Quality Report");
|
|
11267
|
+
lines.push("");
|
|
11268
|
+
lines.push("## Summary");
|
|
11269
|
+
lines.push(`- **Score**: ${result.summary.score}/100 (Grade: ${result.summary.grade})`);
|
|
11270
|
+
lines.push(`- **Files analyzed**: ${result.summary.filesAnalyzed}`);
|
|
11271
|
+
lines.push(`- **Functions analyzed**: ${result.summary.functionsAnalyzed}`);
|
|
11272
|
+
lines.push(`- **Issues found**: ${result.summary.issuesFound}`);
|
|
11273
|
+
lines.push("");
|
|
11274
|
+
lines.push("## Metrics Overview");
|
|
11275
|
+
lines.push("");
|
|
11276
|
+
lines.push("| Metric | Average | Max | Threshold | Status |");
|
|
11277
|
+
lines.push("|--------|---------|-----|-----------|--------|");
|
|
11278
|
+
const formatMetricRow = (name, stat2) => {
|
|
11279
|
+
const status = stat2.violations > 0 ? `${stat2.violations} violations` : "\u2705 OK";
|
|
11280
|
+
return `| ${name} | ${stat2.average} | ${stat2.max} | ${stat2.threshold} | ${status} |`;
|
|
11281
|
+
};
|
|
11282
|
+
lines.push(formatMetricRow("Cognitive Complexity", result.metrics.cognitiveComplexity));
|
|
11283
|
+
lines.push(formatMetricRow("Cyclomatic Complexity", result.metrics.cyclomaticComplexity));
|
|
11284
|
+
lines.push(formatMetricRow("Function Size", result.metrics.functionSize));
|
|
11285
|
+
lines.push(formatMetricRow("Nesting Depth", result.metrics.nestingDepth));
|
|
11286
|
+
lines.push(formatMetricRow("File Size", result.metrics.fileSize));
|
|
11287
|
+
lines.push("");
|
|
11288
|
+
if (result.hotspots.length > 0) {
|
|
11289
|
+
lines.push("## Hotspots (Needs Attention)");
|
|
11290
|
+
lines.push("");
|
|
11291
|
+
for (const hotspot of result.hotspots) {
|
|
11292
|
+
const severityEmoji = hotspot.severity === "high" ? "\u{1F534}" : hotspot.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
11293
|
+
const location = hotspot.function ? `\`${hotspot.function}\` (${hotspot.file})` : `\`${hotspot.file}\``;
|
|
11294
|
+
lines.push(`### ${severityEmoji} ${hotspot.severity.toUpperCase()}: ${location}`);
|
|
11295
|
+
for (const issue of hotspot.issues) {
|
|
11296
|
+
lines.push(`- ${issue}`);
|
|
11297
|
+
}
|
|
11298
|
+
if (hotspot.function) {
|
|
11299
|
+
if (hotspot.metrics.cognitiveComplexity && hotspot.metrics.cognitiveComplexity > thresholds.cognitiveComplexity) {
|
|
11300
|
+
lines.push(`- **Recommendation**: Extract logic into smaller, focused methods`);
|
|
11301
|
+
} else if (hotspot.metrics.lineCount && hotspot.metrics.lineCount > thresholds.functionSize) {
|
|
11302
|
+
lines.push(`- **Recommendation**: Split into multiple functions with single responsibilities`);
|
|
11303
|
+
}
|
|
11304
|
+
} else {
|
|
11305
|
+
lines.push(`- **Recommendation**: Consider splitting this file into smaller modules`);
|
|
11306
|
+
}
|
|
11307
|
+
lines.push("");
|
|
11308
|
+
}
|
|
11309
|
+
} else {
|
|
11310
|
+
lines.push("No hotspots found. Code quality is within acceptable thresholds.");
|
|
11311
|
+
}
|
|
11312
|
+
return lines.join("\n");
|
|
11313
|
+
}
|
|
11314
|
+
|
|
11315
|
+
// src/tools/analyze-hierarchy-patterns.ts
|
|
11316
|
+
import path22 from "path";
|
|
11317
|
+
var analyzeHierarchyPatternsTool = {
|
|
11318
|
+
name: "analyze_hierarchy_patterns",
|
|
11319
|
+
description: `Analyze parent-child relationships in SmartStack entities and recommend optimal hierarchy patterns.
|
|
11320
|
+
|
|
11321
|
+
Detects:
|
|
11322
|
+
- Direct parent-child relationships (via foreign keys)
|
|
11323
|
+
- Self-referencing hierarchies (ParentId pointing to same entity)
|
|
11324
|
+
- Inheritance chains (entity extending another entity)
|
|
11325
|
+
- Collection relationships (ICollection<T>)
|
|
11326
|
+
|
|
11327
|
+
Recommends patterns based on usage:
|
|
11328
|
+
- **TVF + CTE**: Deep hierarchies with frequent traversal (permissions, org charts)
|
|
11329
|
+
- **Materialized Path**: Deep hierarchies with frequent modifications
|
|
11330
|
+
- **Simple ParentId**: Shallow hierarchies (2-3 levels)
|
|
11331
|
+
- **Nested Sets**: Read-heavy, rarely modified trees
|
|
11332
|
+
- **Closure Table**: All operations need to be fast`,
|
|
11333
|
+
inputSchema: {
|
|
11334
|
+
type: "object",
|
|
11335
|
+
properties: {
|
|
11336
|
+
path: {
|
|
11337
|
+
type: "string",
|
|
11338
|
+
description: "Project path to analyze (default: SmartStack.app path from config)"
|
|
11339
|
+
},
|
|
11340
|
+
entityFilter: {
|
|
11341
|
+
type: "string",
|
|
11342
|
+
description: 'Filter entities by name pattern (e.g., "User*", "*Group*")'
|
|
11343
|
+
},
|
|
11344
|
+
includeRecommendations: {
|
|
11345
|
+
type: "boolean",
|
|
11346
|
+
default: true,
|
|
11347
|
+
description: "Include pattern recommendations for each hierarchy"
|
|
11348
|
+
},
|
|
11349
|
+
outputFormat: {
|
|
11350
|
+
type: "string",
|
|
11351
|
+
enum: ["summary", "detailed", "json"],
|
|
11352
|
+
default: "detailed",
|
|
11353
|
+
description: "Output format"
|
|
11354
|
+
}
|
|
11355
|
+
}
|
|
11356
|
+
}
|
|
11357
|
+
};
|
|
11358
|
+
async function handleAnalyzeHierarchyPatterns(args, config) {
|
|
11359
|
+
const input = AnalyzeHierarchyPatternsInputSchema.parse(args);
|
|
11360
|
+
const projectPath = input.path || config.smartstack.projectPath;
|
|
11361
|
+
const includeRecommendations = input.includeRecommendations ?? true;
|
|
11362
|
+
const outputFormat = input.outputFormat || "detailed";
|
|
11363
|
+
logger.info("Analyzing hierarchy patterns", { projectPath });
|
|
11364
|
+
const structure = await findSmartStackStructure(projectPath);
|
|
11365
|
+
if (!structure.domain) {
|
|
11366
|
+
return "# Error\n\nDomain project not found. Cannot analyze entity hierarchies.";
|
|
11367
|
+
}
|
|
11368
|
+
const entityGraph = await buildEntityGraph(structure.domain, input.entityFilter);
|
|
11369
|
+
const hierarchies = detectHierarchies(entityGraph);
|
|
11370
|
+
const recommendations = includeRecommendations ? generateRecommendations4(hierarchies, entityGraph) : [];
|
|
11371
|
+
const result = {
|
|
11372
|
+
totalEntities: entityGraph.size,
|
|
11373
|
+
hierarchicalEntities: hierarchies.filter((h) => h.isHierarchical).length,
|
|
11374
|
+
selfReferencingEntities: hierarchies.filter((h) => h.isSelfReferencing).length,
|
|
11375
|
+
maxDepth: Math.max(0, ...hierarchies.map((h) => h.depth)),
|
|
11376
|
+
hierarchies,
|
|
11377
|
+
recommendations,
|
|
11378
|
+
circularDependencies: detectCircularDependencies(entityGraph)
|
|
11379
|
+
};
|
|
11380
|
+
return formatResult8(result, outputFormat, structure.root);
|
|
11381
|
+
}
|
|
11382
|
+
async function buildEntityGraph(domainPath, filter) {
|
|
11383
|
+
const entityFiles = await findFiles("**/*.cs", { cwd: domainPath });
|
|
11384
|
+
const entityMap = /* @__PURE__ */ new Map();
|
|
11385
|
+
const parsedEntities = [];
|
|
11386
|
+
for (const file of entityFiles) {
|
|
11387
|
+
const content = await readText(file);
|
|
11388
|
+
const parsed = parseEntity(content, file, domainPath);
|
|
11389
|
+
if (parsed && (!filter || matchesFilter(parsed.name, filter))) {
|
|
11390
|
+
parsedEntities.push(parsed);
|
|
11391
|
+
}
|
|
11392
|
+
}
|
|
11393
|
+
for (const entity of parsedEntities) {
|
|
11394
|
+
entityMap.set(entity.name, {
|
|
11395
|
+
name: entity.name,
|
|
11396
|
+
file: entity.file,
|
|
11397
|
+
baseClass: entity.baseClass,
|
|
11398
|
+
isTenantAware: entity.isTenantAware,
|
|
11399
|
+
isSystemEntity: entity.isSystemEntity,
|
|
11400
|
+
parent: null,
|
|
11401
|
+
children: [],
|
|
11402
|
+
depth: 0,
|
|
11403
|
+
isRoot: true,
|
|
11404
|
+
isLeaf: true,
|
|
11405
|
+
isSelfReferencing: !!entity.selfReferenceField,
|
|
11406
|
+
selfReferenceField: entity.selfReferenceField,
|
|
11407
|
+
childCollections: entity.childCollections,
|
|
11408
|
+
foreignKeys: entity.foreignKeys,
|
|
11409
|
+
isHierarchical: false
|
|
11410
|
+
});
|
|
11411
|
+
}
|
|
11412
|
+
for (const entity of parsedEntities) {
|
|
11413
|
+
const node = entityMap.get(entity.name);
|
|
11414
|
+
if (entity.parentEntity && entityMap.has(entity.parentEntity)) {
|
|
11415
|
+
const parent = entityMap.get(entity.parentEntity);
|
|
11416
|
+
node.parent = parent.name;
|
|
11417
|
+
node.isRoot = false;
|
|
11418
|
+
parent.children.push(entity.name);
|
|
11419
|
+
parent.isLeaf = false;
|
|
11420
|
+
}
|
|
11421
|
+
if (entity.childCollections.length > 0 || entity.selfReferenceField) {
|
|
11422
|
+
node.isHierarchical = true;
|
|
11423
|
+
}
|
|
11424
|
+
}
|
|
11425
|
+
for (const node of entityMap.values()) {
|
|
11426
|
+
node.depth = calculateDepth(node.name, entityMap, /* @__PURE__ */ new Set());
|
|
11427
|
+
}
|
|
11428
|
+
for (const node of entityMap.values()) {
|
|
11429
|
+
if (node.children.length > 0) {
|
|
11430
|
+
node.isHierarchical = true;
|
|
11431
|
+
}
|
|
11432
|
+
}
|
|
11433
|
+
return entityMap;
|
|
11434
|
+
}
|
|
11435
|
+
function parseEntity(content, file, domainPath) {
|
|
11436
|
+
const fileName = path22.basename(file, ".cs");
|
|
11437
|
+
if (fileName.endsWith("Dto") || fileName.endsWith("Command") || fileName.endsWith("Query") || fileName.endsWith("Handler") || fileName.endsWith("Validator") || fileName.endsWith("Exception") || fileName.startsWith("I")) {
|
|
11438
|
+
return null;
|
|
11439
|
+
}
|
|
11440
|
+
const classMatch = content.match(
|
|
11441
|
+
/public\s+(?:class|record)\s+(\w+)(?:\s*:\s*([^{]+))?/
|
|
11442
|
+
);
|
|
11443
|
+
if (!classMatch) return null;
|
|
11444
|
+
const entityName = classMatch[1];
|
|
11445
|
+
const inheritance = classMatch[2]?.trim() || "";
|
|
11446
|
+
const hasBaseEntity = inheritance.includes("BaseEntity");
|
|
11447
|
+
const hasSystemEntity = inheritance.includes("SystemEntity");
|
|
11448
|
+
if (!hasBaseEntity && !hasSystemEntity) return null;
|
|
11449
|
+
const hasITenantEntity = inheritance.includes("ITenantEntity");
|
|
11450
|
+
const selfRefMatch = content.match(
|
|
11451
|
+
new RegExp(`public\\s+(?:Guid\\??|${entityName}\\??)\\s+(Parent(?:Id)?|Parent${entityName}(?:Id)?)\\s*\\{`, "i")
|
|
11452
|
+
);
|
|
11453
|
+
const selfReferenceField = selfRefMatch ? selfRefMatch[1] : void 0;
|
|
11454
|
+
const childCollections = [];
|
|
11455
|
+
const collectionMatches = content.matchAll(
|
|
11456
|
+
/public\s+(?:virtual\s+)?ICollection<(\w+)>\s+(\w+)\s*\{/g
|
|
11457
|
+
);
|
|
11458
|
+
for (const match of collectionMatches) {
|
|
11459
|
+
childCollections.push(match[1]);
|
|
11460
|
+
}
|
|
11461
|
+
const foreignKeys = [];
|
|
11462
|
+
const fkMatches = content.matchAll(
|
|
11463
|
+
/public\s+Guid\s+(\w+Id)\s*\{[^}]+\}/g
|
|
11464
|
+
);
|
|
11465
|
+
for (const match of fkMatches) {
|
|
11466
|
+
const propName = match[1];
|
|
11467
|
+
if (selfReferenceField && propName.toLowerCase().includes("parent")) continue;
|
|
11468
|
+
const referencedEntity = propName.replace(/Id$/, "");
|
|
11469
|
+
if (referencedEntity !== entityName) {
|
|
11470
|
+
foreignKeys.push({
|
|
11471
|
+
propertyName: propName,
|
|
11472
|
+
referencedEntity,
|
|
11473
|
+
isNavigationProperty: content.includes(`public virtual ${referencedEntity}?`)
|
|
11474
|
+
});
|
|
11475
|
+
}
|
|
11476
|
+
}
|
|
11477
|
+
const parentEntity = foreignKeys.length > 0 ? foreignKeys[0].referencedEntity : void 0;
|
|
11478
|
+
return {
|
|
11479
|
+
name: entityName,
|
|
11480
|
+
file: path22.relative(domainPath, file),
|
|
11481
|
+
baseClass: hasSystemEntity ? "SystemEntity" : "BaseEntity",
|
|
11482
|
+
isTenantAware: hasITenantEntity,
|
|
11483
|
+
isSystemEntity: hasSystemEntity,
|
|
11484
|
+
parentEntity,
|
|
11485
|
+
selfReferenceField,
|
|
11486
|
+
childCollections,
|
|
11487
|
+
foreignKeys
|
|
11488
|
+
};
|
|
11489
|
+
}
|
|
11490
|
+
function matchesFilter(name, filter) {
|
|
11491
|
+
const pattern = filter.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
11492
|
+
return new RegExp(`^${pattern}$`, "i").test(name);
|
|
11493
|
+
}
|
|
11494
|
+
function calculateDepth(name, entityMap, visited) {
|
|
11495
|
+
if (visited.has(name)) return -1;
|
|
11496
|
+
const node = entityMap.get(name);
|
|
11497
|
+
if (!node || !node.parent) return 0;
|
|
11498
|
+
visited.add(name);
|
|
11499
|
+
const parentDepth = calculateDepth(node.parent, entityMap, visited);
|
|
11500
|
+
return parentDepth === -1 ? -1 : parentDepth + 1;
|
|
11501
|
+
}
|
|
11502
|
+
function detectHierarchies(entityMap) {
|
|
11503
|
+
return Array.from(entityMap.values()).filter(
|
|
11504
|
+
(node) => node.isHierarchical || node.isSelfReferencing || node.children.length > 0
|
|
11505
|
+
);
|
|
11506
|
+
}
|
|
11507
|
+
function detectCircularDependencies(entityMap) {
|
|
11508
|
+
const cycles = [];
|
|
11509
|
+
for (const [name] of entityMap) {
|
|
11510
|
+
let dfs2 = function(current) {
|
|
11511
|
+
if (path27.includes(current)) {
|
|
11512
|
+
const cycleStart = path27.indexOf(current);
|
|
11513
|
+
cycles.push([...path27.slice(cycleStart), current]);
|
|
11514
|
+
return true;
|
|
11515
|
+
}
|
|
11516
|
+
if (visited.has(current)) return false;
|
|
11517
|
+
visited.add(current);
|
|
11518
|
+
path27.push(current);
|
|
11519
|
+
const node = entityMap.get(current);
|
|
11520
|
+
if (node?.parent) {
|
|
11521
|
+
dfs2(node.parent);
|
|
11522
|
+
}
|
|
11523
|
+
path27.pop();
|
|
11524
|
+
return false;
|
|
11525
|
+
};
|
|
11526
|
+
var dfs = dfs2;
|
|
11527
|
+
const visited = /* @__PURE__ */ new Set();
|
|
11528
|
+
const path27 = [];
|
|
11529
|
+
dfs2(name);
|
|
11530
|
+
}
|
|
11531
|
+
const unique = /* @__PURE__ */ new Map();
|
|
11532
|
+
for (const cycle of cycles) {
|
|
11533
|
+
const key = [...cycle].sort().join(",");
|
|
11534
|
+
if (!unique.has(key)) {
|
|
11535
|
+
unique.set(key, cycle);
|
|
11536
|
+
}
|
|
11537
|
+
}
|
|
11538
|
+
return Array.from(unique.values());
|
|
11539
|
+
}
|
|
11540
|
+
function generateRecommendations4(hierarchies, entityMap) {
|
|
11541
|
+
const recommendations = [];
|
|
11542
|
+
for (const hierarchy of hierarchies) {
|
|
11543
|
+
const rec = analyzeHierarchy(hierarchy, entityMap);
|
|
11544
|
+
if (rec) {
|
|
11545
|
+
recommendations.push(rec);
|
|
11546
|
+
}
|
|
11547
|
+
}
|
|
11548
|
+
return recommendations;
|
|
11549
|
+
}
|
|
11550
|
+
function analyzeHierarchy(node, entityMap) {
|
|
11551
|
+
if (!node.isRoot && !node.isSelfReferencing) return null;
|
|
11552
|
+
const depth = calculateMaxChildDepth(node, entityMap);
|
|
11553
|
+
const totalNodes = countDescendants(node, entityMap) + 1;
|
|
11554
|
+
const hasPermissionPattern = node.name.toLowerCase().includes("permission") || node.name.toLowerCase().includes("role") || node.name.toLowerCase().includes("group");
|
|
11555
|
+
let pattern;
|
|
11556
|
+
let reason;
|
|
11557
|
+
let priority;
|
|
11558
|
+
let sqlTemplate;
|
|
11559
|
+
if (node.isSelfReferencing) {
|
|
11560
|
+
if (depth > 3 || hasPermissionPattern) {
|
|
11561
|
+
pattern = "tvf-cte";
|
|
11562
|
+
reason = `Self-referencing hierarchy with variable depth. ${hasPermissionPattern ? "Permission/Group pattern detected - TVF recommended for recursive permission resolution." : ""}`;
|
|
11563
|
+
priority = hasPermissionPattern ? "critical" : "high";
|
|
11564
|
+
sqlTemplate = generateTvfTemplate(node);
|
|
11565
|
+
} else if (depth <= 3) {
|
|
11566
|
+
pattern = "simple-parentid";
|
|
11567
|
+
reason = "Shallow self-referencing hierarchy (depth <= 3). Simple ParentId with eager loading is sufficient.";
|
|
11568
|
+
priority = "low";
|
|
11569
|
+
} else {
|
|
11570
|
+
pattern = "materialized-path";
|
|
11571
|
+
reason = "Deep self-referencing hierarchy. Consider Materialized Path for efficient ancestor/descendant queries.";
|
|
11572
|
+
priority = "medium";
|
|
11573
|
+
}
|
|
11574
|
+
} else if (node.children.length > 0) {
|
|
11575
|
+
if (depth > 2) {
|
|
11576
|
+
pattern = "tvf-cte";
|
|
11577
|
+
reason = `Multi-level parent-child hierarchy (depth: ${depth}). TVF with CTE recommended for efficient traversal.`;
|
|
11578
|
+
priority = "medium";
|
|
11579
|
+
sqlTemplate = generateTvfTemplate(node);
|
|
11580
|
+
} else {
|
|
11581
|
+
pattern = "simple-parentid";
|
|
11582
|
+
reason = `Simple two-level parent-child relationship. Standard FK relationship is optimal.`;
|
|
11583
|
+
priority = "low";
|
|
11584
|
+
}
|
|
11585
|
+
} else {
|
|
11586
|
+
return null;
|
|
11587
|
+
}
|
|
11588
|
+
return {
|
|
11589
|
+
entityName: node.name,
|
|
11590
|
+
currentPattern: node.isSelfReferencing ? "self-referencing" : "parent-child",
|
|
11591
|
+
recommendedPattern: pattern,
|
|
11592
|
+
reason,
|
|
11593
|
+
priority,
|
|
11594
|
+
hierarchyDepth: depth,
|
|
11595
|
+
estimatedNodes: totalNodes,
|
|
11596
|
+
sqlTemplate,
|
|
11597
|
+
implementationSteps: getImplementationSteps(pattern, node)
|
|
11598
|
+
};
|
|
11599
|
+
}
|
|
11600
|
+
function calculateMaxChildDepth(node, entityMap, visited = /* @__PURE__ */ new Set()) {
|
|
11601
|
+
if (visited.has(node.name)) return 0;
|
|
11602
|
+
visited.add(node.name);
|
|
11603
|
+
if (node.isSelfReferencing) {
|
|
11604
|
+
return 5;
|
|
11605
|
+
}
|
|
11606
|
+
if (node.children.length === 0) return 0;
|
|
11607
|
+
let maxChildDepth = 0;
|
|
11608
|
+
for (const childName of node.children) {
|
|
11609
|
+
const child = entityMap.get(childName);
|
|
11610
|
+
if (child) {
|
|
11611
|
+
const childDepth = calculateMaxChildDepth(child, entityMap, visited);
|
|
11612
|
+
maxChildDepth = Math.max(maxChildDepth, childDepth);
|
|
11613
|
+
}
|
|
11614
|
+
}
|
|
11615
|
+
return maxChildDepth + 1;
|
|
11616
|
+
}
|
|
11617
|
+
function countDescendants(node, entityMap, visited = /* @__PURE__ */ new Set()) {
|
|
11618
|
+
if (visited.has(node.name)) return 0;
|
|
11619
|
+
visited.add(node.name);
|
|
11620
|
+
let count = 0;
|
|
11621
|
+
for (const childName of node.children) {
|
|
11622
|
+
const child = entityMap.get(childName);
|
|
11623
|
+
if (child) {
|
|
11624
|
+
count += 1 + countDescendants(child, entityMap, visited);
|
|
11625
|
+
}
|
|
11626
|
+
}
|
|
11627
|
+
return count;
|
|
11628
|
+
}
|
|
11629
|
+
function generateTvfTemplate(node) {
|
|
11630
|
+
const tableName = `${node.name}s`;
|
|
11631
|
+
const idField = "Id";
|
|
11632
|
+
const parentField = node.selfReferenceField || "ParentId";
|
|
11633
|
+
return `-- Table-Valued Function with Recursive CTE
|
|
11634
|
+
-- Returns all ancestors/descendants for ${node.name}
|
|
11635
|
+
CREATE OR ALTER FUNCTION [core].[fn_Get${node.name}Hierarchy]
|
|
11636
|
+
(
|
|
11637
|
+
@${node.name}Id UNIQUEIDENTIFIER,
|
|
11638
|
+
@Direction VARCHAR(10) = 'ancestors' -- 'ancestors' or 'descendants'
|
|
11639
|
+
)
|
|
11640
|
+
RETURNS TABLE
|
|
11641
|
+
AS
|
|
11642
|
+
RETURN
|
|
11643
|
+
(
|
|
11644
|
+
WITH Hierarchy AS (
|
|
11645
|
+
-- Base case: starting node
|
|
11646
|
+
SELECT
|
|
11647
|
+
${idField},
|
|
11648
|
+
${parentField},
|
|
11649
|
+
0 AS Level,
|
|
11650
|
+
CAST(${idField} AS VARCHAR(MAX)) AS Path
|
|
11651
|
+
FROM core.${tableName}
|
|
11652
|
+
WHERE ${idField} = @${node.name}Id
|
|
11653
|
+
AND IsDeleted = 0
|
|
11654
|
+
|
|
11655
|
+
UNION ALL
|
|
11656
|
+
|
|
11657
|
+
-- Recursive case: traverse hierarchy
|
|
11658
|
+
SELECT
|
|
11659
|
+
t.${idField},
|
|
11660
|
+
t.${parentField},
|
|
11661
|
+
h.Level + 1,
|
|
11662
|
+
h.Path + '/' + CAST(t.${idField} AS VARCHAR(MAX))
|
|
11663
|
+
FROM core.${tableName} t
|
|
11664
|
+
INNER JOIN Hierarchy h ON
|
|
11665
|
+
CASE @Direction
|
|
11666
|
+
WHEN 'ancestors' THEN t.${idField}
|
|
11667
|
+
WHEN 'descendants' THEN t.${parentField}
|
|
11668
|
+
END =
|
|
11669
|
+
CASE @Direction
|
|
11670
|
+
WHEN 'ancestors' THEN h.${parentField}
|
|
11671
|
+
WHEN 'descendants' THEN h.${idField}
|
|
11672
|
+
END
|
|
11673
|
+
WHERE t.IsDeleted = 0
|
|
11674
|
+
AND h.Level < 50 -- Prevent infinite recursion
|
|
11675
|
+
)
|
|
11676
|
+
SELECT
|
|
11677
|
+
${idField},
|
|
11678
|
+
${parentField},
|
|
11679
|
+
Level,
|
|
11680
|
+
Path
|
|
11681
|
+
FROM Hierarchy
|
|
11682
|
+
);
|
|
11683
|
+
GO
|
|
11684
|
+
|
|
11685
|
+
-- Usage example:
|
|
11686
|
+
-- SELECT * FROM core.fn_Get${node.name}Hierarchy(@id, 'descendants');
|
|
11687
|
+
-- SELECT * FROM core.fn_Get${node.name}Hierarchy(@id, 'ancestors');`;
|
|
11688
|
+
}
|
|
11689
|
+
function getImplementationSteps(pattern, node) {
|
|
11690
|
+
switch (pattern) {
|
|
11691
|
+
case "tvf-cte":
|
|
11692
|
+
return [
|
|
11693
|
+
`1. Create migration with TVF: fn_Get${node.name}Hierarchy`,
|
|
11694
|
+
`2. Add method to I${node.name}Repository: GetHierarchyAsync(Guid id, HierarchyDirection direction)`,
|
|
11695
|
+
`3. Implement using raw SQL: _context.Database.SqlQuery<${node.name}HierarchyDto>(...)`,
|
|
11696
|
+
`4. Create ${node.name}HierarchyDto with Id, ParentId, Level, Path`,
|
|
11697
|
+
`5. Add unit tests for hierarchy traversal (ancestors/descendants)`
|
|
11698
|
+
];
|
|
11699
|
+
case "materialized-path":
|
|
11700
|
+
return [
|
|
11701
|
+
`1. Add Path column to ${node.name}: public string Path { get; private set; } // e.g., "/1/3/5/"`,
|
|
11702
|
+
`2. Add PathDepth computed column or property`,
|
|
11703
|
+
`3. Create index on Path for prefix queries`,
|
|
11704
|
+
`4. Update Create/Update methods to maintain Path`,
|
|
11705
|
+
`5. Query ancestors: WHERE @path LIKE Path + '%'`,
|
|
11706
|
+
`6. Query descendants: WHERE Path LIKE @path + '%'`
|
|
11707
|
+
];
|
|
11708
|
+
case "simple-parentid":
|
|
11709
|
+
return [
|
|
11710
|
+
`1. Ensure ${node.selfReferenceField || "ParentId"} FK is configured`,
|
|
11711
|
+
`2. Add navigation property: public virtual ${node.name}? Parent { get; }`,
|
|
11712
|
+
`3. Add children collection: public virtual ICollection<${node.name}> Children { get; }`,
|
|
11713
|
+
`4. Use .Include() for eager loading when needed`
|
|
11714
|
+
];
|
|
11715
|
+
case "nested-sets":
|
|
11716
|
+
return [
|
|
11717
|
+
`1. Add Left and Right columns (int)`,
|
|
11718
|
+
`2. Implement tree rebuild on modifications`,
|
|
11719
|
+
`3. Query descendants: WHERE Left > @left AND Right < @right`,
|
|
11720
|
+
`4. Query ancestors: WHERE Left < @left AND Right > @right`,
|
|
11721
|
+
`5. Warning: Updates are expensive - only use for read-heavy scenarios`
|
|
11722
|
+
];
|
|
11723
|
+
case "closure-table":
|
|
11724
|
+
return [
|
|
11725
|
+
`1. Create ${node.name}Closure table: (AncestorId, DescendantId, Depth)`,
|
|
11726
|
+
`2. Populate closure table on Create/Update`,
|
|
11727
|
+
`3. Query all ancestors: WHERE DescendantId = @id`,
|
|
11728
|
+
`4. Query all descendants: WHERE AncestorId = @id`,
|
|
11729
|
+
`5. Maintain closure table on hierarchy changes`
|
|
11730
|
+
];
|
|
11731
|
+
default:
|
|
11732
|
+
return [];
|
|
11733
|
+
}
|
|
11734
|
+
}
|
|
11735
|
+
function formatResult8(result, format, _rootPath) {
|
|
11736
|
+
if (format === "json") {
|
|
11737
|
+
return JSON.stringify(result, null, 2);
|
|
11738
|
+
}
|
|
11739
|
+
const lines = [];
|
|
11740
|
+
lines.push("# Entity Hierarchy Analysis");
|
|
11741
|
+
lines.push("");
|
|
11742
|
+
lines.push("## Summary");
|
|
11743
|
+
lines.push("");
|
|
11744
|
+
lines.push(`| Metric | Value |`);
|
|
11745
|
+
lines.push(`|--------|-------|`);
|
|
11746
|
+
lines.push(`| Total Entities | ${result.totalEntities} |`);
|
|
11747
|
+
lines.push(`| Hierarchical Entities | ${result.hierarchicalEntities} |`);
|
|
11748
|
+
lines.push(`| Self-Referencing | ${result.selfReferencingEntities} |`);
|
|
11749
|
+
lines.push(`| Max Depth | ${result.maxDepth} |`);
|
|
11750
|
+
lines.push("");
|
|
11751
|
+
if (result.circularDependencies.length > 0) {
|
|
11752
|
+
lines.push("## \u26A0\uFE0F Circular Dependencies Detected");
|
|
11753
|
+
lines.push("");
|
|
11754
|
+
for (const cycle of result.circularDependencies) {
|
|
11755
|
+
lines.push(`- ${cycle.join(" \u2192 ")}`);
|
|
11756
|
+
}
|
|
11757
|
+
lines.push("");
|
|
11758
|
+
}
|
|
11759
|
+
if (result.hierarchies.length > 0) {
|
|
11760
|
+
lines.push("## Detected Hierarchies");
|
|
11761
|
+
lines.push("");
|
|
11762
|
+
for (const h of result.hierarchies) {
|
|
11763
|
+
const icon = h.isSelfReferencing ? "\u{1F504}" : "\u{1F4C2}";
|
|
11764
|
+
lines.push(`### ${icon} ${h.name}`);
|
|
11765
|
+
lines.push("");
|
|
11766
|
+
lines.push(`| Property | Value |`);
|
|
11767
|
+
lines.push(`|----------|-------|`);
|
|
11768
|
+
lines.push(`| File | \`${h.file}\` |`);
|
|
11769
|
+
lines.push(`| Type | ${h.isSelfReferencing ? "Self-referencing" : "Parent-child"} |`);
|
|
11770
|
+
lines.push(`| Tenant-aware | ${h.isTenantAware ? "Yes" : "No"} |`);
|
|
11771
|
+
lines.push(`| Depth | ${h.depth} |`);
|
|
11772
|
+
if (h.selfReferenceField) {
|
|
11773
|
+
lines.push(`| Self-ref field | \`${h.selfReferenceField}\` |`);
|
|
11774
|
+
}
|
|
11775
|
+
if (h.children.length > 0) {
|
|
11776
|
+
lines.push(`| Children | ${h.children.join(", ")} |`);
|
|
11777
|
+
}
|
|
11778
|
+
if (h.childCollections.length > 0) {
|
|
11779
|
+
lines.push(`| Collections | ${h.childCollections.join(", ")} |`);
|
|
11780
|
+
}
|
|
11781
|
+
lines.push("");
|
|
11782
|
+
}
|
|
11783
|
+
}
|
|
11784
|
+
if (result.recommendations.length > 0) {
|
|
11785
|
+
lines.push("## Pattern Recommendations");
|
|
11786
|
+
lines.push("");
|
|
11787
|
+
const priorityOrder = ["critical", "high", "medium", "low"];
|
|
11788
|
+
const sorted = [...result.recommendations].sort(
|
|
11789
|
+
(a, b) => priorityOrder.indexOf(a.priority) - priorityOrder.indexOf(b.priority)
|
|
11790
|
+
);
|
|
11791
|
+
for (const rec of sorted) {
|
|
11792
|
+
const priorityIcon = {
|
|
11793
|
+
critical: "\u{1F534}",
|
|
11794
|
+
high: "\u{1F7E0}",
|
|
11795
|
+
medium: "\u{1F7E1}",
|
|
11796
|
+
low: "\u{1F7E2}"
|
|
11797
|
+
}[rec.priority];
|
|
11798
|
+
lines.push(`### ${priorityIcon} ${rec.entityName}`);
|
|
11799
|
+
lines.push("");
|
|
11800
|
+
lines.push(`**Recommended Pattern**: \`${rec.recommendedPattern}\``);
|
|
11801
|
+
lines.push("");
|
|
11802
|
+
lines.push(`**Reason**: ${rec.reason}`);
|
|
11803
|
+
lines.push("");
|
|
11804
|
+
lines.push(`**Priority**: ${rec.priority.toUpperCase()}`);
|
|
11805
|
+
lines.push("");
|
|
11806
|
+
if (format === "detailed") {
|
|
11807
|
+
lines.push("#### Implementation Steps");
|
|
11808
|
+
lines.push("");
|
|
11809
|
+
for (const step of rec.implementationSteps) {
|
|
11810
|
+
lines.push(step);
|
|
11811
|
+
}
|
|
11812
|
+
lines.push("");
|
|
11813
|
+
if (rec.sqlTemplate) {
|
|
11814
|
+
lines.push("#### SQL Template");
|
|
11815
|
+
lines.push("");
|
|
11816
|
+
lines.push("```sql");
|
|
11817
|
+
lines.push(rec.sqlTemplate);
|
|
11818
|
+
lines.push("```");
|
|
11819
|
+
lines.push("");
|
|
11820
|
+
}
|
|
11821
|
+
}
|
|
11822
|
+
}
|
|
11823
|
+
}
|
|
11824
|
+
lines.push("---");
|
|
11825
|
+
lines.push("");
|
|
11826
|
+
lines.push("## Pattern Legend");
|
|
11827
|
+
lines.push("");
|
|
11828
|
+
lines.push("| Pattern | Best For | Trade-offs |");
|
|
11829
|
+
lines.push("|---------|----------|------------|");
|
|
11830
|
+
lines.push("| **TVF + CTE** | Deep hierarchies, frequent traversal | Requires SQL Server, slightly more complex |");
|
|
11831
|
+
lines.push("| **Materialized Path** | Deep hierarchies, frequent modifications | Path maintenance overhead |");
|
|
11832
|
+
lines.push("| **Simple ParentId** | Shallow hierarchies (2-3 levels) | N+1 queries on deep traversal |");
|
|
11833
|
+
lines.push("| **Nested Sets** | Read-heavy, rarely modified | Expensive updates |");
|
|
11834
|
+
lines.push("| **Closure Table** | All operations fast | Storage overhead (N\xB2 entries) |");
|
|
11835
|
+
return lines.join("\n");
|
|
11836
|
+
}
|
|
11837
|
+
|
|
11838
|
+
// src/tools/review-code/types.ts
|
|
11839
|
+
var LAYER_RULES = {
|
|
11840
|
+
domain: {
|
|
11841
|
+
canImport: [],
|
|
11842
|
+
cannotImport: ["application", "infrastructure", "api", "web"]
|
|
11843
|
+
},
|
|
11844
|
+
application: {
|
|
11845
|
+
canImport: ["domain"],
|
|
11846
|
+
cannotImport: ["infrastructure", "api", "web"]
|
|
11847
|
+
},
|
|
11848
|
+
infrastructure: {
|
|
11849
|
+
canImport: ["domain", "application"],
|
|
11850
|
+
cannotImport: ["api", "web"]
|
|
11851
|
+
},
|
|
11852
|
+
api: {
|
|
11853
|
+
canImport: ["domain", "application", "infrastructure"],
|
|
11854
|
+
cannotImport: ["web"]
|
|
11855
|
+
},
|
|
11856
|
+
web: {
|
|
11857
|
+
canImport: ["domain", "application"],
|
|
11858
|
+
cannotImport: ["infrastructure"]
|
|
11859
|
+
}
|
|
11860
|
+
};
|
|
11861
|
+
var FINDING_PREFIXES = {
|
|
11862
|
+
security: "SEC",
|
|
11863
|
+
architecture: "ARCH",
|
|
11864
|
+
"hardcoded-values": "HARD",
|
|
11865
|
+
tests: "TEST",
|
|
11866
|
+
"ai-hallucinations": "AI",
|
|
11867
|
+
performance: "PERF",
|
|
11868
|
+
"dead-code": "DEAD",
|
|
11869
|
+
i18n: "I18N",
|
|
11870
|
+
accessibility: "A11Y"
|
|
11871
|
+
};
|
|
11872
|
+
var findingCounters = {};
|
|
11873
|
+
function generateFindingId(category) {
|
|
11874
|
+
const prefix = FINDING_PREFIXES[category];
|
|
11875
|
+
if (!findingCounters[prefix]) {
|
|
11876
|
+
findingCounters[prefix] = 0;
|
|
11877
|
+
}
|
|
11878
|
+
findingCounters[prefix]++;
|
|
11879
|
+
return `${prefix}-${String(findingCounters[prefix]).padStart(3, "0")}`;
|
|
11880
|
+
}
|
|
11881
|
+
function resetFindingCounters() {
|
|
11882
|
+
Object.keys(findingCounters).forEach((key) => {
|
|
11883
|
+
findingCounters[key] = 0;
|
|
11884
|
+
});
|
|
11885
|
+
}
|
|
11886
|
+
|
|
11887
|
+
// src/tools/review-code/checks/security.ts
|
|
11888
|
+
var SECRET_PATTERNS = [
|
|
11889
|
+
{ pattern: /["']sk[-_]live[-_][a-zA-Z0-9]{20,}["']/gi, name: "Stripe Live Key" },
|
|
11890
|
+
{ pattern: /["']sk[-_]test[-_][a-zA-Z0-9]{20,}["']/gi, name: "Stripe Test Key" },
|
|
11891
|
+
{ pattern: /["'][a-zA-Z0-9]{40}["']/g, name: "Generic API Key (40 chars)" },
|
|
11892
|
+
{ pattern: /password\s*[=:]\s*["'][^"']{4,}["']/gi, name: "Hardcoded Password" },
|
|
11893
|
+
{ pattern: /apikey\s*[=:]\s*["'][^"']{8,}["']/gi, name: "Hardcoded API Key" },
|
|
11894
|
+
{ pattern: /secret\s*[=:]\s*["'][^"']{8,}["']/gi, name: "Hardcoded Secret" },
|
|
11895
|
+
{ pattern: /connectionstring\s*[=:]\s*["'][^"']+["']/gi, name: "Connection String" },
|
|
11896
|
+
{ pattern: /Bearer\s+[a-zA-Z0-9\-_.]+/gi, name: "Bearer Token" },
|
|
11897
|
+
{ pattern: /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/gi, name: "Private Key" }
|
|
11898
|
+
];
|
|
11899
|
+
var SQL_INJECTION_PATTERNS = [
|
|
11900
|
+
{ pattern: /\$["'`]SELECT\s+.+\s+FROM/gi, name: "String interpolation in SQL" },
|
|
11901
|
+
{ pattern: /\+\s*["'].*SELECT.*FROM/gi, name: "Concatenation in SQL" },
|
|
11902
|
+
{ pattern: /string\.Format\s*\(\s*["'].*SELECT/gi, name: "String.Format with SQL" },
|
|
11903
|
+
{ pattern: /ExecuteSqlRaw\s*\(\s*\$"/gi, name: "ExecuteSqlRaw with interpolation" },
|
|
11904
|
+
{ pattern: /FromSqlRaw\s*\(\s*\$"/gi, name: "FromSqlRaw with interpolation" }
|
|
11905
|
+
];
|
|
11906
|
+
var XSS_PATTERNS = [
|
|
11907
|
+
{ pattern: /innerHTML\s*=/gi, name: "innerHTML assignment" },
|
|
11908
|
+
{ pattern: /dangerouslySetInnerHTML/gi, name: "dangerouslySetInnerHTML" },
|
|
11909
|
+
{ pattern: /document\.write\s*\(/gi, name: "document.write" },
|
|
11910
|
+
{ pattern: /eval\s*\(/gi, name: "eval() usage" }
|
|
11911
|
+
];
|
|
11912
|
+
var CONTROLLER_WITHOUT_AUTH = /\[ApiController\][\s\S]*?public\s+class\s+\w+Controller/gi;
|
|
11913
|
+
async function checkSecurity(context) {
|
|
11914
|
+
const findings = [];
|
|
11915
|
+
for (const file of context.files) {
|
|
11916
|
+
if (!["csharp", "typescript", "javascript", "tsx", "jsx"].includes(file.language)) {
|
|
11917
|
+
continue;
|
|
11918
|
+
}
|
|
11919
|
+
const lines = file.content.split("\n");
|
|
11920
|
+
for (const { pattern, name } of SECRET_PATTERNS) {
|
|
11921
|
+
let match;
|
|
11922
|
+
pattern.lastIndex = 0;
|
|
11923
|
+
while ((match = pattern.exec(file.content)) !== null) {
|
|
11924
|
+
const lineNumber = getLineNumber3(file.content, match.index);
|
|
11925
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
11926
|
+
if (isInComment(lineContent)) continue;
|
|
11927
|
+
findings.push({
|
|
11928
|
+
id: generateFindingId("security"),
|
|
11929
|
+
category: "security",
|
|
11930
|
+
severity: "blocking",
|
|
11931
|
+
title: `Hardcoded ${name}`,
|
|
11932
|
+
description: `Found a hardcoded ${name.toLowerCase()} in the source code. Secrets should never be committed to version control.`,
|
|
11933
|
+
file: file.relativePath,
|
|
11934
|
+
line: lineNumber,
|
|
11935
|
+
code: truncateCode2(lineContent),
|
|
11936
|
+
suggestion: "Move this secret to environment variables, Azure Key Vault, or a secure configuration provider.",
|
|
11937
|
+
autoFixable: false,
|
|
11938
|
+
cweId: "CWE-798",
|
|
11939
|
+
references: ["https://cwe.mitre.org/data/definitions/798.html"]
|
|
11940
|
+
});
|
|
11941
|
+
}
|
|
11942
|
+
}
|
|
11943
|
+
if (file.language === "csharp") {
|
|
11944
|
+
for (const { pattern, name } of SQL_INJECTION_PATTERNS) {
|
|
11945
|
+
let match;
|
|
11946
|
+
pattern.lastIndex = 0;
|
|
11947
|
+
while ((match = pattern.exec(file.content)) !== null) {
|
|
11948
|
+
const lineNumber = getLineNumber3(file.content, match.index);
|
|
11949
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
11950
|
+
if (isInComment(lineContent)) continue;
|
|
11951
|
+
findings.push({
|
|
11952
|
+
id: generateFindingId("security"),
|
|
11953
|
+
category: "security",
|
|
11954
|
+
severity: "blocking",
|
|
11955
|
+
title: "Potential SQL Injection",
|
|
11956
|
+
description: `${name} detected. This pattern can lead to SQL injection vulnerabilities.`,
|
|
11957
|
+
file: file.relativePath,
|
|
11958
|
+
line: lineNumber,
|
|
11959
|
+
code: truncateCode2(lineContent),
|
|
11960
|
+
suggestion: "Use parameterized queries or EF Core LINQ methods instead of string interpolation.",
|
|
11961
|
+
autoFixable: false,
|
|
11962
|
+
cweId: "CWE-89",
|
|
11963
|
+
references: ["https://cwe.mitre.org/data/definitions/89.html"]
|
|
11964
|
+
});
|
|
11965
|
+
}
|
|
11966
|
+
}
|
|
11967
|
+
const controllerMatch = CONTROLLER_WITHOUT_AUTH.exec(file.content);
|
|
11968
|
+
if (controllerMatch) {
|
|
11969
|
+
const hasAuthorize = /\[Authorize\]/i.test(file.content.slice(0, controllerMatch.index + 200));
|
|
11970
|
+
const hasAllowAnonymous = /\[AllowAnonymous\]/i.test(file.content.slice(0, controllerMatch.index + 200));
|
|
11971
|
+
const hasNavRoute = /\[NavRoute\(/i.test(file.content);
|
|
11972
|
+
if (!hasAuthorize && !hasAllowAnonymous && !hasNavRoute) {
|
|
11973
|
+
findings.push({
|
|
11974
|
+
id: generateFindingId("security"),
|
|
11975
|
+
category: "security",
|
|
11976
|
+
severity: "critical",
|
|
11977
|
+
title: "Missing Authorization Attribute",
|
|
11978
|
+
description: "This API controller does not have [Authorize], [AllowAnonymous], or [NavRoute] attribute.",
|
|
11979
|
+
file: file.relativePath,
|
|
11980
|
+
line: getLineNumber3(file.content, controllerMatch.index),
|
|
11981
|
+
suggestion: 'Add [Authorize] or [NavRoute("...")] attribute to secure the controller.',
|
|
11982
|
+
autoFixable: false,
|
|
11983
|
+
cweId: "CWE-862"
|
|
11984
|
+
});
|
|
11985
|
+
}
|
|
11986
|
+
}
|
|
11987
|
+
}
|
|
11988
|
+
if (["typescript", "javascript", "tsx", "jsx"].includes(file.language)) {
|
|
11989
|
+
for (const { pattern, name } of XSS_PATTERNS) {
|
|
11990
|
+
let match;
|
|
11991
|
+
pattern.lastIndex = 0;
|
|
11992
|
+
while ((match = pattern.exec(file.content)) !== null) {
|
|
11993
|
+
const lineNumber = getLineNumber3(file.content, match.index);
|
|
11994
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
11995
|
+
if (isInComment(lineContent)) continue;
|
|
11996
|
+
const contextStart = Math.max(0, match.index - 200);
|
|
11997
|
+
const contextEnd = Math.min(file.content.length, match.index + 200);
|
|
11998
|
+
const context2 = file.content.slice(contextStart, contextEnd).toLowerCase();
|
|
11999
|
+
if (context2.includes("sanitize") || context2.includes("dompurify") || context2.includes("escape")) {
|
|
12000
|
+
continue;
|
|
12001
|
+
}
|
|
12002
|
+
findings.push({
|
|
12003
|
+
id: generateFindingId("security"),
|
|
12004
|
+
category: "security",
|
|
12005
|
+
severity: "critical",
|
|
12006
|
+
title: "Potential XSS Vulnerability",
|
|
12007
|
+
description: `${name} detected without apparent sanitization. This can lead to Cross-Site Scripting attacks.`,
|
|
12008
|
+
file: file.relativePath,
|
|
12009
|
+
line: lineNumber,
|
|
12010
|
+
code: truncateCode2(lineContent),
|
|
12011
|
+
suggestion: "Sanitize user input before rendering. Use DOMPurify or React's built-in escaping.",
|
|
12012
|
+
autoFixable: false,
|
|
12013
|
+
cweId: "CWE-79",
|
|
12014
|
+
references: ["https://cwe.mitre.org/data/definitions/79.html"]
|
|
12015
|
+
});
|
|
12016
|
+
}
|
|
12017
|
+
}
|
|
12018
|
+
}
|
|
12019
|
+
}
|
|
12020
|
+
return findings;
|
|
12021
|
+
}
|
|
12022
|
+
function getLineNumber3(content, index) {
|
|
12023
|
+
return content.slice(0, index).split("\n").length;
|
|
12024
|
+
}
|
|
12025
|
+
function truncateCode2(code, maxLength = 100) {
|
|
12026
|
+
if (code.length <= maxLength) return code;
|
|
12027
|
+
return code.slice(0, maxLength - 3) + "...";
|
|
12028
|
+
}
|
|
12029
|
+
function isInComment(line) {
|
|
12030
|
+
const trimmed = line.trim();
|
|
12031
|
+
return trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*");
|
|
12032
|
+
}
|
|
12033
|
+
|
|
12034
|
+
// src/tools/review-code/checks/architecture.ts
|
|
12035
|
+
var CSHARP_LAYER_PATTERNS = {
|
|
12036
|
+
domain: [
|
|
12037
|
+
/namespace\s+\w+\.Domain/i,
|
|
12038
|
+
/SmartStack\.Domain/i
|
|
12039
|
+
],
|
|
12040
|
+
application: [
|
|
12041
|
+
/namespace\s+\w+\.Application/i,
|
|
12042
|
+
/SmartStack\.Application/i
|
|
12043
|
+
],
|
|
12044
|
+
infrastructure: [
|
|
12045
|
+
/namespace\s+\w+\.Infrastructure/i,
|
|
12046
|
+
/SmartStack\.Infrastructure/i,
|
|
12047
|
+
/\.Persistence\./i,
|
|
12048
|
+
/DbContext/i
|
|
12049
|
+
],
|
|
12050
|
+
api: [
|
|
12051
|
+
/namespace\s+\w+\.Api/i,
|
|
12052
|
+
/SmartStack\.Api/i,
|
|
12053
|
+
/\[ApiController\]/i
|
|
12054
|
+
]
|
|
12055
|
+
};
|
|
12056
|
+
var CSHARP_IMPORT_PATTERNS = {
|
|
12057
|
+
domain: /using\s+[\w.]*\.Domain[.\w]*/gi,
|
|
12058
|
+
application: /using\s+[\w.]*\.Application[.\w]*/gi,
|
|
12059
|
+
infrastructure: /using\s+[\w.]*\.Infrastructure[.\w]*/gi,
|
|
12060
|
+
api: /using\s+[\w.]*\.Api[.\w]*/gi
|
|
12061
|
+
};
|
|
12062
|
+
var TS_LAYER_VIOLATIONS = [
|
|
12063
|
+
{
|
|
12064
|
+
pattern: /from\s+['"].*Infrastructure.*['"]/gi,
|
|
12065
|
+
layer: "infrastructure",
|
|
12066
|
+
message: "Web/React code should not import from Infrastructure layer"
|
|
12067
|
+
},
|
|
12068
|
+
{
|
|
12069
|
+
pattern: /from\s+['"].*\.Api.*['"]/gi,
|
|
12070
|
+
layer: "api",
|
|
12071
|
+
message: "Web/React code should not import from Api layer directly"
|
|
12072
|
+
}
|
|
12073
|
+
];
|
|
12074
|
+
var DIRECT_DB_PATTERNS = [
|
|
12075
|
+
{
|
|
12076
|
+
pattern: /private\s+readonly\s+\w*DbContext/gi,
|
|
12077
|
+
name: "Direct DbContext field"
|
|
12078
|
+
},
|
|
12079
|
+
{
|
|
12080
|
+
pattern: /new\s+\w*DbContext\s*\(/gi,
|
|
12081
|
+
name: "DbContext instantiation"
|
|
12082
|
+
},
|
|
12083
|
+
{
|
|
12084
|
+
pattern: /IApplicationDbContext|ICoreDbContext/gi,
|
|
12085
|
+
name: "DbContext interface",
|
|
12086
|
+
allowed: ["service", "repository", "handler"]
|
|
12087
|
+
}
|
|
12088
|
+
];
|
|
12089
|
+
async function checkArchitecture(context) {
|
|
12090
|
+
const findings = [];
|
|
12091
|
+
for (const file of context.files) {
|
|
12092
|
+
const lines = file.content.split("\n");
|
|
12093
|
+
if (file.language === "csharp") {
|
|
12094
|
+
const fileLayer = detectCsharpLayer(file.content, file.relativePath);
|
|
12095
|
+
if (fileLayer && LAYER_RULES[fileLayer]) {
|
|
12096
|
+
const rules = LAYER_RULES[fileLayer];
|
|
12097
|
+
for (const forbiddenLayer of rules.cannotImport) {
|
|
12098
|
+
const importPattern = CSHARP_IMPORT_PATTERNS[forbiddenLayer];
|
|
12099
|
+
if (!importPattern) continue;
|
|
12100
|
+
let match2;
|
|
12101
|
+
importPattern.lastIndex = 0;
|
|
12102
|
+
while ((match2 = importPattern.exec(file.content)) !== null) {
|
|
12103
|
+
const lineNumber = getLineNumber4(file.content, match2.index);
|
|
12104
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
12105
|
+
findings.push({
|
|
12106
|
+
id: generateFindingId("architecture"),
|
|
12107
|
+
category: "architecture",
|
|
12108
|
+
severity: "critical",
|
|
12109
|
+
title: `Layer Violation: ${capitalize2(fileLayer)} \u2192 ${capitalize2(forbiddenLayer)}`,
|
|
12110
|
+
description: `The ${capitalize2(fileLayer)} layer should not import from the ${capitalize2(forbiddenLayer)} layer. This violates Clean Architecture principles.`,
|
|
12111
|
+
file: file.relativePath,
|
|
12112
|
+
line: lineNumber,
|
|
12113
|
+
code: truncateCode3(lineContent),
|
|
12114
|
+
suggestion: `Remove this import. ${getSuggestionForViolation(fileLayer, forbiddenLayer)}`,
|
|
12115
|
+
autoFixable: false
|
|
12116
|
+
});
|
|
12117
|
+
}
|
|
12118
|
+
}
|
|
12119
|
+
}
|
|
12120
|
+
const isController = /Controller\.cs$/i.test(file.relativePath);
|
|
12121
|
+
if (isController) {
|
|
12122
|
+
for (const { pattern, name: _name } of DIRECT_DB_PATTERNS) {
|
|
12123
|
+
let match2;
|
|
12124
|
+
pattern.lastIndex = 0;
|
|
12125
|
+
while ((match2 = pattern.exec(file.content)) !== null) {
|
|
12126
|
+
const hasComplexQuery = /\.Include\(/.test(file.content) && /\.ThenInclude\(/.test(file.content);
|
|
12127
|
+
if (hasComplexQuery) {
|
|
12128
|
+
const lineNumber = getLineNumber4(file.content, match2.index);
|
|
12129
|
+
findings.push({
|
|
12130
|
+
id: generateFindingId("architecture"),
|
|
12131
|
+
category: "architecture",
|
|
12132
|
+
severity: "warning",
|
|
12133
|
+
title: "Complex Query in Controller",
|
|
12134
|
+
description: "Controllers with complex queries (multiple Includes) should consider moving logic to a service.",
|
|
12135
|
+
file: file.relativePath,
|
|
12136
|
+
line: lineNumber,
|
|
12137
|
+
suggestion: "Consider extracting complex query logic to an Application layer service.",
|
|
12138
|
+
autoFixable: false
|
|
12139
|
+
});
|
|
12140
|
+
break;
|
|
12141
|
+
}
|
|
12142
|
+
}
|
|
12143
|
+
}
|
|
12144
|
+
}
|
|
12145
|
+
const newServicePattern = /new\s+(\w+Service)\s*\(/gi;
|
|
12146
|
+
let match;
|
|
12147
|
+
while ((match = newServicePattern.exec(file.content)) !== null) {
|
|
12148
|
+
const lineNumber = getLineNumber4(file.content, match.index);
|
|
12149
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
12150
|
+
if (file.relativePath.includes("Test") || file.relativePath.includes(".test.")) continue;
|
|
12151
|
+
findings.push({
|
|
12152
|
+
id: generateFindingId("architecture"),
|
|
12153
|
+
category: "architecture",
|
|
12154
|
+
severity: "critical",
|
|
12155
|
+
title: "Service Instantiation Instead of DI",
|
|
12156
|
+
description: `Direct instantiation of ${match[1]} detected. Services should be injected via constructor, not instantiated with 'new'.`,
|
|
12157
|
+
file: file.relativePath,
|
|
12158
|
+
line: lineNumber,
|
|
12159
|
+
code: truncateCode3(lineContent),
|
|
12160
|
+
suggestion: "Inject the service interface via constructor dependency injection.",
|
|
12161
|
+
autoFixable: false
|
|
12162
|
+
});
|
|
12163
|
+
}
|
|
12164
|
+
}
|
|
12165
|
+
if (["typescript", "tsx"].includes(file.language) && file.layer === "web") {
|
|
12166
|
+
for (const { pattern, message } of TS_LAYER_VIOLATIONS) {
|
|
12167
|
+
let match;
|
|
12168
|
+
pattern.lastIndex = 0;
|
|
12169
|
+
while ((match = pattern.exec(file.content)) !== null) {
|
|
12170
|
+
const lineNumber = getLineNumber4(file.content, match.index);
|
|
12171
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
12172
|
+
findings.push({
|
|
12173
|
+
id: generateFindingId("architecture"),
|
|
12174
|
+
category: "architecture",
|
|
12175
|
+
severity: "critical",
|
|
12176
|
+
title: "Web Layer Violation",
|
|
12177
|
+
description: message,
|
|
12178
|
+
file: file.relativePath,
|
|
12179
|
+
line: lineNumber,
|
|
12180
|
+
code: truncateCode3(lineContent),
|
|
12181
|
+
suggestion: "Use API client services to communicate with the backend instead of direct imports.",
|
|
12182
|
+
autoFixable: false
|
|
12183
|
+
});
|
|
12184
|
+
}
|
|
12185
|
+
}
|
|
12186
|
+
}
|
|
12187
|
+
}
|
|
12188
|
+
return findings;
|
|
12189
|
+
}
|
|
12190
|
+
function detectCsharpLayer(content, filePath) {
|
|
12191
|
+
const pathLower = filePath.toLowerCase();
|
|
12192
|
+
if (pathLower.includes("/domain/") || pathLower.includes("\\domain\\")) return "domain";
|
|
12193
|
+
if (pathLower.includes("/application/") || pathLower.includes("\\application\\")) return "application";
|
|
12194
|
+
if (pathLower.includes("/infrastructure/") || pathLower.includes("\\infrastructure\\")) return "infrastructure";
|
|
12195
|
+
if (pathLower.includes("/api/") || pathLower.includes("\\api\\")) return "api";
|
|
12196
|
+
for (const [layer, patterns] of Object.entries(CSHARP_LAYER_PATTERNS)) {
|
|
12197
|
+
for (const pattern of patterns) {
|
|
12198
|
+
if (pattern.test(content)) {
|
|
12199
|
+
return layer;
|
|
12200
|
+
}
|
|
12201
|
+
}
|
|
12202
|
+
}
|
|
12203
|
+
return null;
|
|
12204
|
+
}
|
|
12205
|
+
function getSuggestionForViolation(fromLayer, toLayer) {
|
|
12206
|
+
if (fromLayer === "domain" && toLayer === "application") {
|
|
12207
|
+
return "Domain entities should not depend on Application layer. Move shared interfaces to Domain.";
|
|
12208
|
+
}
|
|
12209
|
+
if (fromLayer === "application" && toLayer === "infrastructure") {
|
|
12210
|
+
return "Define an interface in Application layer and implement it in Infrastructure.";
|
|
12211
|
+
}
|
|
12212
|
+
if (fromLayer === "api" && toLayer === "web") {
|
|
12213
|
+
return "API controllers should not depend on Web layer.";
|
|
12214
|
+
}
|
|
12215
|
+
if (toLayer === "infrastructure") {
|
|
12216
|
+
return "Use dependency injection with interfaces defined in Application layer.";
|
|
12217
|
+
}
|
|
12218
|
+
return "Refactor to respect Clean Architecture layer boundaries.";
|
|
12219
|
+
}
|
|
12220
|
+
function getLineNumber4(content, index) {
|
|
12221
|
+
return content.slice(0, index).split("\n").length;
|
|
12222
|
+
}
|
|
12223
|
+
function truncateCode3(code, maxLength = 100) {
|
|
12224
|
+
if (code.length <= maxLength) return code;
|
|
12225
|
+
return code.slice(0, maxLength - 3) + "...";
|
|
12226
|
+
}
|
|
12227
|
+
function capitalize2(str) {
|
|
12228
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
12229
|
+
}
|
|
12230
|
+
|
|
12231
|
+
// src/tools/review-code/checks/hardcoded-values.ts
|
|
12232
|
+
var ACCEPTABLE_NUMBERS = /* @__PURE__ */ new Set([
|
|
12233
|
+
0,
|
|
12234
|
+
1,
|
|
12235
|
+
-1,
|
|
12236
|
+
2,
|
|
12237
|
+
10,
|
|
12238
|
+
100,
|
|
12239
|
+
1e3,
|
|
12240
|
+
// Common sizes
|
|
12241
|
+
8,
|
|
12242
|
+
16,
|
|
12243
|
+
32,
|
|
12244
|
+
64,
|
|
12245
|
+
128,
|
|
12246
|
+
256,
|
|
12247
|
+
512,
|
|
12248
|
+
1024,
|
|
12249
|
+
// HTTP status codes (often used as comparisons)
|
|
12250
|
+
200,
|
|
12251
|
+
201,
|
|
12252
|
+
204,
|
|
12253
|
+
400,
|
|
12254
|
+
401,
|
|
12255
|
+
403,
|
|
12256
|
+
404,
|
|
12257
|
+
500
|
|
12258
|
+
]);
|
|
12259
|
+
var HARDCODED_URL_PATTERN = /["'](https?:\/\/(?!localhost)[^"']+)["']/gi;
|
|
12260
|
+
var HARDCODED_EMAIL_PATTERN = /["']([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})["']/gi;
|
|
12261
|
+
var HARDCODED_IP_PATTERN = /["'](\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})["']/gi;
|
|
12262
|
+
var TIMEOUT_PATTERNS = [
|
|
12263
|
+
{ pattern: /timeout\s*[=:]\s*(\d{4,})/gi, name: "Timeout value" },
|
|
12264
|
+
{ pattern: /TimeSpan\.FromSeconds\s*\(\s*(\d+)\s*\)/gi, name: "TimeSpan.FromSeconds" },
|
|
12265
|
+
{ pattern: /TimeSpan\.FromMinutes\s*\(\s*(\d+)\s*\)/gi, name: "TimeSpan.FromMinutes" },
|
|
12266
|
+
{ pattern: /setTimeout\s*\(\s*[^,]+,\s*(\d{4,})\s*\)/gi, name: "setTimeout" },
|
|
12267
|
+
{ pattern: /delay\s*[=:]\s*(\d{4,})/gi, name: "Delay value" }
|
|
12268
|
+
];
|
|
12269
|
+
var MAGIC_NUMBER_PATTERN = /(?<![a-zA-Z0-9_.])\b(\d+)\b(?![a-zA-Z0-9_])/g;
|
|
12270
|
+
var FEATURE_FLAG_PATTERNS = [
|
|
12271
|
+
{ pattern: /if\s*\(\s*(true|false)\s*\)/gi, name: "Hardcoded boolean condition" },
|
|
12272
|
+
{ pattern: /enabled\s*[=:]\s*(true|false)/gi, name: "Hardcoded enabled flag" }
|
|
12273
|
+
];
|
|
12274
|
+
async function checkHardcodedValues(context) {
|
|
12275
|
+
const findings = [];
|
|
12276
|
+
for (const file of context.files) {
|
|
12277
|
+
if (isConfigOrConstFile(file.relativePath)) continue;
|
|
12278
|
+
const lines = file.content.split("\n");
|
|
12279
|
+
let match;
|
|
12280
|
+
HARDCODED_URL_PATTERN.lastIndex = 0;
|
|
12281
|
+
while ((match = HARDCODED_URL_PATTERN.exec(file.content)) !== null) {
|
|
12282
|
+
const lineNumber = getLineNumber5(file.content, match.index);
|
|
12283
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
12284
|
+
if (isInComment2(lineContent) || isDocExample(lineContent)) continue;
|
|
12285
|
+
findings.push({
|
|
12286
|
+
id: generateFindingId("hardcoded-values"),
|
|
12287
|
+
category: "hardcoded-values",
|
|
12288
|
+
severity: "warning",
|
|
12289
|
+
title: "Hardcoded URL",
|
|
12290
|
+
description: `Found hardcoded URL: ${truncateCode4(match[1], 50)}. URLs should be in configuration.`,
|
|
12291
|
+
file: file.relativePath,
|
|
12292
|
+
line: lineNumber,
|
|
12293
|
+
code: truncateCode4(lineContent),
|
|
12294
|
+
suggestion: "Move this URL to appsettings.json, environment variables, or a configuration service.",
|
|
12295
|
+
autoFixable: false
|
|
12296
|
+
});
|
|
12297
|
+
}
|
|
12298
|
+
HARDCODED_EMAIL_PATTERN.lastIndex = 0;
|
|
12299
|
+
while ((match = HARDCODED_EMAIL_PATTERN.exec(file.content)) !== null) {
|
|
12300
|
+
const lineNumber = getLineNumber5(file.content, match.index);
|
|
12301
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
12302
|
+
if (isInComment2(lineContent) || isDocExample(lineContent)) continue;
|
|
12303
|
+
if (match[1].includes("@test") || match[1].includes("@example")) continue;
|
|
12304
|
+
findings.push({
|
|
12305
|
+
id: generateFindingId("hardcoded-values"),
|
|
12306
|
+
category: "hardcoded-values",
|
|
12307
|
+
severity: "warning",
|
|
12308
|
+
title: "Hardcoded Email",
|
|
12309
|
+
description: `Found hardcoded email: ${match[1]}. Consider using configuration.`,
|
|
12310
|
+
file: file.relativePath,
|
|
12311
|
+
line: lineNumber,
|
|
12312
|
+
code: truncateCode4(lineContent),
|
|
12313
|
+
suggestion: "Move this email to configuration or use a configurable notification service.",
|
|
12314
|
+
autoFixable: false
|
|
12315
|
+
});
|
|
12316
|
+
}
|
|
12317
|
+
HARDCODED_IP_PATTERN.lastIndex = 0;
|
|
12318
|
+
while ((match = HARDCODED_IP_PATTERN.exec(file.content)) !== null) {
|
|
12319
|
+
const lineNumber = getLineNumber5(file.content, match.index);
|
|
12320
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
12321
|
+
if (isInComment2(lineContent)) continue;
|
|
12322
|
+
if (match[1] === "127.0.0.1" || match[1] === "0.0.0.0") continue;
|
|
12323
|
+
findings.push({
|
|
12324
|
+
id: generateFindingId("hardcoded-values"),
|
|
12325
|
+
category: "hardcoded-values",
|
|
12326
|
+
severity: "warning",
|
|
12327
|
+
title: "Hardcoded IP Address",
|
|
12328
|
+
description: `Found hardcoded IP address: ${match[1]}. This should be configurable.`,
|
|
12329
|
+
file: file.relativePath,
|
|
12330
|
+
line: lineNumber,
|
|
12331
|
+
code: truncateCode4(lineContent),
|
|
12332
|
+
suggestion: "Move this IP address to configuration.",
|
|
12333
|
+
autoFixable: false
|
|
12334
|
+
});
|
|
12335
|
+
}
|
|
12336
|
+
for (const { pattern, name } of TIMEOUT_PATTERNS) {
|
|
12337
|
+
pattern.lastIndex = 0;
|
|
12338
|
+
while ((match = pattern.exec(file.content)) !== null) {
|
|
12339
|
+
const value = parseInt(match[1], 10);
|
|
12340
|
+
const lineNumber = getLineNumber5(file.content, match.index);
|
|
12341
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
12342
|
+
if (isInComment2(lineContent)) continue;
|
|
12343
|
+
if (value > 1e3 || value > 60 && name.includes("Minutes")) {
|
|
12344
|
+
findings.push({
|
|
12345
|
+
id: generateFindingId("hardcoded-values"),
|
|
12346
|
+
category: "hardcoded-values",
|
|
12347
|
+
severity: "warning",
|
|
12348
|
+
title: "Hardcoded Timeout",
|
|
12349
|
+
description: `${name} with hardcoded value ${value}. Consider making timeouts configurable.`,
|
|
12350
|
+
file: file.relativePath,
|
|
12351
|
+
line: lineNumber,
|
|
12352
|
+
code: truncateCode4(lineContent),
|
|
12353
|
+
suggestion: "Extract timeout values to configuration for easier adjustment in different environments.",
|
|
12354
|
+
autoFixable: false
|
|
12355
|
+
});
|
|
12356
|
+
}
|
|
12357
|
+
}
|
|
12358
|
+
}
|
|
12359
|
+
if (isBusinessLogicFile(file.relativePath) && file.language === "csharp") {
|
|
12360
|
+
checkMagicNumbers(file.content, file.relativePath, lines, findings);
|
|
12361
|
+
}
|
|
12362
|
+
for (const { pattern, name } of FEATURE_FLAG_PATTERNS) {
|
|
12363
|
+
pattern.lastIndex = 0;
|
|
12364
|
+
while ((match = pattern.exec(file.content)) !== null) {
|
|
12365
|
+
const lineNumber = getLineNumber5(file.content, match.index);
|
|
12366
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
12367
|
+
if (isInComment2(lineContent)) continue;
|
|
12368
|
+
if (lineContent.includes("DEBUG") || lineContent.includes("TEST")) continue;
|
|
12369
|
+
findings.push({
|
|
12370
|
+
id: generateFindingId("hardcoded-values"),
|
|
12371
|
+
category: "hardcoded-values",
|
|
12372
|
+
severity: "warning",
|
|
12373
|
+
title: "Hardcoded Feature Flag",
|
|
12374
|
+
description: `${name} detected. Feature toggles should be configurable, not hardcoded.`,
|
|
12375
|
+
file: file.relativePath,
|
|
12376
|
+
line: lineNumber,
|
|
12377
|
+
code: truncateCode4(lineContent),
|
|
12378
|
+
suggestion: "Use a feature flag system or configuration service instead of hardcoded boolean values.",
|
|
12379
|
+
autoFixable: false
|
|
12380
|
+
});
|
|
12381
|
+
}
|
|
12382
|
+
}
|
|
12383
|
+
}
|
|
12384
|
+
return findings;
|
|
12385
|
+
}
|
|
12386
|
+
function checkMagicNumbers(content, filePath, lines, findings) {
|
|
12387
|
+
const magicNumbersFound = /* @__PURE__ */ new Map();
|
|
12388
|
+
let match;
|
|
12389
|
+
MAGIC_NUMBER_PATTERN.lastIndex = 0;
|
|
12390
|
+
while ((match = MAGIC_NUMBER_PATTERN.exec(content)) !== null) {
|
|
12391
|
+
const value = parseInt(match[1], 10);
|
|
12392
|
+
const lineNumber = getLineNumber5(content, match.index);
|
|
12393
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
12394
|
+
if (ACCEPTABLE_NUMBERS.has(value)) continue;
|
|
12395
|
+
if (isInComment2(lineContent)) continue;
|
|
12396
|
+
if (/const\s+\w+\s*=/.test(lineContent) || /readonly\s+\w+\s*=/.test(lineContent)) continue;
|
|
12397
|
+
if (/\[\s*\d+\s*\]/.test(lineContent)) continue;
|
|
12398
|
+
if (!magicNumbersFound.has(value)) {
|
|
12399
|
+
magicNumbersFound.set(value, []);
|
|
12400
|
+
}
|
|
12401
|
+
magicNumbersFound.get(value).push(lineNumber);
|
|
12402
|
+
}
|
|
12403
|
+
for (const [value, lineNumbers] of magicNumbersFound) {
|
|
12404
|
+
if (lineNumbers.length >= 2) {
|
|
12405
|
+
findings.push({
|
|
12406
|
+
id: generateFindingId("hardcoded-values"),
|
|
12407
|
+
category: "hardcoded-values",
|
|
12408
|
+
severity: "warning",
|
|
12409
|
+
title: "Magic Number",
|
|
12410
|
+
description: `Magic number ${value} appears ${lineNumbers.length} times. Consider extracting to a named constant.`,
|
|
12411
|
+
file: filePath,
|
|
12412
|
+
line: lineNumbers[0],
|
|
12413
|
+
suggestion: `Extract ${value} to a well-named constant that explains its purpose.`,
|
|
12414
|
+
autoFixable: true
|
|
12415
|
+
});
|
|
12416
|
+
}
|
|
12417
|
+
}
|
|
12418
|
+
}
|
|
12419
|
+
function isConfigOrConstFile(filePath) {
|
|
12420
|
+
const lower = filePath.toLowerCase();
|
|
12421
|
+
return lower.includes("config") || lower.includes("constant") || lower.includes("settings") || lower.includes("appsettings") || lower.includes(".json") || lower.includes("test") || lower.includes(".spec.") || lower.includes(".test.");
|
|
12422
|
+
}
|
|
12423
|
+
function isBusinessLogicFile(filePath) {
|
|
12424
|
+
const lower = filePath.toLowerCase();
|
|
12425
|
+
return lower.includes("service") || lower.includes("handler") || lower.includes("controller") || lower.includes("validator");
|
|
12426
|
+
}
|
|
12427
|
+
function isDocExample(line) {
|
|
12428
|
+
if (line.includes("/// ") || line.includes("* ")) return true;
|
|
12429
|
+
const hasExampleKeyword = /\bexample\b/i.test(line);
|
|
12430
|
+
const isExampleInUrl = /example\.(com|org|net)/i.test(line);
|
|
12431
|
+
return hasExampleKeyword && !isExampleInUrl;
|
|
12432
|
+
}
|
|
12433
|
+
function getLineNumber5(content, index) {
|
|
12434
|
+
return content.slice(0, index).split("\n").length;
|
|
12435
|
+
}
|
|
12436
|
+
function truncateCode4(code, maxLength = 100) {
|
|
12437
|
+
if (code.length <= maxLength) return code;
|
|
12438
|
+
return code.slice(0, maxLength - 3) + "...";
|
|
12439
|
+
}
|
|
12440
|
+
function isInComment2(line) {
|
|
12441
|
+
const trimmed = line.trim();
|
|
12442
|
+
return trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*");
|
|
12443
|
+
}
|
|
12444
|
+
|
|
12445
|
+
// src/tools/review-code/checks/tests.ts
|
|
12446
|
+
var ENTITY_PATTERN = /public\s+class\s+(\w+)\s*:\s*(Base|System)Entity/gi;
|
|
12447
|
+
var SERVICE_PATTERN = /public\s+class\s+(\w+Service)\s*:\s*I\w+Service/gi;
|
|
12448
|
+
var CONTROLLER_PATTERN = /public\s+class\s+(\w+Controller)\s*:\s*ControllerBase/gi;
|
|
12449
|
+
var VALIDATOR_PATTERN = /public\s+class\s+(\w+Validator)\s*:\s*AbstractValidator/gi;
|
|
12450
|
+
var USELESS_ASSERTION_PATTERNS = [
|
|
12451
|
+
/Assert\.True\s*\(\s*true\s*\)/gi,
|
|
12452
|
+
/Assert\.False\s*\(\s*false\s*\)/gi,
|
|
12453
|
+
/\.Should\(\)\.BeTrue\(\)/gi,
|
|
12454
|
+
/expect\s*\(\s*true\s*\)\s*\.toBe\s*\(\s*true\s*\)/gi,
|
|
12455
|
+
/expect\s*\(\s*1\s*\)\s*\.toBe\s*\(\s*1\s*\)/gi
|
|
12456
|
+
];
|
|
12457
|
+
async function checkTests(context) {
|
|
12458
|
+
const findings = [];
|
|
12459
|
+
const entities = /* @__PURE__ */ new Set();
|
|
12460
|
+
const services = /* @__PURE__ */ new Set();
|
|
12461
|
+
const controllers = /* @__PURE__ */ new Set();
|
|
12462
|
+
const validators = /* @__PURE__ */ new Set();
|
|
12463
|
+
const existingTests = /* @__PURE__ */ new Set();
|
|
12464
|
+
for (const file of context.files) {
|
|
12465
|
+
if (file.language !== "csharp") continue;
|
|
12466
|
+
let match;
|
|
12467
|
+
ENTITY_PATTERN.lastIndex = 0;
|
|
12468
|
+
while ((match = ENTITY_PATTERN.exec(file.content)) !== null) {
|
|
12469
|
+
entities.add(match[1]);
|
|
12470
|
+
}
|
|
12471
|
+
SERVICE_PATTERN.lastIndex = 0;
|
|
12472
|
+
while ((match = SERVICE_PATTERN.exec(file.content)) !== null) {
|
|
12473
|
+
services.add(match[1]);
|
|
12474
|
+
}
|
|
12475
|
+
CONTROLLER_PATTERN.lastIndex = 0;
|
|
12476
|
+
while ((match = CONTROLLER_PATTERN.exec(file.content)) !== null) {
|
|
12477
|
+
controllers.add(match[1]);
|
|
12478
|
+
}
|
|
12479
|
+
VALIDATOR_PATTERN.lastIndex = 0;
|
|
12480
|
+
while ((match = VALIDATOR_PATTERN.exec(file.content)) !== null) {
|
|
12481
|
+
validators.add(match[1]);
|
|
12482
|
+
}
|
|
12483
|
+
if (file.relativePath.includes("Tests") || file.relativePath.includes(".test.")) {
|
|
12484
|
+
const testClassMatch = /public\s+class\s+(\w+)Tests/gi.exec(file.content);
|
|
12485
|
+
if (testClassMatch) {
|
|
12486
|
+
existingTests.add(testClassMatch[1]);
|
|
12487
|
+
}
|
|
12488
|
+
checkTestQuality(file, findings);
|
|
12489
|
+
}
|
|
12490
|
+
}
|
|
12491
|
+
checkMissingTests(entities, existingTests, "Entity", findings);
|
|
12492
|
+
checkMissingTests(services, existingTests, "Service", findings);
|
|
12493
|
+
checkMissingTests(controllers, existingTests, "Controller", findings);
|
|
12494
|
+
checkMissingTests(validators, existingTests, "Validator", findings);
|
|
12495
|
+
return findings;
|
|
12496
|
+
}
|
|
12497
|
+
function checkMissingTests(components, existingTests, type, findings) {
|
|
12498
|
+
for (const component of components) {
|
|
12499
|
+
const baseName = component.replace(/(Service|Controller|Validator)$/, "");
|
|
12500
|
+
const hasTest = existingTests.has(component) || existingTests.has(baseName) || existingTests.has(`${baseName}${type}`);
|
|
12501
|
+
if (!hasTest) {
|
|
12502
|
+
const priority = getPriority(type);
|
|
12503
|
+
findings.push({
|
|
12504
|
+
id: generateFindingId("tests"),
|
|
12505
|
+
category: "tests",
|
|
12506
|
+
severity: priority === "critical" ? "critical" : "warning",
|
|
12507
|
+
title: `Missing Tests for ${component}`,
|
|
12508
|
+
description: `No test file found for ${type.toLowerCase()} ${component}. ${type}s should have comprehensive test coverage.`,
|
|
12509
|
+
file: `Tests/Unit/${type}s/${component}Tests.cs`,
|
|
12510
|
+
suggestion: `Create a test file: Tests/Unit/${type}s/${component}Tests.cs`,
|
|
12511
|
+
autoFixable: false
|
|
12512
|
+
});
|
|
12513
|
+
}
|
|
12514
|
+
}
|
|
12515
|
+
}
|
|
12516
|
+
function checkTestQuality(file, findings) {
|
|
12517
|
+
for (const pattern of USELESS_ASSERTION_PATTERNS) {
|
|
12518
|
+
let match;
|
|
12519
|
+
pattern.lastIndex = 0;
|
|
12520
|
+
while ((match = pattern.exec(file.content)) !== null) {
|
|
12521
|
+
const lineNumber = getLineNumber6(file.content, match.index);
|
|
12522
|
+
findings.push({
|
|
12523
|
+
id: generateFindingId("tests"),
|
|
12524
|
+
category: "tests",
|
|
12525
|
+
severity: "warning",
|
|
12526
|
+
title: "Useless Test Assertion",
|
|
12527
|
+
description: "This assertion always passes and doesn't test any behavior.",
|
|
12528
|
+
file: file.relativePath,
|
|
12529
|
+
line: lineNumber,
|
|
12530
|
+
code: truncateCode5(match[0]),
|
|
12531
|
+
suggestion: "Replace with meaningful assertions that verify actual behavior.",
|
|
12532
|
+
autoFixable: false
|
|
12533
|
+
});
|
|
12534
|
+
}
|
|
12535
|
+
}
|
|
12536
|
+
const factMatches = file.content.matchAll(/\[Fact\]\s*\n\s*public\s+(?:async\s+)?(?:Task|void)\s+(\w+)/gi);
|
|
12537
|
+
for (const match of factMatches) {
|
|
12538
|
+
const methodName = match[1];
|
|
12539
|
+
const methodStart = match.index || 0;
|
|
12540
|
+
const bodyStart = file.content.indexOf("{", methodStart);
|
|
12541
|
+
const bodyEnd = findMatchingBrace(file.content, bodyStart);
|
|
12542
|
+
if (bodyStart !== -1 && bodyEnd !== -1) {
|
|
12543
|
+
const methodBody = file.content.slice(bodyStart, bodyEnd);
|
|
12544
|
+
const hasAssertion = /Assert\./i.test(methodBody) || /\.Should\(/i.test(methodBody) || /expect\s*\(/i.test(methodBody) || /Verify\s*\(/i.test(methodBody);
|
|
12545
|
+
if (!hasAssertion) {
|
|
12546
|
+
const lineNumber = getLineNumber6(file.content, methodStart);
|
|
12547
|
+
findings.push({
|
|
12548
|
+
id: generateFindingId("tests"),
|
|
12549
|
+
category: "tests",
|
|
12550
|
+
severity: "warning",
|
|
12551
|
+
title: "Test Without Assertions",
|
|
12552
|
+
description: `Test method ${methodName} does not appear to have any assertions.`,
|
|
12553
|
+
file: file.relativePath,
|
|
12554
|
+
line: lineNumber,
|
|
12555
|
+
suggestion: "Add assertions to verify the expected behavior. A test without assertions doesn't prove anything.",
|
|
12556
|
+
autoFixable: false
|
|
12557
|
+
});
|
|
12558
|
+
}
|
|
12559
|
+
}
|
|
12560
|
+
}
|
|
12561
|
+
}
|
|
12562
|
+
function getPriority(type) {
|
|
12563
|
+
switch (type) {
|
|
12564
|
+
case "Service":
|
|
12565
|
+
return "critical";
|
|
12566
|
+
case "Controller":
|
|
12567
|
+
return "high";
|
|
12568
|
+
case "Validator":
|
|
12569
|
+
return "high";
|
|
12570
|
+
case "Entity":
|
|
12571
|
+
return "medium";
|
|
12572
|
+
default:
|
|
12573
|
+
return "low";
|
|
12574
|
+
}
|
|
12575
|
+
}
|
|
12576
|
+
function findMatchingBrace(content, start) {
|
|
12577
|
+
let depth = 0;
|
|
12578
|
+
for (let i = start; i < content.length; i++) {
|
|
12579
|
+
if (content[i] === "{") depth++;
|
|
12580
|
+
if (content[i] === "}") {
|
|
12581
|
+
depth--;
|
|
12582
|
+
if (depth === 0) return i;
|
|
12583
|
+
}
|
|
12584
|
+
}
|
|
12585
|
+
return -1;
|
|
12586
|
+
}
|
|
12587
|
+
function getLineNumber6(content, index) {
|
|
12588
|
+
return content.slice(0, index).split("\n").length;
|
|
12589
|
+
}
|
|
12590
|
+
function truncateCode5(code, maxLength = 100) {
|
|
12591
|
+
if (code.length <= maxLength) return code;
|
|
12592
|
+
return code.slice(0, maxLength - 3) + "...";
|
|
12593
|
+
}
|
|
12594
|
+
|
|
12595
|
+
// src/tools/review-code/checks/ai-hallucinations.ts
|
|
12596
|
+
var FAKE_DOTNET_NAMESPACES = [
|
|
12597
|
+
"System.AI",
|
|
12598
|
+
"System.Machine",
|
|
12599
|
+
"Microsoft.AI.OpenAI",
|
|
12600
|
+
"Microsoft.Extensions.AI",
|
|
12601
|
+
"Azure.AI.TextAnalytics",
|
|
12602
|
+
// Often confused
|
|
12603
|
+
"System.Data.Linq"
|
|
12604
|
+
// Obsolete
|
|
12605
|
+
];
|
|
12606
|
+
var FAKE_NPM_PACKAGES = [
|
|
12607
|
+
"@react/router",
|
|
12608
|
+
// Should be react-router-dom
|
|
12609
|
+
"@microsoft/auth",
|
|
12610
|
+
// Various incorrect forms
|
|
12611
|
+
"react-query/v4",
|
|
12612
|
+
// Incorrect path
|
|
12613
|
+
"@tanstack/query"
|
|
12614
|
+
// Should be @tanstack/react-query
|
|
12615
|
+
];
|
|
12616
|
+
var AI_CODE_PATTERNS = [
|
|
12617
|
+
{
|
|
12618
|
+
pattern: /\/\/\s*TODO:\s*implement/gi,
|
|
12619
|
+
name: "Unimplemented TODO",
|
|
12620
|
+
description: "AI-generated placeholder that was not completed"
|
|
12621
|
+
},
|
|
12622
|
+
{
|
|
12623
|
+
pattern: /throw\s+new\s+NotImplementedException\s*\(\s*\)/gi,
|
|
12624
|
+
name: "NotImplementedException",
|
|
12625
|
+
description: "Method stub that was not implemented"
|
|
12626
|
+
},
|
|
12627
|
+
{
|
|
12628
|
+
pattern: /\/\/\s*\.\.\./gi,
|
|
12629
|
+
name: "Ellipsis comment",
|
|
12630
|
+
description: "AI placeholder comment indicating incomplete code"
|
|
12631
|
+
},
|
|
12632
|
+
{
|
|
12633
|
+
pattern: /\/\*\s*\.\.\.\s*\*\//gi,
|
|
12634
|
+
name: "Ellipsis block comment",
|
|
12635
|
+
description: "AI placeholder indicating incomplete code"
|
|
12636
|
+
}
|
|
12637
|
+
];
|
|
12638
|
+
var FAKE_METHODS = {
|
|
12639
|
+
string: ["IsEmpty", "IsNotEmpty", "ToInt", "ToDouble", "SafeTrim"],
|
|
12640
|
+
List: ["AddIfNotExists", "SafeAdd", "GetOrDefault", "FindOrCreate"],
|
|
12641
|
+
Dictionary: ["GetOrAdd", "TryAdd", "SafeGet"],
|
|
12642
|
+
// Some exist in ConcurrentDictionary
|
|
12643
|
+
Task: ["WaitAll", "WhenAny"],
|
|
12644
|
+
// Should be Task.WaitAll, Task.WhenAny (static)
|
|
12645
|
+
DateTime: ["ToISOString", "FromISOString"]
|
|
12646
|
+
// JavaScript confusion
|
|
12647
|
+
};
|
|
12648
|
+
var CSHARP_USING_PATTERN = /using\s+([\w.]+);/gi;
|
|
12649
|
+
var TS_IMPORT_PATTERN = /import\s+.*\s+from\s+['"]([^'"]+)['"]/gi;
|
|
12650
|
+
async function checkAiHallucinations(context) {
|
|
12651
|
+
const findings = [];
|
|
12652
|
+
const existingFiles = new Set(context.files.map((f) => f.relativePath.toLowerCase()));
|
|
12653
|
+
const existingModules = /* @__PURE__ */ new Set();
|
|
12654
|
+
for (const file of context.files) {
|
|
12655
|
+
if (file.language === "csharp") {
|
|
12656
|
+
const namespaceMatch = /namespace\s+([\w.]+)/gi.exec(file.content);
|
|
12657
|
+
if (namespaceMatch) {
|
|
12658
|
+
existingModules.add(namespaceMatch[1].toLowerCase());
|
|
12659
|
+
}
|
|
12660
|
+
}
|
|
12661
|
+
}
|
|
12662
|
+
for (const file of context.files) {
|
|
12663
|
+
const lines = file.content.split("\n");
|
|
12664
|
+
for (const { pattern, name, description } of AI_CODE_PATTERNS) {
|
|
12665
|
+
let match;
|
|
12666
|
+
pattern.lastIndex = 0;
|
|
12667
|
+
while ((match = pattern.exec(file.content)) !== null) {
|
|
12668
|
+
const lineNumber = getLineNumber7(file.content, match.index);
|
|
12669
|
+
findings.push({
|
|
12670
|
+
id: generateFindingId("ai-hallucinations"),
|
|
12671
|
+
category: "ai-hallucinations",
|
|
12672
|
+
severity: "critical",
|
|
12673
|
+
title: `Incomplete Code: ${name}`,
|
|
12674
|
+
description,
|
|
12675
|
+
file: file.relativePath,
|
|
12676
|
+
line: lineNumber,
|
|
12677
|
+
code: truncateCode6(lines[lineNumber - 1]?.trim() || ""),
|
|
12678
|
+
suggestion: "Complete the implementation or remove the placeholder.",
|
|
12679
|
+
autoFixable: false
|
|
12680
|
+
});
|
|
12681
|
+
}
|
|
12682
|
+
}
|
|
12683
|
+
if (file.language === "csharp") {
|
|
12684
|
+
let match;
|
|
12685
|
+
CSHARP_USING_PATTERN.lastIndex = 0;
|
|
12686
|
+
while ((match = CSHARP_USING_PATTERN.exec(file.content)) !== null) {
|
|
12687
|
+
const namespace = match[1];
|
|
12688
|
+
for (const fakeNs of FAKE_DOTNET_NAMESPACES) {
|
|
12689
|
+
if (namespace.startsWith(fakeNs)) {
|
|
12690
|
+
const lineNumber = getLineNumber7(file.content, match.index);
|
|
12691
|
+
findings.push({
|
|
12692
|
+
id: generateFindingId("ai-hallucinations"),
|
|
12693
|
+
category: "ai-hallucinations",
|
|
12694
|
+
severity: "blocking",
|
|
12695
|
+
title: "Non-existent Namespace",
|
|
12696
|
+
description: `The namespace '${namespace}' does not exist in .NET. This may be AI-hallucinated code.`,
|
|
12697
|
+
file: file.relativePath,
|
|
12698
|
+
line: lineNumber,
|
|
12699
|
+
code: truncateCode6(match[0]),
|
|
12700
|
+
suggestion: "Verify this namespace exists. Check NuGet packages or use the correct namespace.",
|
|
12701
|
+
autoFixable: false
|
|
12702
|
+
});
|
|
12703
|
+
}
|
|
12704
|
+
}
|
|
12705
|
+
}
|
|
12706
|
+
for (const [typeName, fakeMethods] of Object.entries(FAKE_METHODS)) {
|
|
12707
|
+
for (const fakeMethod of fakeMethods) {
|
|
12708
|
+
const methodPattern = new RegExp(`\\.${fakeMethod}\\s*\\(`, "gi");
|
|
12709
|
+
let methodMatch;
|
|
12710
|
+
while ((methodMatch = methodPattern.exec(file.content)) !== null) {
|
|
12711
|
+
const lineNumber = getLineNumber7(file.content, methodMatch.index);
|
|
12712
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
12713
|
+
const contextStart = Math.max(0, methodMatch.index - 50);
|
|
12714
|
+
const context2 = file.content.slice(contextStart, methodMatch.index).toLowerCase();
|
|
12715
|
+
if (context2.includes(typeName.toLowerCase()) || typeName === "string") {
|
|
12716
|
+
findings.push({
|
|
12717
|
+
id: generateFindingId("ai-hallucinations"),
|
|
12718
|
+
category: "ai-hallucinations",
|
|
12719
|
+
severity: "blocking",
|
|
12720
|
+
title: `Non-existent Method: ${fakeMethod}`,
|
|
12721
|
+
description: `The method '${fakeMethod}' does not exist on ${typeName} in .NET. This is likely AI-hallucinated code.`,
|
|
12722
|
+
file: file.relativePath,
|
|
12723
|
+
line: lineNumber,
|
|
12724
|
+
code: truncateCode6(lineContent),
|
|
12725
|
+
suggestion: `Use the correct .NET method. For example: string.IsNullOrEmpty() instead of IsEmpty().`,
|
|
12726
|
+
autoFixable: false
|
|
12727
|
+
});
|
|
12728
|
+
}
|
|
12729
|
+
}
|
|
12730
|
+
}
|
|
12731
|
+
}
|
|
12732
|
+
}
|
|
12733
|
+
if (["typescript", "javascript", "tsx", "jsx"].includes(file.language)) {
|
|
12734
|
+
let match;
|
|
12735
|
+
TS_IMPORT_PATTERN.lastIndex = 0;
|
|
12736
|
+
while ((match = TS_IMPORT_PATTERN.exec(file.content)) !== null) {
|
|
12737
|
+
const importPath = match[1];
|
|
12738
|
+
for (const fakePkg of FAKE_NPM_PACKAGES) {
|
|
12739
|
+
if (importPath === fakePkg || importPath.startsWith(fakePkg + "/")) {
|
|
12740
|
+
const lineNumber = getLineNumber7(file.content, match.index);
|
|
12741
|
+
findings.push({
|
|
12742
|
+
id: generateFindingId("ai-hallucinations"),
|
|
12743
|
+
category: "ai-hallucinations",
|
|
12744
|
+
severity: "blocking",
|
|
12745
|
+
title: "Non-existent Package",
|
|
12746
|
+
description: `The package '${importPath}' does not exist. This may be AI-hallucinated code.`,
|
|
12747
|
+
file: file.relativePath,
|
|
12748
|
+
line: lineNumber,
|
|
12749
|
+
code: truncateCode6(lines[lineNumber - 1]?.trim() || ""),
|
|
12750
|
+
suggestion: "Verify this package exists on npm. Check the correct package name.",
|
|
12751
|
+
autoFixable: false
|
|
12752
|
+
});
|
|
12753
|
+
}
|
|
12754
|
+
}
|
|
12755
|
+
if (importPath.startsWith("./") || importPath.startsWith("../")) {
|
|
12756
|
+
const normalizedPath = resolveRelativePath(file.relativePath, importPath);
|
|
12757
|
+
const possibleExtensions = [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
12758
|
+
const exists = possibleExtensions.some(
|
|
12759
|
+
(ext) => existingFiles.has(normalizedPath.toLowerCase() + ext) || existingFiles.has(normalizedPath.toLowerCase())
|
|
12760
|
+
);
|
|
12761
|
+
if (!exists && !importPath.includes("*")) {
|
|
12762
|
+
const lineNumber = getLineNumber7(file.content, match.index);
|
|
12763
|
+
findings.push({
|
|
12764
|
+
id: generateFindingId("ai-hallucinations"),
|
|
12765
|
+
category: "ai-hallucinations",
|
|
12766
|
+
severity: "critical",
|
|
12767
|
+
title: "Import Path Not Found",
|
|
12768
|
+
description: `The import path '${importPath}' does not resolve to an existing file.`,
|
|
12769
|
+
file: file.relativePath,
|
|
12770
|
+
line: lineNumber,
|
|
12771
|
+
code: truncateCode6(lines[lineNumber - 1]?.trim() || ""),
|
|
12772
|
+
suggestion: "Verify the file exists or fix the import path.",
|
|
12773
|
+
autoFixable: false
|
|
12774
|
+
});
|
|
12775
|
+
}
|
|
12776
|
+
}
|
|
12777
|
+
}
|
|
12778
|
+
}
|
|
12779
|
+
}
|
|
12780
|
+
return findings;
|
|
12781
|
+
}
|
|
12782
|
+
function resolveRelativePath(fromFile, importPath) {
|
|
12783
|
+
fromFile = fromFile.replace(/\\/g, "/");
|
|
12784
|
+
importPath = importPath.replace(/\\/g, "/");
|
|
12785
|
+
const fromDir = fromFile.substring(0, fromFile.lastIndexOf("/"));
|
|
12786
|
+
const parts = fromDir.split("/");
|
|
12787
|
+
for (const segment of importPath.split("/")) {
|
|
12788
|
+
if (segment === "..") {
|
|
12789
|
+
parts.pop();
|
|
12790
|
+
} else if (segment !== ".") {
|
|
12791
|
+
parts.push(segment);
|
|
12792
|
+
}
|
|
12793
|
+
}
|
|
12794
|
+
return parts.join("/");
|
|
12795
|
+
}
|
|
12796
|
+
function getLineNumber7(content, index) {
|
|
12797
|
+
return content.slice(0, index).split("\n").length;
|
|
12798
|
+
}
|
|
12799
|
+
function truncateCode6(code, maxLength = 100) {
|
|
12800
|
+
if (code.length <= maxLength) return code;
|
|
12801
|
+
return code.slice(0, maxLength - 3) + "...";
|
|
12802
|
+
}
|
|
12803
|
+
|
|
12804
|
+
// src/tools/review-code/checks/performance.ts
|
|
12805
|
+
var N_PLUS_ONE_PATTERNS = [
|
|
12806
|
+
{
|
|
12807
|
+
// Query inside foreach/for loop
|
|
12808
|
+
pattern: /(?:foreach|for)\s*\([^)]+\)\s*\{[^}]*(?:await\s+)?_context\.\w+\./gi,
|
|
12809
|
+
name: "Query in loop",
|
|
12810
|
+
description: "Database query inside a loop can cause N+1 performance issues"
|
|
12811
|
+
},
|
|
12812
|
+
{
|
|
12813
|
+
// Nested Select with query
|
|
12814
|
+
pattern: /\.Select\s*\([^)]*=>[^)]*\.(?:First|Single|Where|Any)\s*\(/gi,
|
|
12815
|
+
name: "Nested query in Select",
|
|
12816
|
+
description: "Nested query inside Select projection causes N+1"
|
|
12817
|
+
},
|
|
12818
|
+
{
|
|
12819
|
+
// Lazy loading trigger
|
|
12820
|
+
pattern: /(?:foreach|for)\s*\([^)]+\)\s*\{[^}]*\.\w+Navigation\./gi,
|
|
12821
|
+
name: "Navigation property in loop",
|
|
12822
|
+
description: "Accessing navigation property in loop triggers lazy loading"
|
|
12823
|
+
}
|
|
12824
|
+
];
|
|
12825
|
+
var OVER_FETCHING_PATTERNS = [
|
|
12826
|
+
{
|
|
12827
|
+
// Multiple Include calls
|
|
12828
|
+
pattern: /\.Include\s*\([^)]+\)\s*\.Include\s*\([^)]+\)\s*\.Include\s*\([^)]+\)/gi,
|
|
12829
|
+
name: "Triple Include",
|
|
12830
|
+
description: "Too many Include calls can load excessive data"
|
|
12831
|
+
},
|
|
12832
|
+
{
|
|
12833
|
+
// ToList before Where
|
|
12834
|
+
pattern: /\.ToList(?:Async)?\s*\(\s*\)\s*\.(?:Where|Select|OrderBy)/gi,
|
|
12835
|
+
name: "ToList before filtering",
|
|
12836
|
+
description: "ToList() before filtering loads all records into memory"
|
|
12837
|
+
},
|
|
12838
|
+
{
|
|
12839
|
+
// GetAll without pagination
|
|
12840
|
+
pattern: /GetAll(?:Async)?\s*\(\s*\)/gi,
|
|
12841
|
+
name: "GetAll without pagination",
|
|
12842
|
+
description: "Fetching all records without pagination can cause memory issues"
|
|
12843
|
+
}
|
|
12844
|
+
];
|
|
12845
|
+
var FRONTEND_PERF_PATTERNS = [
|
|
12846
|
+
{
|
|
12847
|
+
// Multiple fetch in useEffect
|
|
12848
|
+
pattern: /useEffect\s*\(\s*(?:async\s*)?\(\s*\)\s*=>\s*\{[^}]*fetch[^}]*fetch/gi,
|
|
12849
|
+
name: "Multiple fetches in useEffect",
|
|
12850
|
+
description: "Multiple sequential API calls in useEffect should be combined or parallelized"
|
|
12851
|
+
},
|
|
12852
|
+
{
|
|
12853
|
+
// Fetch without dependency array
|
|
12854
|
+
pattern: /useEffect\s*\(\s*(?:async\s*)?\(\s*\)\s*=>\s*\{[^}]*(?:fetch|axios|api\.)[^}]*\}\s*\)/gi,
|
|
12855
|
+
name: "Fetch without dependencies",
|
|
12856
|
+
description: "API call in useEffect without dependency array runs on every render"
|
|
12857
|
+
},
|
|
12858
|
+
{
|
|
12859
|
+
// setState in loop
|
|
12860
|
+
pattern: /(?:for|while|forEach)\s*\([^)]*\)[^{]*\{[^}]*set\w+\s*\(/gi,
|
|
12861
|
+
name: "setState in loop",
|
|
12862
|
+
description: "Calling setState in a loop causes multiple re-renders"
|
|
12863
|
+
},
|
|
12864
|
+
{
|
|
12865
|
+
// Large inline objects in JSX
|
|
12866
|
+
pattern: /style\s*=\s*\{\s*\{[^}]{100,}\}/gi,
|
|
12867
|
+
name: "Large inline style object",
|
|
12868
|
+
description: "Large inline objects create new references on each render"
|
|
12869
|
+
}
|
|
12870
|
+
];
|
|
12871
|
+
var MISSING_OPTIMIZATION_PATTERNS = [
|
|
12872
|
+
{
|
|
12873
|
+
// No pagination in list queries
|
|
12874
|
+
pattern: /\.ToListAsync\s*\(\s*\)/gi,
|
|
12875
|
+
contextCheck: (content, index) => {
|
|
12876
|
+
const context = content.slice(Math.max(0, index - 200), index);
|
|
12877
|
+
return !context.includes("Take") && !context.includes("Skip") && !context.includes("PageSize");
|
|
12878
|
+
},
|
|
12879
|
+
name: "List query without pagination",
|
|
12880
|
+
description: "Large list queries should use pagination"
|
|
12881
|
+
},
|
|
12882
|
+
{
|
|
12883
|
+
// No AsNoTracking for read-only queries
|
|
12884
|
+
pattern: /\.(?:Where|Select|Include)[^;]*\.ToListAsync/gi,
|
|
12885
|
+
contextCheck: (content, index) => {
|
|
12886
|
+
const context = content.slice(Math.max(0, index - 300), index + 100);
|
|
12887
|
+
return !context.includes("AsNoTracking") && !context.includes("Update") && !context.includes("Add");
|
|
12888
|
+
},
|
|
12889
|
+
name: "Missing AsNoTracking",
|
|
12890
|
+
description: "Read-only queries should use AsNoTracking for better performance"
|
|
12891
|
+
}
|
|
12892
|
+
];
|
|
12893
|
+
async function checkPerformance(context) {
|
|
12894
|
+
const findings = [];
|
|
12895
|
+
for (const file of context.files) {
|
|
12896
|
+
const lines = file.content.split("\n");
|
|
12897
|
+
if (file.language === "csharp") {
|
|
12898
|
+
for (const { pattern, name, description } of N_PLUS_ONE_PATTERNS) {
|
|
12899
|
+
let match;
|
|
12900
|
+
pattern.lastIndex = 0;
|
|
12901
|
+
while ((match = pattern.exec(file.content)) !== null) {
|
|
12902
|
+
const lineNumber = getLineNumber8(file.content, match.index);
|
|
12903
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
12904
|
+
findings.push({
|
|
12905
|
+
id: generateFindingId("performance"),
|
|
12906
|
+
category: "performance",
|
|
12907
|
+
severity: "critical",
|
|
12908
|
+
title: `N+1 Query: ${name}`,
|
|
12909
|
+
description,
|
|
12910
|
+
file: file.relativePath,
|
|
12911
|
+
line: lineNumber,
|
|
12912
|
+
code: truncateCode7(lineContent),
|
|
12913
|
+
suggestion: "Use Include() or batch the queries. Consider using .Select() projection to load only needed data.",
|
|
12914
|
+
autoFixable: false,
|
|
12915
|
+
references: ["https://docs.microsoft.com/en-us/ef/core/querying/related-data"]
|
|
12916
|
+
});
|
|
12917
|
+
}
|
|
12918
|
+
}
|
|
12919
|
+
for (const { pattern, name, description } of OVER_FETCHING_PATTERNS) {
|
|
12920
|
+
let match;
|
|
12921
|
+
pattern.lastIndex = 0;
|
|
12922
|
+
while ((match = pattern.exec(file.content)) !== null) {
|
|
12923
|
+
const lineNumber = getLineNumber8(file.content, match.index);
|
|
12924
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
12925
|
+
findings.push({
|
|
12926
|
+
id: generateFindingId("performance"),
|
|
12927
|
+
category: "performance",
|
|
12928
|
+
severity: "warning",
|
|
12929
|
+
title: `Over-fetching: ${name}`,
|
|
12930
|
+
description,
|
|
12931
|
+
file: file.relativePath,
|
|
12932
|
+
line: lineNumber,
|
|
12933
|
+
code: truncateCode7(lineContent),
|
|
12934
|
+
suggestion: "Consider using .Select() projection to load only the fields you need, or split into multiple queries.",
|
|
12935
|
+
autoFixable: false
|
|
12936
|
+
});
|
|
12937
|
+
}
|
|
12938
|
+
}
|
|
12939
|
+
for (const { pattern, contextCheck, name, description } of MISSING_OPTIMIZATION_PATTERNS) {
|
|
12940
|
+
let match;
|
|
12941
|
+
pattern.lastIndex = 0;
|
|
12942
|
+
while ((match = pattern.exec(file.content)) !== null) {
|
|
12943
|
+
if (contextCheck && !contextCheck(file.content, match.index)) continue;
|
|
12944
|
+
const lineNumber = getLineNumber8(file.content, match.index);
|
|
12945
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
12946
|
+
if (file.relativePath.includes("Test")) continue;
|
|
12947
|
+
findings.push({
|
|
12948
|
+
id: generateFindingId("performance"),
|
|
12949
|
+
category: "performance",
|
|
12950
|
+
severity: "warning",
|
|
12951
|
+
title: `Performance: ${name}`,
|
|
12952
|
+
description,
|
|
12953
|
+
file: file.relativePath,
|
|
12954
|
+
line: lineNumber,
|
|
12955
|
+
code: truncateCode7(lineContent),
|
|
12956
|
+
suggestion: name.includes("pagination") ? "Add .Skip() and .Take() for pagination." : "Add .AsNoTracking() for read-only queries.",
|
|
12957
|
+
autoFixable: false
|
|
12958
|
+
});
|
|
12959
|
+
}
|
|
12960
|
+
}
|
|
12961
|
+
}
|
|
12962
|
+
if (["typescript", "javascript", "tsx", "jsx"].includes(file.language)) {
|
|
12963
|
+
for (const { pattern, name, description } of FRONTEND_PERF_PATTERNS) {
|
|
12964
|
+
let match;
|
|
12965
|
+
pattern.lastIndex = 0;
|
|
12966
|
+
while ((match = pattern.exec(file.content)) !== null) {
|
|
12967
|
+
const lineNumber = getLineNumber8(file.content, match.index);
|
|
12968
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
12969
|
+
findings.push({
|
|
12970
|
+
id: generateFindingId("performance"),
|
|
12971
|
+
category: "performance",
|
|
12972
|
+
severity: "warning",
|
|
12973
|
+
title: `Frontend Performance: ${name}`,
|
|
12974
|
+
description,
|
|
12975
|
+
file: file.relativePath,
|
|
12976
|
+
line: lineNumber,
|
|
12977
|
+
code: truncateCode7(lineContent),
|
|
12978
|
+
suggestion: getSuggestionForFrontendPerf(name),
|
|
12979
|
+
autoFixable: false
|
|
12980
|
+
});
|
|
12981
|
+
}
|
|
12982
|
+
}
|
|
12983
|
+
checkMissingMemoization(file, lines, findings);
|
|
12984
|
+
}
|
|
12985
|
+
}
|
|
12986
|
+
return findings;
|
|
12987
|
+
}
|
|
12988
|
+
function checkMissingMemoization(file, lines, findings) {
|
|
12989
|
+
const expensivePatterns = [
|
|
12990
|
+
/\.filter\s*\([^)]+\)\.map\s*\(/gi,
|
|
12991
|
+
/\.reduce\s*\([^)]+\)/gi,
|
|
12992
|
+
/\.sort\s*\([^)]*\)/gi,
|
|
12993
|
+
/JSON\.parse\s*\(/gi
|
|
12994
|
+
];
|
|
12995
|
+
for (const pattern of expensivePatterns) {
|
|
12996
|
+
let match;
|
|
12997
|
+
pattern.lastIndex = 0;
|
|
12998
|
+
while ((match = pattern.exec(file.content)) !== null) {
|
|
12999
|
+
const lineNumber = getLineNumber8(file.content, match.index);
|
|
13000
|
+
const contextStart = Math.max(0, match.index - 100);
|
|
13001
|
+
const context = file.content.slice(contextStart, match.index);
|
|
13002
|
+
if (context.includes("useMemo") || context.includes("useCallback")) continue;
|
|
13003
|
+
const beforeMatch = file.content.slice(Math.max(0, match.index - 500), match.index);
|
|
13004
|
+
if (beforeMatch.includes("return (") || beforeMatch.includes("return <")) {
|
|
13005
|
+
findings.push({
|
|
13006
|
+
id: generateFindingId("performance"),
|
|
13007
|
+
category: "performance",
|
|
13008
|
+
severity: "warning",
|
|
13009
|
+
title: "Expensive Computation in Render",
|
|
13010
|
+
description: "Expensive array operations in render body should be memoized.",
|
|
13011
|
+
file: file.relativePath,
|
|
13012
|
+
line: lineNumber,
|
|
13013
|
+
code: truncateCode7(lines[lineNumber - 1]?.trim() || ""),
|
|
13014
|
+
suggestion: "Wrap this computation with useMemo() to prevent recalculation on every render.",
|
|
13015
|
+
autoFixable: false
|
|
13016
|
+
});
|
|
13017
|
+
}
|
|
13018
|
+
}
|
|
13019
|
+
}
|
|
13020
|
+
}
|
|
13021
|
+
function getSuggestionForFrontendPerf(name) {
|
|
13022
|
+
const suggestions = {
|
|
13023
|
+
"Multiple fetches in useEffect": "Use Promise.all() to parallelize requests or combine into a single API call.",
|
|
13024
|
+
"Fetch without dependencies": "Add a dependency array to prevent the effect from running on every render.",
|
|
13025
|
+
"setState in loop": "Batch state updates or use a reducer pattern.",
|
|
13026
|
+
"Large inline style object": "Extract styles to a useMemo() hook or a constant outside the component."
|
|
13027
|
+
};
|
|
13028
|
+
return suggestions[name] || "Optimize this code for better performance.";
|
|
13029
|
+
}
|
|
13030
|
+
function getLineNumber8(content, index) {
|
|
13031
|
+
return content.slice(0, index).split("\n").length;
|
|
13032
|
+
}
|
|
13033
|
+
function truncateCode7(code, maxLength = 100) {
|
|
13034
|
+
if (code.length <= maxLength) return code;
|
|
13035
|
+
return code.slice(0, maxLength - 3) + "...";
|
|
13036
|
+
}
|
|
13037
|
+
|
|
13038
|
+
// src/tools/review-code/checks/dead-code.ts
|
|
13039
|
+
var COMMENTED_CODE_PATTERNS = [
|
|
13040
|
+
{
|
|
13041
|
+
pattern: /\/\/\s*(?:if|for|while|switch|return|var|let|const|public|private|async)\s+/gi,
|
|
13042
|
+
name: "Commented code block"
|
|
13043
|
+
},
|
|
13044
|
+
{
|
|
13045
|
+
pattern: /\/\*[\s\S]*?(?:if|for|while|return|function)[\s\S]*?\*\//gi,
|
|
13046
|
+
name: "Commented code block"
|
|
13047
|
+
}
|
|
13048
|
+
];
|
|
13049
|
+
var TODO_PATTERNS = [
|
|
13050
|
+
{
|
|
13051
|
+
pattern: /\/\/\s*TODO[:\s]/gi,
|
|
13052
|
+
name: "TODO comment"
|
|
13053
|
+
},
|
|
13054
|
+
{
|
|
13055
|
+
pattern: /\/\/\s*FIXME[:\s]/gi,
|
|
13056
|
+
name: "FIXME comment"
|
|
13057
|
+
},
|
|
13058
|
+
{
|
|
13059
|
+
pattern: /\/\/\s*HACK[:\s]/gi,
|
|
13060
|
+
name: "HACK comment"
|
|
13061
|
+
},
|
|
13062
|
+
{
|
|
13063
|
+
pattern: /\/\/\s*XXX[:\s]/gi,
|
|
13064
|
+
name: "XXX comment"
|
|
13065
|
+
}
|
|
13066
|
+
];
|
|
13067
|
+
var CSHARP_UNUSED_IMPORT_PATTERN = /using\s+([\w.]+);/gi;
|
|
13068
|
+
var CSHARP_PRIVATE_METHOD_PATTERN = /private\s+(?:async\s+)?(?:static\s+)?(?:Task<?\w*>?|\w+)\s+(\w+)\s*\(/gi;
|
|
13069
|
+
var CSHARP_PRIVATE_FIELD_PATTERN = /private\s+(?:readonly\s+)?(?:static\s+)?\w+(?:<[^>]+>)?\s+_?(\w+)\s*[;=]/gi;
|
|
13070
|
+
async function checkDeadCode(context) {
|
|
13071
|
+
const findings = [];
|
|
13072
|
+
for (const file of context.files) {
|
|
13073
|
+
const lines = file.content.split("\n");
|
|
13074
|
+
for (const { pattern } of COMMENTED_CODE_PATTERNS) {
|
|
13075
|
+
let match;
|
|
13076
|
+
pattern.lastIndex = 0;
|
|
13077
|
+
while ((match = pattern.exec(file.content)) !== null) {
|
|
13078
|
+
const lineNumber = getLineNumber9(file.content, match.index);
|
|
13079
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
13080
|
+
if (lineContent.includes("///") || lineContent.includes("/**")) continue;
|
|
13081
|
+
findings.push({
|
|
13082
|
+
id: generateFindingId("dead-code"),
|
|
13083
|
+
category: "dead-code",
|
|
13084
|
+
severity: "warning",
|
|
13085
|
+
title: "Commented Code",
|
|
13086
|
+
description: "Commented-out code should be removed. Use version control to preserve history.",
|
|
13087
|
+
file: file.relativePath,
|
|
13088
|
+
line: lineNumber,
|
|
13089
|
+
code: truncateCode8(lineContent),
|
|
13090
|
+
suggestion: "Remove commented code. If needed later, it can be recovered from git history.",
|
|
13091
|
+
autoFixable: true
|
|
13092
|
+
});
|
|
13093
|
+
}
|
|
13094
|
+
}
|
|
13095
|
+
for (const { pattern, name } of TODO_PATTERNS) {
|
|
13096
|
+
let match;
|
|
13097
|
+
pattern.lastIndex = 0;
|
|
13098
|
+
while ((match = pattern.exec(file.content)) !== null) {
|
|
13099
|
+
const lineNumber = getLineNumber9(file.content, match.index);
|
|
13100
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
13101
|
+
findings.push({
|
|
13102
|
+
id: generateFindingId("dead-code"),
|
|
13103
|
+
category: "dead-code",
|
|
13104
|
+
severity: "info",
|
|
13105
|
+
title: `${name} Found`,
|
|
13106
|
+
description: `${name} indicates incomplete or temporary code that may need attention.`,
|
|
13107
|
+
file: file.relativePath,
|
|
13108
|
+
line: lineNumber,
|
|
13109
|
+
code: truncateCode8(lineContent),
|
|
13110
|
+
suggestion: "Address the TODO/FIXME or create a ticket to track it.",
|
|
13111
|
+
autoFixable: false
|
|
13112
|
+
});
|
|
13113
|
+
}
|
|
13114
|
+
}
|
|
13115
|
+
if (file.language === "csharp") {
|
|
13116
|
+
const privateMethods = /* @__PURE__ */ new Map();
|
|
13117
|
+
let match;
|
|
13118
|
+
CSHARP_PRIVATE_METHOD_PATTERN.lastIndex = 0;
|
|
13119
|
+
while ((match = CSHARP_PRIVATE_METHOD_PATTERN.exec(file.content)) !== null) {
|
|
13120
|
+
privateMethods.set(match[1], getLineNumber9(file.content, match.index));
|
|
13121
|
+
}
|
|
13122
|
+
for (const [methodName, lineNumber] of privateMethods) {
|
|
13123
|
+
const regex = new RegExp(`\\b${methodName}\\s*\\(`, "g");
|
|
13124
|
+
const occurrences = (file.content.match(regex) || []).length;
|
|
13125
|
+
if (occurrences === 1) {
|
|
13126
|
+
findings.push({
|
|
13127
|
+
id: generateFindingId("dead-code"),
|
|
13128
|
+
category: "dead-code",
|
|
13129
|
+
severity: "warning",
|
|
13130
|
+
title: "Unused Private Method",
|
|
13131
|
+
description: `Private method '${methodName}' is declared but never called.`,
|
|
13132
|
+
file: file.relativePath,
|
|
13133
|
+
line: lineNumber,
|
|
13134
|
+
suggestion: "Remove the unused method or make it public if it should be accessible.",
|
|
13135
|
+
autoFixable: true
|
|
13136
|
+
});
|
|
13137
|
+
}
|
|
13138
|
+
}
|
|
13139
|
+
const privateFields = /* @__PURE__ */ new Map();
|
|
13140
|
+
CSHARP_PRIVATE_FIELD_PATTERN.lastIndex = 0;
|
|
13141
|
+
while ((match = CSHARP_PRIVATE_FIELD_PATTERN.exec(file.content)) !== null) {
|
|
13142
|
+
const fieldName = match[1];
|
|
13143
|
+
if (fieldName.startsWith("_") && fieldName.length > 1) {
|
|
13144
|
+
privateFields.set(fieldName, getLineNumber9(file.content, match.index));
|
|
13145
|
+
}
|
|
13146
|
+
}
|
|
13147
|
+
for (const [fieldName, lineNumber] of privateFields) {
|
|
13148
|
+
const regex = new RegExp(`\\b${fieldName}\\b`, "g");
|
|
13149
|
+
const occurrences = (file.content.match(regex) || []).length;
|
|
13150
|
+
if (occurrences === 1) {
|
|
13151
|
+
findings.push({
|
|
13152
|
+
id: generateFindingId("dead-code"),
|
|
13153
|
+
category: "dead-code",
|
|
13154
|
+
severity: "warning",
|
|
13155
|
+
title: "Unused Private Field",
|
|
13156
|
+
description: `Private field '${fieldName}' is declared but never used.`,
|
|
13157
|
+
file: file.relativePath,
|
|
13158
|
+
line: lineNumber,
|
|
13159
|
+
suggestion: "Remove the unused field.",
|
|
13160
|
+
autoFixable: true
|
|
13161
|
+
});
|
|
13162
|
+
}
|
|
13163
|
+
}
|
|
13164
|
+
checkUnusedCsharpUsings(file, lines, findings);
|
|
13165
|
+
}
|
|
13166
|
+
if (["typescript", "javascript", "tsx", "jsx"].includes(file.language)) {
|
|
13167
|
+
checkUnusedJsImports(file, lines, findings);
|
|
13168
|
+
checkUnusedJsVariables(file, lines, findings);
|
|
13169
|
+
}
|
|
13170
|
+
}
|
|
13171
|
+
return findings;
|
|
13172
|
+
}
|
|
13173
|
+
function checkUnusedCsharpUsings(file, _lines, findings) {
|
|
13174
|
+
const usings = /* @__PURE__ */ new Map();
|
|
13175
|
+
let match;
|
|
13176
|
+
CSHARP_UNUSED_IMPORT_PATTERN.lastIndex = 0;
|
|
13177
|
+
while ((match = CSHARP_UNUSED_IMPORT_PATTERN.exec(file.content)) !== null) {
|
|
13178
|
+
const namespace = match[1];
|
|
13179
|
+
usings.set(namespace, getLineNumber9(file.content, match.index));
|
|
13180
|
+
}
|
|
13181
|
+
const contentAfterUsings = file.content.replace(/using\s+[\w.]+;/g, "");
|
|
13182
|
+
for (const [namespace, lineNumber] of usings) {
|
|
13183
|
+
const parts = namespace.split(".");
|
|
13184
|
+
const lastPart = parts[parts.length - 1];
|
|
13185
|
+
const isUsed = contentAfterUsings.includes(lastPart) || contentAfterUsings.includes(namespace);
|
|
13186
|
+
const alwaysNeeded = ["System", "System.Collections.Generic", "System.Linq", "System.Threading.Tasks"];
|
|
13187
|
+
if (alwaysNeeded.includes(namespace)) continue;
|
|
13188
|
+
if (!isUsed) {
|
|
13189
|
+
findings.push({
|
|
13190
|
+
id: generateFindingId("dead-code"),
|
|
13191
|
+
category: "dead-code",
|
|
13192
|
+
severity: "info",
|
|
13193
|
+
title: "Potentially Unused Using",
|
|
13194
|
+
description: `The using '${namespace}' may not be needed in this file.`,
|
|
13195
|
+
file: file.relativePath,
|
|
13196
|
+
line: lineNumber,
|
|
13197
|
+
suggestion: "Remove unused using statements to keep the file clean.",
|
|
13198
|
+
autoFixable: true
|
|
13199
|
+
});
|
|
13200
|
+
}
|
|
13201
|
+
}
|
|
13202
|
+
}
|
|
13203
|
+
function checkUnusedJsImports(file, _lines, findings) {
|
|
13204
|
+
const imports = /* @__PURE__ */ new Map();
|
|
13205
|
+
let match;
|
|
13206
|
+
const importPattern = /import\s+(?:\{([^}]+)\}|(\w+)(?:\s*,\s*\{([^}]+)\})?)\s+from\s+['"][^'"]+['"]/gi;
|
|
13207
|
+
while ((match = importPattern.exec(file.content)) !== null) {
|
|
13208
|
+
const lineNumber = getLineNumber9(file.content, match.index);
|
|
13209
|
+
const namedImports = match[1] || match[3];
|
|
13210
|
+
const defaultImport = match[2];
|
|
13211
|
+
if (namedImports) {
|
|
13212
|
+
const names = namedImports.split(",").map((n) => {
|
|
13213
|
+
const parts = n.trim().split(/\s+as\s+/);
|
|
13214
|
+
return parts[parts.length - 1].trim();
|
|
13215
|
+
});
|
|
13216
|
+
for (const name of names) {
|
|
13217
|
+
if (name) imports.set(name, { line: lineNumber, full: match[0] });
|
|
13218
|
+
}
|
|
13219
|
+
}
|
|
13220
|
+
if (defaultImport && defaultImport !== "React") {
|
|
13221
|
+
imports.set(defaultImport, { line: lineNumber, full: match[0] });
|
|
13222
|
+
}
|
|
13223
|
+
}
|
|
13224
|
+
const contentAfterImports = file.content.replace(/import\s+.*from\s+['"][^'"]+['"]/g, "");
|
|
13225
|
+
for (const [name, { line }] of imports) {
|
|
13226
|
+
const regex = new RegExp(`\\b${name}\\b`, "g");
|
|
13227
|
+
const occurrences = (contentAfterImports.match(regex) || []).length;
|
|
13228
|
+
if (occurrences === 0) {
|
|
13229
|
+
findings.push({
|
|
13230
|
+
id: generateFindingId("dead-code"),
|
|
13231
|
+
category: "dead-code",
|
|
13232
|
+
severity: "warning",
|
|
13233
|
+
title: "Unused Import",
|
|
13234
|
+
description: `Import '${name}' is declared but never used.`,
|
|
13235
|
+
file: file.relativePath,
|
|
13236
|
+
line,
|
|
13237
|
+
suggestion: "Remove unused imports.",
|
|
13238
|
+
autoFixable: true
|
|
13239
|
+
});
|
|
13240
|
+
}
|
|
13241
|
+
}
|
|
13242
|
+
}
|
|
13243
|
+
function checkUnusedJsVariables(file, _lines, findings) {
|
|
13244
|
+
const varPattern = /(?:const|let)\s+(\w+)\s*=\s*(?!function|async\s+function)/gi;
|
|
13245
|
+
const variables = /* @__PURE__ */ new Map();
|
|
13246
|
+
let match;
|
|
13247
|
+
while ((match = varPattern.exec(file.content)) !== null) {
|
|
13248
|
+
const varName = match[1];
|
|
13249
|
+
if (varName.startsWith("_") || varName === "i" || varName === "j") continue;
|
|
13250
|
+
variables.set(varName, getLineNumber9(file.content, match.index));
|
|
13251
|
+
}
|
|
13252
|
+
for (const [varName, lineNumber] of variables) {
|
|
13253
|
+
const regex = new RegExp(`\\b${varName}\\b`, "g");
|
|
13254
|
+
const occurrences = (file.content.match(regex) || []).length;
|
|
13255
|
+
if (occurrences === 1) {
|
|
13256
|
+
findings.push({
|
|
13257
|
+
id: generateFindingId("dead-code"),
|
|
13258
|
+
category: "dead-code",
|
|
13259
|
+
severity: "warning",
|
|
13260
|
+
title: "Unused Variable",
|
|
13261
|
+
description: `Variable '${varName}' is declared but never used.`,
|
|
13262
|
+
file: file.relativePath,
|
|
13263
|
+
line: lineNumber,
|
|
13264
|
+
suggestion: "Remove unused variables or prefix with underscore if intentionally unused.",
|
|
13265
|
+
autoFixable: true
|
|
13266
|
+
});
|
|
13267
|
+
}
|
|
13268
|
+
}
|
|
13269
|
+
}
|
|
13270
|
+
function getLineNumber9(content, index) {
|
|
13271
|
+
return content.slice(0, index).split("\n").length;
|
|
13272
|
+
}
|
|
13273
|
+
function truncateCode8(code, maxLength = 100) {
|
|
13274
|
+
if (code.length <= maxLength) return code;
|
|
13275
|
+
return code.slice(0, maxLength - 3) + "...";
|
|
13276
|
+
}
|
|
13277
|
+
|
|
13278
|
+
// src/tools/review-code/checks/i18n.ts
|
|
13279
|
+
var JSX_TEXT_PATTERN = />([A-Z][a-z]+(?:\s+[a-z]+)*[.!?]?)</gi;
|
|
13280
|
+
var JSX_ATTR_PATTERNS = [
|
|
13281
|
+
{ pattern: /placeholder\s*=\s*["']([^"']+)["']/gi, name: "placeholder" },
|
|
13282
|
+
{ pattern: /title\s*=\s*["']([A-Z][^"']+)["']/gi, name: "title" },
|
|
13283
|
+
{ pattern: /aria-label\s*=\s*["']([^"']+)["']/gi, name: "aria-label" },
|
|
13284
|
+
{ pattern: /alt\s*=\s*["']([A-Z][^"']+)["']/gi, name: "alt" },
|
|
13285
|
+
{ pattern: /label\s*=\s*["']([A-Z][^"']+)["']/gi, name: "label" }
|
|
13286
|
+
];
|
|
13287
|
+
var CSHARP_ERROR_PATTERNS = [
|
|
13288
|
+
{ pattern: /throw\s+new\s+\w*Exception\s*\(\s*["']([^"']+)["']/gi, name: "Exception message" },
|
|
13289
|
+
{ pattern: /ModelState\.AddModelError\s*\([^,]+,\s*["']([^"']+)["']/gi, name: "Validation error" },
|
|
13290
|
+
{ pattern: /\.WithMessage\s*\(\s*["']([^"']+)["']/gi, name: "FluentValidation message" },
|
|
13291
|
+
{ pattern: /BadRequest\s*\(\s*["']([^"']+)["']/gi, name: "BadRequest message" },
|
|
13292
|
+
{ pattern: /NotFound\s*\(\s*["']([^"']+)["']/gi, name: "NotFound message" }
|
|
13293
|
+
];
|
|
13294
|
+
var NOTIFICATION_PATTERNS = [
|
|
13295
|
+
{ pattern: /toast\.\w+\s*\(\s*["']([^"']+)["']/gi, name: "Toast notification" },
|
|
13296
|
+
{ pattern: /notify\s*\(\s*["']([^"']+)["']/gi, name: "Notification" },
|
|
13297
|
+
{ pattern: /alert\s*\(\s*["']([^"']+)["']/gi, name: "Alert" },
|
|
13298
|
+
{ pattern: /confirm\s*\(\s*["']([^"']+)["']/gi, name: "Confirm dialog" }
|
|
13299
|
+
];
|
|
13300
|
+
var EXCLUDED_PATTERNS = [
|
|
13301
|
+
/^[a-z]+$/i,
|
|
13302
|
+
// Single words (likely identifiers)
|
|
13303
|
+
/^[A-Z_]+$/,
|
|
13304
|
+
// CONSTANTS
|
|
13305
|
+
/^\d+$/,
|
|
13306
|
+
// Numbers
|
|
13307
|
+
/^[a-z0-9-_]+$/i,
|
|
13308
|
+
// IDs, slugs
|
|
13309
|
+
/^https?:\/\//i,
|
|
13310
|
+
// URLs
|
|
13311
|
+
/^[a-zA-Z0-9._%+-]+@/,
|
|
13312
|
+
// Emails
|
|
13313
|
+
/^\{.*\}$/,
|
|
13314
|
+
// Template variables
|
|
13315
|
+
/^#[0-9a-f]+$/i
|
|
13316
|
+
// Color codes
|
|
13317
|
+
];
|
|
13318
|
+
async function checkI18n(context) {
|
|
13319
|
+
const findings = [];
|
|
13320
|
+
for (const file of context.files) {
|
|
13321
|
+
if (!isUiFile(file.relativePath)) continue;
|
|
13322
|
+
const lines = file.content.split("\n");
|
|
13323
|
+
if (["tsx", "jsx"].includes(file.language)) {
|
|
13324
|
+
let match;
|
|
13325
|
+
JSX_TEXT_PATTERN.lastIndex = 0;
|
|
13326
|
+
while ((match = JSX_TEXT_PATTERN.exec(file.content)) !== null) {
|
|
13327
|
+
const text = match[1].trim();
|
|
13328
|
+
if (!shouldExclude(text) && text.length > 2) {
|
|
13329
|
+
const lineNumber = getLineNumber10(file.content, match.index);
|
|
13330
|
+
findings.push({
|
|
13331
|
+
id: generateFindingId("i18n"),
|
|
13332
|
+
category: "i18n",
|
|
13333
|
+
severity: "warning",
|
|
13334
|
+
title: "Hardcoded UI Text",
|
|
13335
|
+
description: `Text "${truncateCode9(text, 30)}" should use translation function.`,
|
|
13336
|
+
file: file.relativePath,
|
|
13337
|
+
line: lineNumber,
|
|
13338
|
+
code: truncateCode9(lines[lineNumber - 1]?.trim() || ""),
|
|
13339
|
+
suggestion: `Use {t('key')} instead of hardcoded text. Example: {t('buttons.submit')}`,
|
|
13340
|
+
autoFixable: false
|
|
13341
|
+
});
|
|
13342
|
+
}
|
|
13343
|
+
}
|
|
13344
|
+
for (const { pattern, name } of JSX_ATTR_PATTERNS) {
|
|
13345
|
+
pattern.lastIndex = 0;
|
|
13346
|
+
while ((match = pattern.exec(file.content)) !== null) {
|
|
13347
|
+
const text = match[1].trim();
|
|
13348
|
+
if (!shouldExclude(text) && text.length > 2) {
|
|
13349
|
+
const lineNumber = getLineNumber10(file.content, match.index);
|
|
13350
|
+
findings.push({
|
|
13351
|
+
id: generateFindingId("i18n"),
|
|
13352
|
+
category: "i18n",
|
|
13353
|
+
severity: "warning",
|
|
13354
|
+
title: `Non-translated ${name}`,
|
|
13355
|
+
description: `The ${name} attribute "${truncateCode9(text, 30)}" should be translated.`,
|
|
13356
|
+
file: file.relativePath,
|
|
13357
|
+
line: lineNumber,
|
|
13358
|
+
code: truncateCode9(lines[lineNumber - 1]?.trim() || ""),
|
|
13359
|
+
suggestion: `Use {t('key')} for the ${name} attribute.`,
|
|
13360
|
+
autoFixable: false
|
|
13361
|
+
});
|
|
13362
|
+
}
|
|
13363
|
+
}
|
|
13364
|
+
}
|
|
13365
|
+
for (const { pattern, name } of NOTIFICATION_PATTERNS) {
|
|
13366
|
+
pattern.lastIndex = 0;
|
|
13367
|
+
while ((match = pattern.exec(file.content)) !== null) {
|
|
13368
|
+
const text = match[1].trim();
|
|
13369
|
+
if (!shouldExclude(text) && text.length > 2) {
|
|
13370
|
+
const lineNumber = getLineNumber10(file.content, match.index);
|
|
13371
|
+
findings.push({
|
|
13372
|
+
id: generateFindingId("i18n"),
|
|
13373
|
+
category: "i18n",
|
|
13374
|
+
severity: "warning",
|
|
13375
|
+
title: `Non-translated ${name}`,
|
|
13376
|
+
description: `${name} message "${truncateCode9(text, 30)}" should be translated.`,
|
|
13377
|
+
file: file.relativePath,
|
|
13378
|
+
line: lineNumber,
|
|
13379
|
+
code: truncateCode9(lines[lineNumber - 1]?.trim() || ""),
|
|
13380
|
+
suggestion: `Use t('key') for notification messages.`,
|
|
13381
|
+
autoFixable: false
|
|
13382
|
+
});
|
|
13383
|
+
}
|
|
13384
|
+
}
|
|
13385
|
+
}
|
|
13386
|
+
if (!file.content.includes("useTranslation") && !file.content.includes("from 'react-i18next'")) {
|
|
13387
|
+
const hasHardcodedStrings = JSX_TEXT_PATTERN.test(file.content);
|
|
13388
|
+
if (hasHardcodedStrings) {
|
|
13389
|
+
findings.push({
|
|
13390
|
+
id: generateFindingId("i18n"),
|
|
13391
|
+
category: "i18n",
|
|
13392
|
+
severity: "info",
|
|
13393
|
+
title: "Missing i18n Setup",
|
|
13394
|
+
description: "This file has UI text but does not import useTranslation hook.",
|
|
13395
|
+
file: file.relativePath,
|
|
13396
|
+
line: 1,
|
|
13397
|
+
suggestion: "Add: import { useTranslation } from 'react-i18next';",
|
|
13398
|
+
autoFixable: true
|
|
13399
|
+
});
|
|
13400
|
+
}
|
|
13401
|
+
}
|
|
13402
|
+
}
|
|
13403
|
+
if (file.language === "csharp") {
|
|
13404
|
+
for (const { pattern, name } of CSHARP_ERROR_PATTERNS) {
|
|
13405
|
+
let match;
|
|
13406
|
+
pattern.lastIndex = 0;
|
|
13407
|
+
while ((match = pattern.exec(file.content)) !== null) {
|
|
13408
|
+
const text = match[1].trim();
|
|
13409
|
+
if (text.length > 10 && !shouldExclude(text)) {
|
|
13410
|
+
const lineNumber = getLineNumber10(file.content, match.index);
|
|
13411
|
+
findings.push({
|
|
13412
|
+
id: generateFindingId("i18n"),
|
|
13413
|
+
category: "i18n",
|
|
13414
|
+
severity: "warning",
|
|
13415
|
+
title: `Non-translated ${name}`,
|
|
13416
|
+
description: `${name} "${truncateCode9(text, 30)}" should use localization.`,
|
|
13417
|
+
file: file.relativePath,
|
|
13418
|
+
line: lineNumber,
|
|
13419
|
+
code: truncateCode9(lines[lineNumber - 1]?.trim() || ""),
|
|
13420
|
+
suggestion: "Use IStringLocalizer or resource files for error messages.",
|
|
13421
|
+
autoFixable: false
|
|
13422
|
+
});
|
|
13423
|
+
}
|
|
13424
|
+
}
|
|
13425
|
+
}
|
|
13426
|
+
}
|
|
13427
|
+
}
|
|
13428
|
+
return findings;
|
|
13429
|
+
}
|
|
13430
|
+
function isUiFile(filePath) {
|
|
13431
|
+
const lower = filePath.toLowerCase();
|
|
13432
|
+
return lower.includes("/pages/") || lower.includes("/components/") || lower.includes("/views/") || lower.includes("/ui/") || lower.endsWith(".tsx") || lower.endsWith(".jsx") || lower.includes("controller") && lower.endsWith(".cs");
|
|
13433
|
+
}
|
|
13434
|
+
function shouldExclude(text) {
|
|
13435
|
+
for (const pattern of EXCLUDED_PATTERNS) {
|
|
13436
|
+
if (pattern.test(text)) return true;
|
|
13437
|
+
}
|
|
13438
|
+
if (text.length <= 2) return true;
|
|
13439
|
+
if (text.includes("(") || text.includes(")") || text.includes("{")) return true;
|
|
13440
|
+
const alphaChars = text.replace(/[^a-zA-Z]/g, "").length;
|
|
13441
|
+
if (alphaChars < text.length * 0.5) return true;
|
|
13442
|
+
return false;
|
|
13443
|
+
}
|
|
13444
|
+
function getLineNumber10(content, index) {
|
|
13445
|
+
return content.slice(0, index).split("\n").length;
|
|
13446
|
+
}
|
|
13447
|
+
function truncateCode9(code, maxLength = 100) {
|
|
13448
|
+
if (code.length <= maxLength) return code;
|
|
13449
|
+
return code.slice(0, maxLength - 3) + "...";
|
|
13450
|
+
}
|
|
13451
|
+
|
|
13452
|
+
// src/tools/review-code/checks/accessibility.ts
|
|
13453
|
+
var IMG_WITHOUT_ALT = /<img(?![^>]*\balt\s*=)[^>]*>/gi;
|
|
13454
|
+
var IMG_EMPTY_ALT = /<img[^>]*\balt\s*=\s*["']\s*["'][^>]*>/gi;
|
|
13455
|
+
var LINK_WITHOUT_TEXT = /<a[^>]*>\s*<\/a>/gi;
|
|
13456
|
+
var LINK_ONLY_ICON = /<a[^>]*>\s*<(?:svg|i|span[^>]*class="[^"]*icon)[^>]*>\s*<\/a>/gi;
|
|
13457
|
+
var BUTTON_WITHOUT_TEXT = /<button[^>]*>\s*<(?:svg|i|span[^>]*icon)[^>]*>\s*<\/button>/gi;
|
|
13458
|
+
var ONCLICK_ON_DIV = /<div[^>]*onClick[^>]*>/gi;
|
|
13459
|
+
var ONCLICK_ON_SPAN = /<span[^>]*onClick[^>]*>/gi;
|
|
13460
|
+
var HEADING_PATTERN = /<h([1-6])/gi;
|
|
13461
|
+
var POSITIVE_TABINDEX = /tabIndex\s*=\s*\{?\s*["']?([1-9]\d*)["']?\s*\}?/gi;
|
|
13462
|
+
var OUTLINE_NONE = /outline\s*:\s*none|outline\s*:\s*0/gi;
|
|
13463
|
+
var ARIA_HIDDEN_FOCUSABLE = /aria-hidden\s*=\s*["']true["'][^>]*(?:tabIndex|onClick|onKeyDown)/gi;
|
|
13464
|
+
async function checkAccessibility(context) {
|
|
13465
|
+
const findings = [];
|
|
13466
|
+
for (const file of context.files) {
|
|
13467
|
+
if (!["tsx", "jsx", "typescript", "javascript"].includes(file.language)) continue;
|
|
13468
|
+
if (!isUiFile2(file.relativePath)) continue;
|
|
13469
|
+
const lines = file.content.split("\n");
|
|
13470
|
+
checkPattern(file, lines, IMG_WITHOUT_ALT, {
|
|
13471
|
+
title: "Image Missing Alt Text",
|
|
13472
|
+
description: "Images must have an alt attribute for screen readers.",
|
|
13473
|
+
severity: "critical",
|
|
13474
|
+
suggestion: 'Add alt="description" to the image. Use alt="" only for decorative images.',
|
|
13475
|
+
cweId: void 0
|
|
13476
|
+
}, findings);
|
|
13477
|
+
let match;
|
|
13478
|
+
IMG_EMPTY_ALT.lastIndex = 0;
|
|
13479
|
+
while ((match = IMG_EMPTY_ALT.exec(file.content)) !== null) {
|
|
13480
|
+
if (match[0].includes("aria-hidden") || match[0].includes('role="presentation"')) continue;
|
|
13481
|
+
const lineNumber = getLineNumber11(file.content, match.index);
|
|
13482
|
+
findings.push({
|
|
13483
|
+
id: generateFindingId("accessibility"),
|
|
13484
|
+
category: "accessibility",
|
|
13485
|
+
severity: "warning",
|
|
13486
|
+
title: "Empty Alt on Potentially Informative Image",
|
|
13487
|
+
description: "Empty alt text should only be used for decorative images.",
|
|
13488
|
+
file: file.relativePath,
|
|
13489
|
+
line: lineNumber,
|
|
13490
|
+
code: truncateCode10(lines[lineNumber - 1]?.trim() || ""),
|
|
13491
|
+
suggestion: 'Add descriptive alt text or add role="presentation" if truly decorative.',
|
|
13492
|
+
autoFixable: false
|
|
13493
|
+
});
|
|
13494
|
+
}
|
|
13495
|
+
checkPattern(file, lines, LINK_WITHOUT_TEXT, {
|
|
13496
|
+
title: "Empty Link",
|
|
13497
|
+
description: "Links must have accessible text content.",
|
|
13498
|
+
severity: "critical",
|
|
13499
|
+
suggestion: "Add text content or aria-label to the link."
|
|
13500
|
+
}, findings);
|
|
13501
|
+
checkPattern(file, lines, LINK_ONLY_ICON, {
|
|
13502
|
+
title: "Icon-Only Link",
|
|
13503
|
+
description: "Links containing only icons need accessible labels.",
|
|
13504
|
+
severity: "warning",
|
|
13505
|
+
suggestion: "Add aria-label or visually hidden text to describe the link."
|
|
13506
|
+
}, findings);
|
|
13507
|
+
checkPattern(file, lines, BUTTON_WITHOUT_TEXT, {
|
|
13508
|
+
title: "Icon-Only Button",
|
|
13509
|
+
description: "Buttons containing only icons need accessible labels.",
|
|
13510
|
+
severity: "warning",
|
|
13511
|
+
suggestion: "Add aria-label or visually hidden text to describe the button action."
|
|
13512
|
+
}, findings);
|
|
13513
|
+
checkPattern(file, lines, ONCLICK_ON_DIV, {
|
|
13514
|
+
title: "Click Handler on Non-Interactive Element",
|
|
13515
|
+
description: "div elements with onClick are not keyboard accessible by default.",
|
|
13516
|
+
severity: "warning",
|
|
13517
|
+
suggestion: 'Use a button element, or add role="button", tabIndex={0}, and onKeyDown handler.'
|
|
13518
|
+
}, findings);
|
|
13519
|
+
checkPattern(file, lines, ONCLICK_ON_SPAN, {
|
|
13520
|
+
title: "Click Handler on Non-Interactive Element",
|
|
13521
|
+
description: "span elements with onClick are not keyboard accessible by default.",
|
|
13522
|
+
severity: "warning",
|
|
13523
|
+
suggestion: 'Use a button element, or add role="button", tabIndex={0}, and onKeyDown handler.'
|
|
13524
|
+
}, findings);
|
|
13525
|
+
POSITIVE_TABINDEX.lastIndex = 0;
|
|
13526
|
+
while ((match = POSITIVE_TABINDEX.exec(file.content)) !== null) {
|
|
13527
|
+
const lineNumber = getLineNumber11(file.content, match.index);
|
|
13528
|
+
findings.push({
|
|
13529
|
+
id: generateFindingId("accessibility"),
|
|
13530
|
+
category: "accessibility",
|
|
13531
|
+
severity: "warning",
|
|
13532
|
+
title: "Positive tabIndex",
|
|
13533
|
+
description: `tabIndex="${match[1]}" disrupts natural tab order and confuses keyboard users.`,
|
|
13534
|
+
file: file.relativePath,
|
|
13535
|
+
line: lineNumber,
|
|
13536
|
+
code: truncateCode10(lines[lineNumber - 1]?.trim() || ""),
|
|
13537
|
+
suggestion: "Use tabIndex={0} to add to natural tab order, or tabIndex={-1} to remove. Avoid positive values.",
|
|
13538
|
+
autoFixable: true
|
|
13539
|
+
});
|
|
13540
|
+
}
|
|
13541
|
+
OUTLINE_NONE.lastIndex = 0;
|
|
13542
|
+
while ((match = OUTLINE_NONE.exec(file.content)) !== null) {
|
|
13543
|
+
const lineNumber = getLineNumber11(file.content, match.index);
|
|
13544
|
+
const context2 = file.content.slice(Math.max(0, match.index - 100), match.index + 200);
|
|
13545
|
+
if (context2.includes(":focus") && (context2.includes("box-shadow") || context2.includes("border"))) continue;
|
|
13546
|
+
findings.push({
|
|
13547
|
+
id: generateFindingId("accessibility"),
|
|
13548
|
+
category: "accessibility",
|
|
13549
|
+
severity: "critical",
|
|
13550
|
+
title: "Focus Indicator Removed",
|
|
13551
|
+
description: "Removing outline without an alternative makes it impossible for keyboard users to see focus.",
|
|
13552
|
+
file: file.relativePath,
|
|
13553
|
+
line: lineNumber,
|
|
13554
|
+
code: truncateCode10(lines[lineNumber - 1]?.trim() || ""),
|
|
13555
|
+
suggestion: "Provide an alternative focus indicator using box-shadow or border on :focus state.",
|
|
13556
|
+
autoFixable: false
|
|
13557
|
+
});
|
|
13558
|
+
}
|
|
13559
|
+
checkPattern(file, lines, ARIA_HIDDEN_FOCUSABLE, {
|
|
13560
|
+
title: "Focusable Element Hidden from Assistive Technology",
|
|
13561
|
+
description: 'Elements with aria-hidden="true" should not be focusable.',
|
|
13562
|
+
severity: "critical",
|
|
13563
|
+
suggestion: 'Remove aria-hidden="true" or add tabIndex={-1} and remove click handlers.'
|
|
13564
|
+
}, findings);
|
|
13565
|
+
checkHeadingHierarchy(file, lines, findings);
|
|
13566
|
+
}
|
|
13567
|
+
return findings;
|
|
13568
|
+
}
|
|
13569
|
+
function checkPattern(file, lines, pattern, issue, findings) {
|
|
13570
|
+
let match;
|
|
13571
|
+
pattern.lastIndex = 0;
|
|
13572
|
+
while ((match = pattern.exec(file.content)) !== null) {
|
|
13573
|
+
const lineNumber = getLineNumber11(file.content, match.index);
|
|
13574
|
+
findings.push({
|
|
13575
|
+
id: generateFindingId("accessibility"),
|
|
13576
|
+
category: "accessibility",
|
|
13577
|
+
severity: issue.severity,
|
|
13578
|
+
title: issue.title,
|
|
13579
|
+
description: issue.description,
|
|
13580
|
+
file: file.relativePath,
|
|
13581
|
+
line: lineNumber,
|
|
13582
|
+
code: truncateCode10(lines[lineNumber - 1]?.trim() || ""),
|
|
13583
|
+
suggestion: issue.suggestion,
|
|
13584
|
+
autoFixable: false,
|
|
13585
|
+
cweId: issue.cweId
|
|
13586
|
+
});
|
|
13587
|
+
}
|
|
13588
|
+
}
|
|
13589
|
+
function checkHeadingHierarchy(file, lines, findings) {
|
|
13590
|
+
const headings = [];
|
|
13591
|
+
let match;
|
|
13592
|
+
HEADING_PATTERN.lastIndex = 0;
|
|
13593
|
+
while ((match = HEADING_PATTERN.exec(file.content)) !== null) {
|
|
13594
|
+
headings.push({
|
|
13595
|
+
level: parseInt(match[1], 10),
|
|
13596
|
+
line: getLineNumber11(file.content, match.index)
|
|
13597
|
+
});
|
|
13598
|
+
}
|
|
13599
|
+
for (let i = 1; i < headings.length; i++) {
|
|
13600
|
+
const current = headings[i];
|
|
13601
|
+
const previous = headings[i - 1];
|
|
13602
|
+
if (current.level > previous.level + 1) {
|
|
13603
|
+
findings.push({
|
|
13604
|
+
id: generateFindingId("accessibility"),
|
|
13605
|
+
category: "accessibility",
|
|
13606
|
+
severity: "warning",
|
|
13607
|
+
title: "Heading Level Skipped",
|
|
13608
|
+
description: `Heading jumps from h${previous.level} to h${current.level}. Screen reader users may think content is missing.`,
|
|
13609
|
+
file: file.relativePath,
|
|
13610
|
+
line: current.line,
|
|
13611
|
+
code: truncateCode10(lines[current.line - 1]?.trim() || ""),
|
|
13612
|
+
suggestion: `Use h${previous.level + 1} instead of h${current.level}.`,
|
|
13613
|
+
autoFixable: true
|
|
13614
|
+
});
|
|
13615
|
+
}
|
|
13616
|
+
}
|
|
13617
|
+
if (headings.length > 0 && headings[0].level !== 1) {
|
|
13618
|
+
findings.push({
|
|
13619
|
+
id: generateFindingId("accessibility"),
|
|
13620
|
+
category: "accessibility",
|
|
13621
|
+
severity: "info",
|
|
13622
|
+
title: "Page Does Not Start with h1",
|
|
13623
|
+
description: "Pages should typically start with an h1 heading.",
|
|
13624
|
+
file: file.relativePath,
|
|
13625
|
+
line: headings[0].line,
|
|
13626
|
+
suggestion: "Consider adding an h1 as the main page heading.",
|
|
13627
|
+
autoFixable: false
|
|
13628
|
+
});
|
|
13629
|
+
}
|
|
13630
|
+
}
|
|
13631
|
+
function isUiFile2(filePath) {
|
|
13632
|
+
const lower = filePath.toLowerCase();
|
|
13633
|
+
return lower.includes("/pages/") || lower.includes("/components/") || lower.includes("/views/") || lower.includes("/ui/") || lower.endsWith(".tsx") || lower.endsWith(".jsx");
|
|
13634
|
+
}
|
|
13635
|
+
function getLineNumber11(content, index) {
|
|
13636
|
+
return content.slice(0, index).split("\n").length;
|
|
13637
|
+
}
|
|
13638
|
+
function truncateCode10(code, maxLength = 100) {
|
|
13639
|
+
if (code.length <= maxLength) return code;
|
|
13640
|
+
return code.slice(0, maxLength - 3) + "...";
|
|
13641
|
+
}
|
|
13642
|
+
|
|
13643
|
+
// src/tools/review-code/formatters/markdown.ts
|
|
13644
|
+
var SEVERITY_EMOJI = {
|
|
13645
|
+
blocking: "\u{1F534}",
|
|
13646
|
+
critical: "\u{1F7E0}",
|
|
13647
|
+
warning: "\u{1F7E1}",
|
|
13648
|
+
info: "\u{1F535}"
|
|
13649
|
+
};
|
|
13650
|
+
var GRADE_EMOJI = {
|
|
13651
|
+
A: "\u{1F7E2}",
|
|
13652
|
+
B: "\u{1F7E2}",
|
|
13653
|
+
C: "\u{1F7E1}",
|
|
13654
|
+
D: "\u{1F7E0}",
|
|
13655
|
+
F: "\u{1F534}"
|
|
13656
|
+
};
|
|
13657
|
+
function formatMarkdownReport(result) {
|
|
13658
|
+
const lines = [];
|
|
13659
|
+
lines.push("# Code Review Report");
|
|
13660
|
+
lines.push("");
|
|
13661
|
+
lines.push("## Summary");
|
|
13662
|
+
lines.push("");
|
|
13663
|
+
lines.push("| Metric | Value |");
|
|
13664
|
+
lines.push("|--------|-------|");
|
|
13665
|
+
lines.push(`| Status | ${result.summary.status === "passed" ? "\u2705 PASSED" : result.summary.status === "failed" ? "\u274C FAILED" : "\u26A0\uFE0F WARNING"} |`);
|
|
13666
|
+
lines.push(`| Score | ${result.summary.score}/100 |`);
|
|
13667
|
+
lines.push(`| Grade | ${GRADE_EMOJI[result.summary.grade]} ${result.summary.grade} |`);
|
|
13668
|
+
lines.push(`| Files Scanned | ${result.stats.filesScanned} |`);
|
|
13669
|
+
lines.push(`| Lines Scanned | ${result.stats.linesScanned.toLocaleString()} |`);
|
|
13670
|
+
lines.push(`| Duration | ${result.stats.duration}ms |`);
|
|
13671
|
+
lines.push("");
|
|
13672
|
+
lines.push("### Issues Found");
|
|
13673
|
+
lines.push("");
|
|
13674
|
+
lines.push("| Severity | Count |");
|
|
13675
|
+
lines.push("|----------|-------|");
|
|
13676
|
+
lines.push(`| ${SEVERITY_EMOJI.blocking} Blocking | ${result.stats.findingsCount.blocking} |`);
|
|
13677
|
+
lines.push(`| ${SEVERITY_EMOJI.critical} Critical | ${result.stats.findingsCount.critical} |`);
|
|
13678
|
+
lines.push(`| ${SEVERITY_EMOJI.warning} Warning | ${result.stats.findingsCount.warning} |`);
|
|
13679
|
+
lines.push(`| ${SEVERITY_EMOJI.info} Info | ${result.stats.findingsCount.info} |`);
|
|
13680
|
+
lines.push("");
|
|
13681
|
+
const categoriesWithIssues = Object.entries(result.stats.byCategory).filter(([_, count]) => count > 0).sort((a, b) => b[1] - a[1]);
|
|
13682
|
+
if (categoriesWithIssues.length > 0) {
|
|
13683
|
+
lines.push("### By Category");
|
|
13684
|
+
lines.push("");
|
|
13685
|
+
lines.push("| Category | Count |");
|
|
13686
|
+
lines.push("|----------|-------|");
|
|
13687
|
+
for (const [category, count] of categoriesWithIssues) {
|
|
13688
|
+
lines.push(`| ${formatCategoryName(category)} | ${count} |`);
|
|
13689
|
+
}
|
|
13690
|
+
lines.push("");
|
|
13691
|
+
}
|
|
13692
|
+
const blockingFindings = result.findings.filter((f) => f.severity === "blocking");
|
|
13693
|
+
const criticalFindings = result.findings.filter((f) => f.severity === "critical");
|
|
13694
|
+
const warningFindings = result.findings.filter((f) => f.severity === "warning");
|
|
13695
|
+
const infoFindings = result.findings.filter((f) => f.severity === "info");
|
|
13696
|
+
if (blockingFindings.length > 0) {
|
|
13697
|
+
lines.push("---");
|
|
13698
|
+
lines.push("");
|
|
13699
|
+
lines.push(`## ${SEVERITY_EMOJI.blocking} Blocking Issues (${blockingFindings.length})`);
|
|
13700
|
+
lines.push("");
|
|
13701
|
+
lines.push("> These issues MUST be fixed before merging.");
|
|
13702
|
+
lines.push("");
|
|
13703
|
+
formatFindings(lines, blockingFindings);
|
|
13704
|
+
}
|
|
13705
|
+
if (criticalFindings.length > 0) {
|
|
13706
|
+
lines.push("---");
|
|
13707
|
+
lines.push("");
|
|
13708
|
+
lines.push(`## ${SEVERITY_EMOJI.critical} Critical Issues (${criticalFindings.length})`);
|
|
13709
|
+
lines.push("");
|
|
13710
|
+
lines.push("> These issues should be fixed as soon as possible.");
|
|
13711
|
+
lines.push("");
|
|
13712
|
+
formatFindings(lines, criticalFindings);
|
|
13713
|
+
}
|
|
13714
|
+
if (warningFindings.length > 0) {
|
|
13715
|
+
lines.push("---");
|
|
13716
|
+
lines.push("");
|
|
13717
|
+
lines.push(`## ${SEVERITY_EMOJI.warning} Warnings (${warningFindings.length})`);
|
|
13718
|
+
lines.push("");
|
|
13719
|
+
formatFindings(lines, warningFindings);
|
|
13720
|
+
}
|
|
13721
|
+
if (infoFindings.length > 0) {
|
|
13722
|
+
lines.push("---");
|
|
13723
|
+
lines.push("");
|
|
13724
|
+
lines.push(`## ${SEVERITY_EMOJI.info} Info (${infoFindings.length})`);
|
|
13725
|
+
lines.push("");
|
|
13726
|
+
formatFindings(lines, infoFindings);
|
|
13727
|
+
}
|
|
13728
|
+
if (result.findings.length === 0) {
|
|
13729
|
+
lines.push("---");
|
|
13730
|
+
lines.push("");
|
|
13731
|
+
lines.push("## \u2728 No Issues Found");
|
|
13732
|
+
lines.push("");
|
|
13733
|
+
lines.push("Great job! Your code passed all checks.");
|
|
13734
|
+
lines.push("");
|
|
13735
|
+
}
|
|
13736
|
+
return lines.join("\n");
|
|
13737
|
+
}
|
|
13738
|
+
function formatFindings(lines, findings) {
|
|
13739
|
+
for (const finding of findings) {
|
|
13740
|
+
lines.push(`### ${finding.id}: ${finding.title}`);
|
|
13741
|
+
lines.push("");
|
|
13742
|
+
lines.push(`**Category**: ${formatCategoryName(finding.category)}`);
|
|
13743
|
+
lines.push(`**File**: \`${finding.file}${finding.line ? `:${finding.line}` : ""}\``);
|
|
13744
|
+
if (finding.cweId) {
|
|
13745
|
+
lines.push(`**CWE**: ${finding.cweId}`);
|
|
10596
13746
|
}
|
|
10597
|
-
|
|
10598
|
-
|
|
13747
|
+
lines.push("");
|
|
13748
|
+
lines.push(finding.description);
|
|
13749
|
+
lines.push("");
|
|
13750
|
+
if (finding.code) {
|
|
13751
|
+
lines.push("**Code**:");
|
|
13752
|
+
lines.push("```");
|
|
13753
|
+
lines.push(finding.code);
|
|
13754
|
+
lines.push("```");
|
|
13755
|
+
lines.push("");
|
|
10599
13756
|
}
|
|
10600
|
-
|
|
10601
|
-
|
|
13757
|
+
lines.push(`**Fix**: ${finding.suggestion}`);
|
|
13758
|
+
if (finding.autoFixable) {
|
|
13759
|
+
lines.push("");
|
|
13760
|
+
lines.push("> \u{1F4A1} This issue can be auto-fixed.");
|
|
10602
13761
|
}
|
|
10603
|
-
if (
|
|
10604
|
-
|
|
13762
|
+
if (finding.references && finding.references.length > 0) {
|
|
13763
|
+
lines.push("");
|
|
13764
|
+
lines.push("**References**:");
|
|
13765
|
+
for (const ref of finding.references) {
|
|
13766
|
+
lines.push(`- ${ref}`);
|
|
13767
|
+
}
|
|
10605
13768
|
}
|
|
10606
|
-
|
|
10607
|
-
|
|
10608
|
-
|
|
10609
|
-
|
|
10610
|
-
|
|
10611
|
-
|
|
10612
|
-
|
|
10613
|
-
|
|
10614
|
-
|
|
10615
|
-
|
|
10616
|
-
|
|
10617
|
-
|
|
10618
|
-
|
|
13769
|
+
lines.push("");
|
|
13770
|
+
}
|
|
13771
|
+
}
|
|
13772
|
+
function formatCategoryName(category) {
|
|
13773
|
+
const names = {
|
|
13774
|
+
security: "Security",
|
|
13775
|
+
architecture: "Architecture",
|
|
13776
|
+
"hardcoded-values": "Hardcoded Values",
|
|
13777
|
+
tests: "Tests",
|
|
13778
|
+
"ai-hallucinations": "AI Hallucinations",
|
|
13779
|
+
performance: "Performance",
|
|
13780
|
+
"dead-code": "Dead Code",
|
|
13781
|
+
i18n: "Internationalization",
|
|
13782
|
+
accessibility: "Accessibility"
|
|
13783
|
+
};
|
|
13784
|
+
return names[category] || category;
|
|
13785
|
+
}
|
|
13786
|
+
|
|
13787
|
+
// src/tools/review-code.ts
|
|
13788
|
+
var reviewCodeTool = {
|
|
13789
|
+
name: "review_code",
|
|
13790
|
+
description: `Comprehensive code review tool for SmartStack projects.
|
|
13791
|
+
|
|
13792
|
+
Analyzes code for 9 categories of issues:
|
|
13793
|
+
- **security**: OWASP vulnerabilities, hardcoded secrets, SQL injection, XSS
|
|
13794
|
+
- **architecture**: Clean Architecture layer violations
|
|
13795
|
+
- **hardcoded-values**: Magic numbers, hardcoded URLs, feature flags
|
|
13796
|
+
- **tests**: Missing tests, test quality issues
|
|
13797
|
+
- **ai-hallucinations**: Non-existent imports, phantom methods
|
|
13798
|
+
- **performance**: N+1 queries, over-fetching, missing memoization
|
|
13799
|
+
- **dead-code**: Unused imports, functions, variables
|
|
13800
|
+
- **i18n**: Non-internationalized UI text
|
|
13801
|
+
- **accessibility**: Missing alt text, ARIA issues, focus indicators
|
|
13802
|
+
|
|
13803
|
+
Returns a markdown report with findings grouped by severity (blocking, critical, warning, info).`,
|
|
13804
|
+
inputSchema: {
|
|
13805
|
+
type: "object",
|
|
13806
|
+
properties: {
|
|
13807
|
+
path: {
|
|
13808
|
+
type: "string",
|
|
13809
|
+
description: "Project path to review (default: SmartStack.app path)"
|
|
13810
|
+
},
|
|
13811
|
+
scope: {
|
|
13812
|
+
type: "string",
|
|
13813
|
+
enum: ["all", "changed", "staged"],
|
|
13814
|
+
default: "all",
|
|
13815
|
+
description: "Files to review: all, only changed (git diff), or staged files"
|
|
13816
|
+
},
|
|
13817
|
+
checks: {
|
|
13818
|
+
type: "array",
|
|
13819
|
+
items: {
|
|
13820
|
+
type: "string",
|
|
13821
|
+
enum: [
|
|
13822
|
+
"security",
|
|
13823
|
+
"architecture",
|
|
13824
|
+
"hardcoded-values",
|
|
13825
|
+
"tests",
|
|
13826
|
+
"ai-hallucinations",
|
|
13827
|
+
"performance",
|
|
13828
|
+
"dead-code",
|
|
13829
|
+
"i18n",
|
|
13830
|
+
"accessibility",
|
|
13831
|
+
"all"
|
|
13832
|
+
]
|
|
13833
|
+
},
|
|
13834
|
+
default: ["all"],
|
|
13835
|
+
description: "Categories of checks to run"
|
|
13836
|
+
},
|
|
13837
|
+
severity: {
|
|
13838
|
+
type: "string",
|
|
13839
|
+
enum: ["blocking", "critical", "warning", "all"],
|
|
13840
|
+
default: "all",
|
|
13841
|
+
description: "Filter results by minimum severity"
|
|
13842
|
+
}
|
|
13843
|
+
}
|
|
13844
|
+
}
|
|
13845
|
+
};
|
|
13846
|
+
async function handleReviewCode(args, config) {
|
|
13847
|
+
const startTime = Date.now();
|
|
13848
|
+
const input = ReviewCodeInputSchema.parse(args);
|
|
13849
|
+
const projectPath = input.path || config.smartstack.projectPath;
|
|
13850
|
+
logger.info("Starting code review", { projectPath, scope: input.scope, checks: input.checks });
|
|
13851
|
+
resetFindingCounters();
|
|
13852
|
+
const structure = await findSmartStackStructure(projectPath);
|
|
13853
|
+
if (!structure) {
|
|
13854
|
+
return "# Code Review Error\n\nCould not detect SmartStack project structure at the specified path.";
|
|
13855
|
+
}
|
|
13856
|
+
const files = await getFilesToAnalyze(projectPath, input.scope, structure);
|
|
13857
|
+
if (files.length === 0) {
|
|
13858
|
+
return "# Code Review Report\n\nNo files to analyze for the specified scope.";
|
|
13859
|
+
}
|
|
13860
|
+
logger.info(`Analyzing ${files.length} files`);
|
|
13861
|
+
const context = {
|
|
13862
|
+
projectPath,
|
|
13863
|
+
files,
|
|
13864
|
+
scope: input.scope
|
|
13865
|
+
};
|
|
13866
|
+
const checksToRun = input.checks.includes("all") ? ["security", "architecture", "hardcoded-values", "tests", "ai-hallucinations", "performance", "dead-code", "i18n", "accessibility"] : input.checks;
|
|
13867
|
+
const checkFunctions = {
|
|
13868
|
+
security: checkSecurity,
|
|
13869
|
+
architecture: checkArchitecture,
|
|
13870
|
+
"hardcoded-values": checkHardcodedValues,
|
|
13871
|
+
tests: checkTests,
|
|
13872
|
+
"ai-hallucinations": checkAiHallucinations,
|
|
13873
|
+
performance: checkPerformance,
|
|
13874
|
+
"dead-code": checkDeadCode,
|
|
13875
|
+
i18n: checkI18n,
|
|
13876
|
+
accessibility: checkAccessibility
|
|
13877
|
+
};
|
|
13878
|
+
const checkPromises = checksToRun.filter((check) => checkFunctions[check]).map(async (check) => {
|
|
13879
|
+
try {
|
|
13880
|
+
logger.debug(`Running ${check} check`);
|
|
13881
|
+
return await checkFunctions[check](context);
|
|
13882
|
+
} catch (error) {
|
|
13883
|
+
logger.error(`Error in ${check} check`, { error });
|
|
13884
|
+
return [];
|
|
13885
|
+
}
|
|
13886
|
+
});
|
|
13887
|
+
const checkResults = await Promise.all(checkPromises);
|
|
13888
|
+
const allFindings = checkResults.flat();
|
|
13889
|
+
const filteredFindings = filterBySeverity(allFindings, input.severity);
|
|
13890
|
+
const stats = calculateStats(filteredFindings, files, startTime);
|
|
13891
|
+
const summary = calculateSummary2(filteredFindings, stats);
|
|
13892
|
+
const result = {
|
|
13893
|
+
summary,
|
|
13894
|
+
findings: filteredFindings,
|
|
13895
|
+
stats
|
|
13896
|
+
};
|
|
13897
|
+
logger.info("Code review complete", {
|
|
13898
|
+
filesScanned: stats.filesScanned,
|
|
13899
|
+
issuesFound: filteredFindings.length,
|
|
13900
|
+
duration: stats.duration
|
|
13901
|
+
});
|
|
13902
|
+
return formatMarkdownReport(result);
|
|
13903
|
+
}
|
|
13904
|
+
async function getFilesToAnalyze(projectPath, scope, _structure) {
|
|
13905
|
+
let filePaths = [];
|
|
13906
|
+
if (scope === "changed") {
|
|
13907
|
+
filePaths = await getChangedFiles(projectPath);
|
|
13908
|
+
} else if (scope === "staged") {
|
|
13909
|
+
filePaths = await getStagedFiles(projectPath);
|
|
13910
|
+
} else {
|
|
13911
|
+
const patterns = ["**/*.cs", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"];
|
|
13912
|
+
const excludePatterns = ["**/bin/**", "**/obj/**", "**/node_modules/**", "**/dist/**", "**/*.d.ts", "**/Migrations/**"];
|
|
13913
|
+
for (const pattern of patterns) {
|
|
13914
|
+
const found = await findFiles(pattern, {
|
|
13915
|
+
cwd: projectPath,
|
|
13916
|
+
ignore: excludePatterns
|
|
10619
13917
|
});
|
|
13918
|
+
filePaths.push(...found);
|
|
10620
13919
|
}
|
|
10621
13920
|
}
|
|
10622
|
-
|
|
10623
|
-
|
|
10624
|
-
|
|
10625
|
-
|
|
10626
|
-
|
|
10627
|
-
|
|
10628
|
-
|
|
10629
|
-
|
|
10630
|
-
|
|
13921
|
+
const files = [];
|
|
13922
|
+
for (const filePath of filePaths) {
|
|
13923
|
+
try {
|
|
13924
|
+
const fullPath = filePath.startsWith(projectPath) ? filePath : `${projectPath}/${filePath}`;
|
|
13925
|
+
const content = await readText(fullPath);
|
|
13926
|
+
const relativePath = filePath.replace(projectPath, "").replace(/^[/\\]/, "");
|
|
13927
|
+
files.push({
|
|
13928
|
+
path: fullPath,
|
|
13929
|
+
relativePath,
|
|
13930
|
+
content,
|
|
13931
|
+
language: detectLanguage(filePath),
|
|
13932
|
+
layer: detectLayer(relativePath),
|
|
13933
|
+
lineCount: content.split("\n").length
|
|
10631
13934
|
});
|
|
13935
|
+
} catch {
|
|
13936
|
+
logger.debug(`Could not read file: ${filePath}`);
|
|
10632
13937
|
}
|
|
10633
13938
|
}
|
|
10634
|
-
|
|
10635
|
-
hotspots.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
10636
|
-
return hotspots.slice(0, 20);
|
|
13939
|
+
return files;
|
|
10637
13940
|
}
|
|
10638
|
-
function
|
|
10639
|
-
const
|
|
10640
|
-
|
|
10641
|
-
|
|
10642
|
-
|
|
10643
|
-
|
|
13941
|
+
function detectLanguage(filePath) {
|
|
13942
|
+
const ext = filePath.toLowerCase().split(".").pop();
|
|
13943
|
+
switch (ext) {
|
|
13944
|
+
case "cs":
|
|
13945
|
+
return "csharp";
|
|
13946
|
+
case "ts":
|
|
13947
|
+
return "typescript";
|
|
13948
|
+
case "tsx":
|
|
13949
|
+
return "tsx";
|
|
13950
|
+
case "js":
|
|
13951
|
+
return "javascript";
|
|
13952
|
+
case "jsx":
|
|
13953
|
+
return "jsx";
|
|
13954
|
+
case "css":
|
|
13955
|
+
return "css";
|
|
13956
|
+
case "json":
|
|
13957
|
+
return "json";
|
|
13958
|
+
default:
|
|
13959
|
+
return "other";
|
|
13960
|
+
}
|
|
13961
|
+
}
|
|
13962
|
+
function detectLayer(relativePath) {
|
|
13963
|
+
const lower = relativePath.toLowerCase();
|
|
13964
|
+
if (lower.includes("/domain/") || lower.includes("\\domain\\")) return "domain";
|
|
13965
|
+
if (lower.includes("/application/") || lower.includes("\\application\\")) return "application";
|
|
13966
|
+
if (lower.includes("/infrastructure/") || lower.includes("\\infrastructure\\")) return "infrastructure";
|
|
13967
|
+
if (lower.includes("/api/") || lower.includes("\\api\\")) return "api";
|
|
13968
|
+
if (lower.includes("/web/") || lower.includes("\\web\\") || lower.includes("/src/")) return "web";
|
|
13969
|
+
return void 0;
|
|
13970
|
+
}
|
|
13971
|
+
function filterBySeverity(findings, minSeverity) {
|
|
13972
|
+
if (minSeverity === "all") return findings;
|
|
13973
|
+
const severityOrder = ["blocking", "critical", "warning", "info"];
|
|
13974
|
+
const minIndex = severityOrder.indexOf(minSeverity);
|
|
13975
|
+
return findings.filter((f) => severityOrder.indexOf(f.severity) <= minIndex);
|
|
13976
|
+
}
|
|
13977
|
+
function calculateStats(findings, files, startTime) {
|
|
13978
|
+
const byCategory = {
|
|
13979
|
+
security: 0,
|
|
13980
|
+
architecture: 0,
|
|
13981
|
+
"hardcoded-values": 0,
|
|
13982
|
+
tests: 0,
|
|
13983
|
+
"ai-hallucinations": 0,
|
|
13984
|
+
performance: 0,
|
|
13985
|
+
"dead-code": 0,
|
|
13986
|
+
i18n: 0,
|
|
13987
|
+
accessibility: 0
|
|
13988
|
+
};
|
|
13989
|
+
const findingsCount = {
|
|
13990
|
+
blocking: 0,
|
|
13991
|
+
critical: 0,
|
|
13992
|
+
warning: 0,
|
|
13993
|
+
info: 0
|
|
13994
|
+
};
|
|
13995
|
+
for (const finding of findings) {
|
|
13996
|
+
findingsCount[finding.severity]++;
|
|
13997
|
+
byCategory[finding.category]++;
|
|
13998
|
+
}
|
|
13999
|
+
return {
|
|
14000
|
+
filesScanned: files.length,
|
|
14001
|
+
linesScanned: files.reduce((sum, f) => sum + f.lineCount, 0),
|
|
14002
|
+
duration: Date.now() - startTime,
|
|
14003
|
+
findingsCount,
|
|
14004
|
+
byCategory
|
|
14005
|
+
};
|
|
14006
|
+
}
|
|
14007
|
+
function calculateSummary2(_findings, stats) {
|
|
14008
|
+
let score = 100;
|
|
14009
|
+
score -= stats.findingsCount.blocking * 20;
|
|
14010
|
+
score -= stats.findingsCount.critical * 5;
|
|
14011
|
+
score -= stats.findingsCount.warning * 1;
|
|
14012
|
+
score = Math.max(0, Math.min(100, score));
|
|
10644
14013
|
let grade;
|
|
10645
14014
|
if (score >= 90) grade = "A";
|
|
10646
14015
|
else if (score >= 80) grade = "B";
|
|
10647
14016
|
else if (score >= 70) grade = "C";
|
|
10648
14017
|
else if (score >= 60) grade = "D";
|
|
10649
14018
|
else grade = "F";
|
|
10650
|
-
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
|
|
10655
|
-
issuesFound: hotspots.length
|
|
10656
|
-
};
|
|
10657
|
-
}
|
|
10658
|
-
function isExcludedPath(filePath) {
|
|
10659
|
-
const exclusions = [
|
|
10660
|
-
/[/\\]bin[/\\]/,
|
|
10661
|
-
/[/\\]obj[/\\]/,
|
|
10662
|
-
/[/\\]node_modules[/\\]/,
|
|
10663
|
-
/[/\\]Migrations[/\\]/,
|
|
10664
|
-
/\.test\./,
|
|
10665
|
-
/\.spec\./,
|
|
10666
|
-
/Tests[/\\]/,
|
|
10667
|
-
/\.d\.ts$/,
|
|
10668
|
-
/\.min\./
|
|
10669
|
-
];
|
|
10670
|
-
return exclusions.some((pattern) => pattern.test(filePath));
|
|
10671
|
-
}
|
|
10672
|
-
function getLineNumber2(content, index) {
|
|
10673
|
-
const normalizedContent = content.substring(0, index).replace(/\r\n/g, "\n");
|
|
10674
|
-
return normalizedContent.split("\n").length;
|
|
10675
|
-
}
|
|
10676
|
-
function formatQualityReport(result, thresholds) {
|
|
10677
|
-
const lines = [];
|
|
10678
|
-
lines.push("# Code Quality Report");
|
|
10679
|
-
lines.push("");
|
|
10680
|
-
lines.push("## Summary");
|
|
10681
|
-
lines.push(`- **Score**: ${result.summary.score}/100 (Grade: ${result.summary.grade})`);
|
|
10682
|
-
lines.push(`- **Files analyzed**: ${result.summary.filesAnalyzed}`);
|
|
10683
|
-
lines.push(`- **Functions analyzed**: ${result.summary.functionsAnalyzed}`);
|
|
10684
|
-
lines.push(`- **Issues found**: ${result.summary.issuesFound}`);
|
|
10685
|
-
lines.push("");
|
|
10686
|
-
lines.push("## Metrics Overview");
|
|
10687
|
-
lines.push("");
|
|
10688
|
-
lines.push("| Metric | Average | Max | Threshold | Status |");
|
|
10689
|
-
lines.push("|--------|---------|-----|-----------|--------|");
|
|
10690
|
-
const formatMetricRow = (name, stat2) => {
|
|
10691
|
-
const status = stat2.violations > 0 ? `${stat2.violations} violations` : "\u2705 OK";
|
|
10692
|
-
return `| ${name} | ${stat2.average} | ${stat2.max} | ${stat2.threshold} | ${status} |`;
|
|
10693
|
-
};
|
|
10694
|
-
lines.push(formatMetricRow("Cognitive Complexity", result.metrics.cognitiveComplexity));
|
|
10695
|
-
lines.push(formatMetricRow("Cyclomatic Complexity", result.metrics.cyclomaticComplexity));
|
|
10696
|
-
lines.push(formatMetricRow("Function Size", result.metrics.functionSize));
|
|
10697
|
-
lines.push(formatMetricRow("Nesting Depth", result.metrics.nestingDepth));
|
|
10698
|
-
lines.push(formatMetricRow("File Size", result.metrics.fileSize));
|
|
10699
|
-
lines.push("");
|
|
10700
|
-
if (result.hotspots.length > 0) {
|
|
10701
|
-
lines.push("## Hotspots (Needs Attention)");
|
|
10702
|
-
lines.push("");
|
|
10703
|
-
for (const hotspot of result.hotspots) {
|
|
10704
|
-
const severityEmoji = hotspot.severity === "high" ? "\u{1F534}" : hotspot.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
10705
|
-
const location = hotspot.function ? `\`${hotspot.function}\` (${hotspot.file})` : `\`${hotspot.file}\``;
|
|
10706
|
-
lines.push(`### ${severityEmoji} ${hotspot.severity.toUpperCase()}: ${location}`);
|
|
10707
|
-
for (const issue of hotspot.issues) {
|
|
10708
|
-
lines.push(`- ${issue}`);
|
|
10709
|
-
}
|
|
10710
|
-
if (hotspot.function) {
|
|
10711
|
-
if (hotspot.metrics.cognitiveComplexity && hotspot.metrics.cognitiveComplexity > thresholds.cognitiveComplexity) {
|
|
10712
|
-
lines.push(`- **Recommendation**: Extract logic into smaller, focused methods`);
|
|
10713
|
-
} else if (hotspot.metrics.lineCount && hotspot.metrics.lineCount > thresholds.functionSize) {
|
|
10714
|
-
lines.push(`- **Recommendation**: Split into multiple functions with single responsibilities`);
|
|
10715
|
-
}
|
|
10716
|
-
} else {
|
|
10717
|
-
lines.push(`- **Recommendation**: Consider splitting this file into smaller modules`);
|
|
10718
|
-
}
|
|
10719
|
-
lines.push("");
|
|
10720
|
-
}
|
|
14019
|
+
let status;
|
|
14020
|
+
if (stats.findingsCount.blocking > 0) {
|
|
14021
|
+
status = "failed";
|
|
14022
|
+
} else if (stats.findingsCount.critical > 0) {
|
|
14023
|
+
status = "warning";
|
|
10721
14024
|
} else {
|
|
10722
|
-
|
|
14025
|
+
status = "passed";
|
|
10723
14026
|
}
|
|
10724
|
-
|
|
14027
|
+
let message;
|
|
14028
|
+
if (status === "failed") {
|
|
14029
|
+
message = `Review failed with ${stats.findingsCount.blocking} blocking issue(s). These must be fixed before merging.`;
|
|
14030
|
+
} else if (status === "warning") {
|
|
14031
|
+
message = `Review passed with warnings. ${stats.findingsCount.critical} critical issue(s) should be addressed.`;
|
|
14032
|
+
} else {
|
|
14033
|
+
message = "Code review passed. Great job!";
|
|
14034
|
+
}
|
|
14035
|
+
return { status, score, grade, message };
|
|
10725
14036
|
}
|
|
10726
14037
|
|
|
10727
14038
|
// src/resources/conventions.ts
|
|
@@ -11650,12 +14961,141 @@ const { activeTab, setActiveTab } = useTabNavigation<TabId>('info', VALID_TABS);
|
|
|
11650
14961
|
- Invalid tabs fallback to default
|
|
11651
14962
|
- Uses \`replace: true\` to avoid polluting browser history
|
|
11652
14963
|
|
|
14964
|
+
#### Lazy Loading for Tabs (Performance)
|
|
14965
|
+
|
|
14966
|
+
Pages with tabs MUST implement lazy loading to avoid fetching all tab data upfront.
|
|
14967
|
+
|
|
14968
|
+
**Pattern 1: useQuery with enabled condition (Recommended)**
|
|
14969
|
+
|
|
14970
|
+
\`\`\`tsx
|
|
14971
|
+
// \u274C BAD - Loads all data on mount
|
|
14972
|
+
const { data: info } = useQuery(['info', id], fetchInfo);
|
|
14973
|
+
const { data: settings } = useQuery(['settings', id], fetchSettings);
|
|
14974
|
+
|
|
14975
|
+
// \u2705 GOOD - Only loads active tab data
|
|
14976
|
+
const { data: info } = useQuery(
|
|
14977
|
+
['info', id],
|
|
14978
|
+
fetchInfo,
|
|
14979
|
+
{ enabled: activeTab === 'info' }
|
|
14980
|
+
);
|
|
14981
|
+
|
|
14982
|
+
const { data: settings } = useQuery(
|
|
14983
|
+
['settings', id],
|
|
14984
|
+
fetchSettings,
|
|
14985
|
+
{ enabled: activeTab === 'settings' }
|
|
14986
|
+
);
|
|
14987
|
+
\`\`\`
|
|
14988
|
+
|
|
14989
|
+
**Pattern 2: React.lazy() for heavy tab components**
|
|
14990
|
+
|
|
14991
|
+
\`\`\`tsx
|
|
14992
|
+
// Lazy load tab components
|
|
14993
|
+
const TabInfo = React.lazy(() => import('./tabs/TabInfo'));
|
|
14994
|
+
const TabSettings = React.lazy(() => import('./tabs/TabSettings'));
|
|
14995
|
+
|
|
14996
|
+
// In JSX with Suspense
|
|
14997
|
+
<TabsContent value="info">
|
|
14998
|
+
<Suspense fallback={<Skeleton />}>
|
|
14999
|
+
<TabInfo entityId={id} />
|
|
15000
|
+
</Suspense>
|
|
15001
|
+
</TabsContent>
|
|
15002
|
+
\`\`\`
|
|
15003
|
+
|
|
15004
|
+
**Pattern 3: Conditional rendering**
|
|
15005
|
+
|
|
15006
|
+
\`\`\`tsx
|
|
15007
|
+
// Render tab content only when active
|
|
15008
|
+
{activeTab === 'settings' && <SettingsTab entityId={id} />}
|
|
15009
|
+
\`\`\`
|
|
15010
|
+
|
|
15011
|
+
| Pattern | Use Case |
|
|
15012
|
+
|---------|----------|
|
|
15013
|
+
| useQuery enabled | Simple data fetching per tab |
|
|
15014
|
+
| React.lazy + Suspense | Heavy components with own data fetching |
|
|
15015
|
+
| Conditional rendering | Light components, quick tab switches |
|
|
15016
|
+
|
|
15017
|
+
### Breadcrumb Navigation
|
|
15018
|
+
|
|
15019
|
+
All pages MUST include a Breadcrumb component for consistent navigation and user orientation.
|
|
15020
|
+
|
|
15021
|
+
#### Breadcrumb Component
|
|
15022
|
+
|
|
15023
|
+
\`\`\`tsx
|
|
15024
|
+
import { Breadcrumb } from '@/components/ui/Breadcrumb';
|
|
15025
|
+
|
|
15026
|
+
// In your page component
|
|
15027
|
+
<Breadcrumb
|
|
15028
|
+
items={[
|
|
15029
|
+
{ label: t('nav.administration'), href: '/platform/administration' },
|
|
15030
|
+
{ label: t('nav.users'), href: '/platform/administration/users' },
|
|
15031
|
+
{ label: user.fullName } // Last item = current page (no href)
|
|
15032
|
+
]}
|
|
15033
|
+
/>
|
|
15034
|
+
\`\`\`
|
|
15035
|
+
|
|
15036
|
+
#### Breadcrumb Rules
|
|
15037
|
+
|
|
15038
|
+
| Rule | Description |
|
|
15039
|
+
|------|-------------|
|
|
15040
|
+
| First item | Context/Application level with Home icon (auto-added if no icon) |
|
|
15041
|
+
| Intermediate items | Clickable links to parent pages |
|
|
15042
|
+
| Last item | Current page name, no href, bold text |
|
|
15043
|
+
| Labels | Use i18n translation keys from \`navigation.json\` |
|
|
15044
|
+
| Detail pages | Include entity name (e.g., user's full name) |
|
|
15045
|
+
|
|
15046
|
+
#### Hierarchy Pattern
|
|
15047
|
+
|
|
15048
|
+
\`\`\`
|
|
15049
|
+
Context > Application > Module > [Section] > [Entity Name]
|
|
15050
|
+
\`\`\`
|
|
15051
|
+
|
|
15052
|
+
**Examples:**
|
|
15053
|
+
|
|
15054
|
+
| Page Type | Breadcrumb Items |
|
|
15055
|
+
|-----------|------------------|
|
|
15056
|
+
| List page | Administration > Users |
|
|
15057
|
+
| Detail page | Administration > Users > John Doe |
|
|
15058
|
+
| Nested page | Administration > Users > John Doe > Permissions |
|
|
15059
|
+
|
|
15060
|
+
#### BreadcrumbItem Interface
|
|
15061
|
+
|
|
15062
|
+
\`\`\`tsx
|
|
15063
|
+
interface BreadcrumbItem {
|
|
15064
|
+
label: string; // Display text (use t() for i18n)
|
|
15065
|
+
href?: string; // Link destination (omit for current page)
|
|
15066
|
+
icon?: ReactNode; // Optional icon (Home auto-added to first item)
|
|
15067
|
+
}
|
|
15068
|
+
\`\`\`
|
|
15069
|
+
|
|
15070
|
+
#### Page Position
|
|
15071
|
+
|
|
15072
|
+
Breadcrumb should be the FIRST element in the page content:
|
|
15073
|
+
|
|
15074
|
+
\`\`\`tsx
|
|
15075
|
+
return (
|
|
15076
|
+
<div className="space-y-6">
|
|
15077
|
+
{/* 1. Breadcrumb - always first */}
|
|
15078
|
+
<Breadcrumb items={[...]} />
|
|
15079
|
+
|
|
15080
|
+
{/* 2. Scope Banner (if multi-tenant) */}
|
|
15081
|
+
<ScopeBanner />
|
|
15082
|
+
|
|
15083
|
+
{/* 3. Page Header */}
|
|
15084
|
+
<div className="flex items-center gap-4">...</div>
|
|
15085
|
+
|
|
15086
|
+
{/* 4. Page Content */}
|
|
15087
|
+
...
|
|
15088
|
+
</div>
|
|
15089
|
+
);
|
|
15090
|
+
\`\`\`
|
|
15091
|
+
|
|
11653
15092
|
### Frontend Validation Checks
|
|
11654
15093
|
|
|
11655
15094
|
| Check | Description |
|
|
11656
15095
|
|-------|-------------|
|
|
11657
15096
|
| \`layouts\` | Verify no \`max-w-*\` in content wrapper, standard padding present |
|
|
11658
|
-
| \`tabs\` | Verify \`useTabNavigation\` hook usage for pages with tabs |
|
|
15097
|
+
| \`tabs\` | Verify \`useTabNavigation\` hook usage AND lazy loading patterns for pages with tabs |
|
|
15098
|
+
| \`breadcrumb\` | Verify Breadcrumb component present as first element in page |
|
|
11659
15099
|
|
|
11660
15100
|
---
|
|
11661
15101
|
|
|
@@ -11695,6 +15135,9 @@ const { activeTab, setActiveTab } = useTabNavigation<TabId>('info', VALID_TABS);
|
|
|
11695
15135
|
| Layout padding | \`lg:px-10\` | Standard horizontal padding |
|
|
11696
15136
|
| Tab navigation | \`useTabNavigation\` hook | Sync tabs with URL |
|
|
11697
15137
|
| Tab URL | \`?tab=name\` | Shareable deep links to tabs |
|
|
15138
|
+
| Tab lazy loading | \`enabled: activeTab === 'x'\` | Load data only for active tab |
|
|
15139
|
+
| Breadcrumb | \`<Breadcrumb items={[...]}/>\` | Navigation hierarchy, first page element |
|
|
15140
|
+
| Breadcrumb items | \`{ label, href?, icon? }\` | Last item without href = current page |
|
|
11698
15141
|
|
|
11699
15142
|
---
|
|
11700
15143
|
|
|
@@ -11859,7 +15302,7 @@ Run specific or all checks:
|
|
|
11859
15302
|
}
|
|
11860
15303
|
|
|
11861
15304
|
// src/resources/project-info.ts
|
|
11862
|
-
import
|
|
15305
|
+
import path23 from "path";
|
|
11863
15306
|
var projectInfoResourceTemplate = {
|
|
11864
15307
|
uri: "smartstack://project",
|
|
11865
15308
|
name: "SmartStack Project Info",
|
|
@@ -11896,16 +15339,16 @@ async function getProjectInfoResource(config) {
|
|
|
11896
15339
|
lines.push("```");
|
|
11897
15340
|
lines.push(`${projectInfo.name}/`);
|
|
11898
15341
|
if (structure.domain) {
|
|
11899
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
15342
|
+
lines.push(`\u251C\u2500\u2500 ${path23.basename(structure.domain)}/ # Domain layer (entities)`);
|
|
11900
15343
|
}
|
|
11901
15344
|
if (structure.application) {
|
|
11902
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
15345
|
+
lines.push(`\u251C\u2500\u2500 ${path23.basename(structure.application)}/ # Application layer (services)`);
|
|
11903
15346
|
}
|
|
11904
15347
|
if (structure.infrastructure) {
|
|
11905
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
15348
|
+
lines.push(`\u251C\u2500\u2500 ${path23.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
|
|
11906
15349
|
}
|
|
11907
15350
|
if (structure.api) {
|
|
11908
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
15351
|
+
lines.push(`\u251C\u2500\u2500 ${path23.basename(structure.api)}/ # API layer (controllers)`);
|
|
11909
15352
|
}
|
|
11910
15353
|
if (structure.web) {
|
|
11911
15354
|
lines.push(`\u2514\u2500\u2500 web/smartstack-web/ # React frontend`);
|
|
@@ -11918,8 +15361,8 @@ async function getProjectInfoResource(config) {
|
|
|
11918
15361
|
lines.push("| Project | Path |");
|
|
11919
15362
|
lines.push("|---------|------|");
|
|
11920
15363
|
for (const csproj of projectInfo.csprojFiles) {
|
|
11921
|
-
const name =
|
|
11922
|
-
const relativePath =
|
|
15364
|
+
const name = path23.basename(csproj, ".csproj");
|
|
15365
|
+
const relativePath = path23.relative(projectPath, csproj);
|
|
11923
15366
|
lines.push(`| ${name} | \`${relativePath}\` |`);
|
|
11924
15367
|
}
|
|
11925
15368
|
lines.push("");
|
|
@@ -11929,10 +15372,10 @@ async function getProjectInfoResource(config) {
|
|
|
11929
15372
|
cwd: structure.migrations,
|
|
11930
15373
|
ignore: ["*.Designer.cs"]
|
|
11931
15374
|
});
|
|
11932
|
-
const migrations = migrationFiles.map((f) =>
|
|
15375
|
+
const migrations = migrationFiles.map((f) => path23.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
|
|
11933
15376
|
lines.push("## EF Core Migrations");
|
|
11934
15377
|
lines.push("");
|
|
11935
|
-
lines.push(`**Location**: \`${
|
|
15378
|
+
lines.push(`**Location**: \`${path23.relative(projectPath, structure.migrations)}\``);
|
|
11936
15379
|
lines.push(`**Total Migrations**: ${migrations.length}`);
|
|
11937
15380
|
lines.push("");
|
|
11938
15381
|
if (migrations.length > 0) {
|
|
@@ -11967,11 +15410,11 @@ async function getProjectInfoResource(config) {
|
|
|
11967
15410
|
lines.push("dotnet build");
|
|
11968
15411
|
lines.push("");
|
|
11969
15412
|
lines.push("# Run API");
|
|
11970
|
-
lines.push(`cd ${structure.api ?
|
|
15413
|
+
lines.push(`cd ${structure.api ? path23.relative(projectPath, structure.api) : "src/Api"}`);
|
|
11971
15414
|
lines.push("dotnet run");
|
|
11972
15415
|
lines.push("");
|
|
11973
15416
|
lines.push("# Run frontend");
|
|
11974
|
-
lines.push(`cd ${structure.web ?
|
|
15417
|
+
lines.push(`cd ${structure.web ? path23.relative(projectPath, structure.web) : "web"}`);
|
|
11975
15418
|
lines.push("npm run dev");
|
|
11976
15419
|
lines.push("");
|
|
11977
15420
|
lines.push("# Create migration");
|
|
@@ -11994,7 +15437,7 @@ async function getProjectInfoResource(config) {
|
|
|
11994
15437
|
}
|
|
11995
15438
|
|
|
11996
15439
|
// src/resources/api-endpoints.ts
|
|
11997
|
-
import
|
|
15440
|
+
import path24 from "path";
|
|
11998
15441
|
var apiEndpointsResourceTemplate = {
|
|
11999
15442
|
uri: "smartstack://api/",
|
|
12000
15443
|
name: "SmartStack API Endpoints",
|
|
@@ -12019,7 +15462,7 @@ async function getApiEndpointsResource(config, endpointFilter) {
|
|
|
12019
15462
|
}
|
|
12020
15463
|
async function parseController(filePath, _rootPath) {
|
|
12021
15464
|
const content = await readText(filePath);
|
|
12022
|
-
const fileName =
|
|
15465
|
+
const fileName = path24.basename(filePath, ".cs");
|
|
12023
15466
|
const controllerName = fileName.replace("Controller", "");
|
|
12024
15467
|
const endpoints = [];
|
|
12025
15468
|
const routeMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
|
|
@@ -12166,7 +15609,7 @@ function getMethodEmoji(method) {
|
|
|
12166
15609
|
}
|
|
12167
15610
|
|
|
12168
15611
|
// src/resources/db-schema.ts
|
|
12169
|
-
import
|
|
15612
|
+
import path25 from "path";
|
|
12170
15613
|
var dbSchemaResourceTemplate = {
|
|
12171
15614
|
uri: "smartstack://schema/",
|
|
12172
15615
|
name: "SmartStack Database Schema",
|
|
@@ -12182,7 +15625,7 @@ async function getDbSchemaResource(config, tableFilter) {
|
|
|
12182
15625
|
if (structure.domain) {
|
|
12183
15626
|
const entityFiles = await findEntityFiles(structure.domain);
|
|
12184
15627
|
for (const file of entityFiles) {
|
|
12185
|
-
const entity = await
|
|
15628
|
+
const entity = await parseEntity2(file, structure.root, config);
|
|
12186
15629
|
if (entity) {
|
|
12187
15630
|
entities.push(entity);
|
|
12188
15631
|
}
|
|
@@ -12196,7 +15639,7 @@ async function getDbSchemaResource(config, tableFilter) {
|
|
|
12196
15639
|
) : entities;
|
|
12197
15640
|
return formatSchema(filteredEntities, tableFilter, config);
|
|
12198
15641
|
}
|
|
12199
|
-
async function
|
|
15642
|
+
async function parseEntity2(filePath, rootPath, _config) {
|
|
12200
15643
|
const content = await readText(filePath);
|
|
12201
15644
|
const classMatch = content.match(/public\s+(?:class|record)\s+(\w+)(?:\s*:\s*(\w+))?/);
|
|
12202
15645
|
if (!classMatch) return null;
|
|
@@ -12256,7 +15699,7 @@ async function parseEntity(filePath, rootPath, _config) {
|
|
|
12256
15699
|
tableName,
|
|
12257
15700
|
properties,
|
|
12258
15701
|
relationships,
|
|
12259
|
-
file:
|
|
15702
|
+
file: path25.relative(rootPath, filePath)
|
|
12260
15703
|
};
|
|
12261
15704
|
}
|
|
12262
15705
|
async function enrichFromConfigurations(entities, infrastructurePath, _config) {
|
|
@@ -12402,7 +15845,7 @@ function formatSchema(entities, filter, _config) {
|
|
|
12402
15845
|
}
|
|
12403
15846
|
|
|
12404
15847
|
// src/resources/entities.ts
|
|
12405
|
-
import
|
|
15848
|
+
import path26 from "path";
|
|
12406
15849
|
var entitiesResourceTemplate = {
|
|
12407
15850
|
uri: "smartstack://entities/",
|
|
12408
15851
|
name: "SmartStack Entities",
|
|
@@ -12462,7 +15905,7 @@ async function parseEntitySummary(filePath, rootPath, config) {
|
|
|
12462
15905
|
hasSoftDelete,
|
|
12463
15906
|
hasRowVersion,
|
|
12464
15907
|
file: filePath,
|
|
12465
|
-
relativePath:
|
|
15908
|
+
relativePath: path26.relative(rootPath, filePath)
|
|
12466
15909
|
};
|
|
12467
15910
|
}
|
|
12468
15911
|
function inferTableInfo(entityName, config) {
|
|
@@ -12665,7 +16108,11 @@ async function createServer() {
|
|
|
12665
16108
|
analyzeExtensionPointsTool,
|
|
12666
16109
|
// Security & Code Quality Tools
|
|
12667
16110
|
validateSecurityTool,
|
|
12668
|
-
analyzeCodeQualityTool
|
|
16111
|
+
analyzeCodeQualityTool,
|
|
16112
|
+
// Hierarchy Analysis Tools
|
|
16113
|
+
analyzeHierarchyPatternsTool,
|
|
16114
|
+
// Code Review Tool
|
|
16115
|
+
reviewCodeTool
|
|
12669
16116
|
]
|
|
12670
16117
|
};
|
|
12671
16118
|
});
|
|
@@ -12731,6 +16178,14 @@ async function createServer() {
|
|
|
12731
16178
|
case "analyze_code_quality":
|
|
12732
16179
|
result = await handleAnalyzeCodeQuality(args ?? {}, config);
|
|
12733
16180
|
break;
|
|
16181
|
+
// Hierarchy Analysis Tools
|
|
16182
|
+
case "analyze_hierarchy_patterns":
|
|
16183
|
+
result = await handleAnalyzeHierarchyPatterns(args ?? {}, config);
|
|
16184
|
+
break;
|
|
16185
|
+
// Code Review Tool
|
|
16186
|
+
case "review_code":
|
|
16187
|
+
result = await handleReviewCode(args ?? {}, config);
|
|
16188
|
+
break;
|
|
12734
16189
|
default:
|
|
12735
16190
|
throw new Error(`Unknown tool: ${name}`);
|
|
12736
16191
|
}
|