@atlashub/smartstack-mcp 1.20.0 → 1.22.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 +267 -33
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -386,6 +386,9 @@ var QualityMetricSchema = z.enum([
|
|
|
386
386
|
"parameter-count",
|
|
387
387
|
"code-duplication",
|
|
388
388
|
"file-size",
|
|
389
|
+
"unused-members",
|
|
390
|
+
"duplicated-strings",
|
|
391
|
+
"unassigned-fields",
|
|
389
392
|
"all"
|
|
390
393
|
]);
|
|
391
394
|
var AnalyzeCodeQualityInputSchema = z.object({
|
|
@@ -2435,6 +2438,27 @@ Handlebars.registerHelper("camelCase", (str) => {
|
|
|
2435
2438
|
Handlebars.registerHelper("kebabCase", (str) => {
|
|
2436
2439
|
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
2437
2440
|
});
|
|
2441
|
+
function resolveHierarchy(navRoute) {
|
|
2442
|
+
if (!navRoute) {
|
|
2443
|
+
return { context: "", application: "", module: "", domainPath: "", infraPath: "", controllerArea: "" };
|
|
2444
|
+
}
|
|
2445
|
+
const segments = navRoute.split(".");
|
|
2446
|
+
const toPascal = (s) => s.split("-").map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
2447
|
+
const context = segments[0] ? toPascal(segments[0]) : "";
|
|
2448
|
+
const application = segments[1] ? toPascal(segments[1]) : "";
|
|
2449
|
+
const module = segments[2] ? toPascal(segments[2]) : segments[1] ? toPascal(segments[1]) : "";
|
|
2450
|
+
let domainPath = "";
|
|
2451
|
+
if (segments.length >= 3) {
|
|
2452
|
+
domainPath = path8.join(context, application, module);
|
|
2453
|
+
} else if (segments.length === 2) {
|
|
2454
|
+
domainPath = path8.join(context, module);
|
|
2455
|
+
} else if (segments.length === 1) {
|
|
2456
|
+
domainPath = context;
|
|
2457
|
+
}
|
|
2458
|
+
const infraPath = module || "";
|
|
2459
|
+
const controllerArea = context;
|
|
2460
|
+
return { context, application, module, domainPath, infraPath, controllerArea };
|
|
2461
|
+
}
|
|
2438
2462
|
async function handleScaffoldExtension(args, config) {
|
|
2439
2463
|
const input = ScaffoldExtensionInputSchema.parse(args);
|
|
2440
2464
|
const dryRun = input.options?.dryRun || false;
|
|
@@ -2579,17 +2603,21 @@ async function scaffoldFeature(name, options, structure, config, result, dryRun
|
|
|
2579
2603
|
result.instructions.push(`${withRepository ? withValidation ? "5" : "4" : withValidation ? "4" : "3"}. Create migration: \`dotnet ef migrations add ${migrationPrefix}_vX.X.X_XXX_Add${name} --context ${dbContextName}\``);
|
|
2580
2604
|
result.instructions.push(`${withRepository ? withValidation ? "6" : "5" : withValidation ? "5" : "4"}. Run migration: \`dotnet ef database update --context ${dbContextName}\``);
|
|
2581
2605
|
if (!skipComponent) {
|
|
2582
|
-
|
|
2606
|
+
const featureHierarchy = resolveHierarchy(options?.navRoute);
|
|
2607
|
+
const featureComponentPath = featureHierarchy.context && featureHierarchy.module ? `@/components/${featureHierarchy.context.toLowerCase()}/${featureHierarchy.module.toLowerCase()}/${name}` : `./components/${name}`;
|
|
2608
|
+
result.instructions.push(`Import component: \`import { ${name} } from '${featureComponentPath}';\``);
|
|
2583
2609
|
}
|
|
2584
2610
|
}
|
|
2585
2611
|
async function scaffoldService(name, options, structure, config, result, dryRun = false) {
|
|
2586
|
-
const
|
|
2612
|
+
const hierarchy = resolveHierarchy(options?.navRoute);
|
|
2613
|
+
const interfaceNamespace = options?.namespace || `${config.conventions.namespaces.application}.Common.Interfaces`;
|
|
2614
|
+
const implNamespace = hierarchy.infraPath ? `${config.conventions.namespaces.infrastructure}.Services.${hierarchy.infraPath}` : `${config.conventions.namespaces.infrastructure}.Services`;
|
|
2587
2615
|
const methods = options?.methods || ["GetByIdAsync", "GetAllAsync", "CreateAsync", "UpdateAsync", "DeleteAsync"];
|
|
2588
2616
|
const interfaceTemplate = `using System.Threading;
|
|
2589
2617
|
using System.Threading.Tasks;
|
|
2590
2618
|
using System.Collections.Generic;
|
|
2591
2619
|
|
|
2592
|
-
namespace {{
|
|
2620
|
+
namespace {{interfaceNamespace}};
|
|
2593
2621
|
|
|
2594
2622
|
/// <summary>
|
|
2595
2623
|
/// Service interface for {{name}} operations
|
|
@@ -2610,7 +2638,7 @@ using System.Threading.Tasks;
|
|
|
2610
2638
|
using System.Collections.Generic;
|
|
2611
2639
|
using Microsoft.Extensions.Logging;
|
|
2612
2640
|
|
|
2613
|
-
namespace {{
|
|
2641
|
+
namespace {{implNamespace}};
|
|
2614
2642
|
|
|
2615
2643
|
/// <summary>
|
|
2616
2644
|
/// Service implementation for {{name}} operations
|
|
@@ -2640,19 +2668,22 @@ public class {{name}}Service : I{{name}}Service
|
|
|
2640
2668
|
const diTemplate = `// Add to DependencyInjection.cs or ServiceCollectionExtensions.cs:
|
|
2641
2669
|
services.AddScoped<I{{name}}Service, {{name}}Service>();
|
|
2642
2670
|
`;
|
|
2643
|
-
const context = {
|
|
2671
|
+
const context = { interfaceNamespace, implNamespace, name, methods };
|
|
2644
2672
|
const interfaceContent = Handlebars.compile(interfaceTemplate)(context);
|
|
2645
2673
|
const implementationContent = Handlebars.compile(implementationTemplate)(context);
|
|
2646
2674
|
const diContent = Handlebars.compile(diTemplate)(context);
|
|
2647
2675
|
const projectRoot = config.smartstack.projectPath;
|
|
2648
|
-
const
|
|
2649
|
-
const
|
|
2650
|
-
const
|
|
2651
|
-
const
|
|
2676
|
+
const appPath = structure.application || projectRoot;
|
|
2677
|
+
const infraPath = structure.infrastructure || path8.join(projectRoot, "Infrastructure");
|
|
2678
|
+
const interfacesDir = path8.join(appPath, "Common", "Interfaces");
|
|
2679
|
+
const implDir = hierarchy.infraPath ? path8.join(infraPath, "Services", hierarchy.infraPath) : path8.join(infraPath, "Services");
|
|
2680
|
+
const interfacePath = path8.join(interfacesDir, `I${name}Service.cs`);
|
|
2681
|
+
const implementationPath = path8.join(implDir, `${name}Service.cs`);
|
|
2652
2682
|
validatePathSecurity(interfacePath, projectRoot);
|
|
2653
2683
|
validatePathSecurity(implementationPath, projectRoot);
|
|
2654
2684
|
if (!dryRun) {
|
|
2655
|
-
await ensureDirectory(
|
|
2685
|
+
await ensureDirectory(interfacesDir);
|
|
2686
|
+
await ensureDirectory(implDir);
|
|
2656
2687
|
await writeText(interfacePath, interfaceContent);
|
|
2657
2688
|
await writeText(implementationPath, implementationContent);
|
|
2658
2689
|
}
|
|
@@ -2662,7 +2693,8 @@ services.AddScoped<I{{name}}Service, {{name}}Service>();
|
|
|
2662
2693
|
result.instructions.push(diContent);
|
|
2663
2694
|
}
|
|
2664
2695
|
async function scaffoldEntity(name, options, structure, config, result, dryRun = false) {
|
|
2665
|
-
const
|
|
2696
|
+
const hierarchy = resolveHierarchy(options?.navRoute);
|
|
2697
|
+
const namespace = options?.namespace || (hierarchy.domainPath ? `${config.conventions.namespaces.domain}.${hierarchy.domainPath.replace(/[\\/]/g, ".")}` : config.conventions.namespaces.domain);
|
|
2666
2698
|
const baseEntity = options?.baseEntity;
|
|
2667
2699
|
const isSystemEntity = options?.isSystemEntity || false;
|
|
2668
2700
|
const tablePrefix = options?.tablePrefix || "ref_";
|
|
@@ -2892,15 +2924,17 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
|
|
|
2892
2924
|
const entityContent = Handlebars.compile(entityTemplate)(context);
|
|
2893
2925
|
const configContent = Handlebars.compile(configTemplate)(context);
|
|
2894
2926
|
const projectRoot = config.smartstack.projectPath;
|
|
2895
|
-
const
|
|
2896
|
-
const
|
|
2897
|
-
const
|
|
2898
|
-
const
|
|
2927
|
+
const domainBase = structure.domain || path8.join(projectRoot, "Domain");
|
|
2928
|
+
const infraBase = structure.infrastructure || path8.join(projectRoot, "Infrastructure");
|
|
2929
|
+
const entityDir = hierarchy.domainPath ? path8.join(domainBase, hierarchy.domainPath) : domainBase;
|
|
2930
|
+
const configDir = hierarchy.infraPath ? path8.join(infraBase, "Persistence", "Configurations", hierarchy.infraPath) : path8.join(infraBase, "Persistence", "Configurations");
|
|
2931
|
+
const entityFilePath = path8.join(entityDir, `${name}.cs`);
|
|
2932
|
+
const configFilePath = path8.join(configDir, `${name}Configuration.cs`);
|
|
2899
2933
|
validatePathSecurity(entityFilePath, projectRoot);
|
|
2900
2934
|
validatePathSecurity(configFilePath, projectRoot);
|
|
2901
2935
|
if (!dryRun) {
|
|
2902
|
-
await ensureDirectory(
|
|
2903
|
-
await ensureDirectory(
|
|
2936
|
+
await ensureDirectory(entityDir);
|
|
2937
|
+
await ensureDirectory(configDir);
|
|
2904
2938
|
await writeText(entityFilePath, entityContent);
|
|
2905
2939
|
await writeText(configFilePath, configContent);
|
|
2906
2940
|
}
|
|
@@ -3217,7 +3251,8 @@ GO
|
|
|
3217
3251
|
result.instructions.push("- Group memberships (user belongs to parent groups)");
|
|
3218
3252
|
}
|
|
3219
3253
|
async function scaffoldController(name, options, structure, config, result, dryRun = false) {
|
|
3220
|
-
const
|
|
3254
|
+
const hierarchy = resolveHierarchy(options?.navRoute);
|
|
3255
|
+
const namespace = options?.namespace || (hierarchy.controllerArea ? `${config.conventions.namespaces.api}.Controllers.${hierarchy.controllerArea}` : `${config.conventions.namespaces.api}.Controllers`);
|
|
3221
3256
|
const navRoute = options?.navRoute;
|
|
3222
3257
|
const navRouteSuffix = options?.navRouteSuffix;
|
|
3223
3258
|
const routeAttribute = navRoute ? navRouteSuffix ? `[NavRoute("${navRoute}", Suffix = "${navRouteSuffix}")]` : `[NavRoute("${navRoute}")]` : `[Route("api/[controller]")]`;
|
|
@@ -3313,11 +3348,11 @@ public record Update{{name}}Request();
|
|
|
3313
3348
|
const controllerContent = Handlebars.compile(controllerTemplate)(context);
|
|
3314
3349
|
const projectRoot = config.smartstack.projectPath;
|
|
3315
3350
|
const apiPath = structure.api || path8.join(projectRoot, "Api");
|
|
3316
|
-
const
|
|
3317
|
-
const controllerFilePath = path8.join(
|
|
3351
|
+
const controllersDir = hierarchy.controllerArea ? path8.join(apiPath, "Controllers", hierarchy.controllerArea) : path8.join(apiPath, "Controllers");
|
|
3352
|
+
const controllerFilePath = path8.join(controllersDir, `${name}Controller.cs`);
|
|
3318
3353
|
validatePathSecurity(controllerFilePath, projectRoot);
|
|
3319
3354
|
if (!dryRun) {
|
|
3320
|
-
await ensureDirectory(
|
|
3355
|
+
await ensureDirectory(controllersDir);
|
|
3321
3356
|
await writeText(controllerFilePath, controllerContent);
|
|
3322
3357
|
}
|
|
3323
3358
|
result.files.push({ path: controllerFilePath, content: controllerContent, type: "created" });
|
|
@@ -3343,6 +3378,7 @@ public record Update{{name}}Request();
|
|
|
3343
3378
|
}
|
|
3344
3379
|
}
|
|
3345
3380
|
async function scaffoldComponent(name, options, structure, config, result, dryRun = false) {
|
|
3381
|
+
const hierarchy = resolveHierarchy(options?.navRoute);
|
|
3346
3382
|
const componentTemplate = `import React, { useState, useEffect } from 'react';
|
|
3347
3383
|
|
|
3348
3384
|
interface {{name}}Props {
|
|
@@ -3495,7 +3531,8 @@ export function use{{name}}(options: Use{{name}}Options = {}) {
|
|
|
3495
3531
|
const hookContent = Handlebars.compile(hookTemplate)(context);
|
|
3496
3532
|
const projectRoot = config.smartstack.projectPath;
|
|
3497
3533
|
const webPath = structure.web || path8.join(projectRoot, "web");
|
|
3498
|
-
const
|
|
3534
|
+
const componentsBase = path8.join(webPath, "src", "components");
|
|
3535
|
+
const componentsPath = options?.outputPath ? options.outputPath : hierarchy.context && hierarchy.module ? path8.join(componentsBase, hierarchy.context.toLowerCase(), hierarchy.module.toLowerCase()) : componentsBase;
|
|
3499
3536
|
const hooksPath = path8.join(webPath, "src", "hooks");
|
|
3500
3537
|
const componentFilePath = path8.join(componentsPath, `${name}.tsx`);
|
|
3501
3538
|
const hookFilePath = path8.join(hooksPath, `use${name}.ts`);
|
|
@@ -3510,8 +3547,9 @@ export function use{{name}}(options: Use{{name}}Options = {}) {
|
|
|
3510
3547
|
result.files.push({ path: componentFilePath, content: componentContent, type: "created" });
|
|
3511
3548
|
result.files.push({ path: hookFilePath, content: hookContent, type: "created" });
|
|
3512
3549
|
result.instructions.push("Import and use the component:");
|
|
3513
|
-
|
|
3514
|
-
result.instructions.push(`import {
|
|
3550
|
+
const componentImportPath = hierarchy.context && hierarchy.module ? `@/components/${hierarchy.context.toLowerCase()}/${hierarchy.module.toLowerCase()}/${name}` : `./components/${name}`;
|
|
3551
|
+
result.instructions.push(`import { ${name} } from '${componentImportPath}';`);
|
|
3552
|
+
result.instructions.push(`import { use${name} } from '@/hooks/use${name}';`);
|
|
3515
3553
|
}
|
|
3516
3554
|
async function scaffoldTest(name, options, structure, config, result, dryRun = false) {
|
|
3517
3555
|
const isSystemEntity = options?.isSystemEntity || false;
|
|
@@ -3639,7 +3677,8 @@ public class {{name}}ServiceTests
|
|
|
3639
3677
|
result.instructions.push("- FluentAssertions");
|
|
3640
3678
|
}
|
|
3641
3679
|
async function scaffoldDtos(name, options, structure, config, result, dryRun = false) {
|
|
3642
|
-
const
|
|
3680
|
+
const hierarchy = resolveHierarchy(options?.navRoute);
|
|
3681
|
+
const namespace = options?.namespace || (hierarchy.domainPath ? `${config.conventions.namespaces.application}.${hierarchy.domainPath.replace(/[\\/]/g, ".")}.DTOs` : `${config.conventions.namespaces.application}.DTOs`);
|
|
3643
3682
|
const isSystemEntity = options?.isSystemEntity || false;
|
|
3644
3683
|
const properties = options?.entityProperties || [
|
|
3645
3684
|
{ name: "Name", type: "string", required: true, maxLength: 200 },
|
|
@@ -3756,7 +3795,7 @@ public record Update{{name}}Dto
|
|
|
3756
3795
|
const createContent = Handlebars.compile(createDtoTemplate)(context);
|
|
3757
3796
|
const updateContent = Handlebars.compile(updateDtoTemplate)(context);
|
|
3758
3797
|
const basePath = structure.application || config.smartstack.projectPath;
|
|
3759
|
-
const dtosPath = path8.join(basePath, "DTOs", name);
|
|
3798
|
+
const dtosPath = hierarchy.domainPath ? path8.join(basePath, hierarchy.domainPath, "DTOs") : path8.join(basePath, "DTOs", name);
|
|
3760
3799
|
const responseFilePath = path8.join(dtosPath, `${name}ResponseDto.cs`);
|
|
3761
3800
|
const createFilePath = path8.join(dtosPath, `Create${name}Dto.cs`);
|
|
3762
3801
|
const updateFilePath = path8.join(dtosPath, `Update${name}Dto.cs`);
|
|
@@ -3775,7 +3814,8 @@ public record Update{{name}}Dto
|
|
|
3775
3814
|
result.instructions.push(`- Update${name}Dto: For PUT requests`);
|
|
3776
3815
|
}
|
|
3777
3816
|
async function scaffoldValidator(name, options, structure, config, result, dryRun = false) {
|
|
3778
|
-
const
|
|
3817
|
+
const hierarchy = resolveHierarchy(options?.navRoute);
|
|
3818
|
+
const namespace = options?.namespace || (hierarchy.domainPath ? `${config.conventions.namespaces.application}.${hierarchy.domainPath.replace(/[\\/]/g, ".")}.Validators` : `${config.conventions.namespaces.application}.Validators`);
|
|
3779
3819
|
const properties = options?.entityProperties || [
|
|
3780
3820
|
{ name: "Name", type: "string", required: true, maxLength: 200 },
|
|
3781
3821
|
{ name: "Description", type: "string?", required: false, maxLength: 500 }
|
|
@@ -3853,7 +3893,7 @@ public class Update{{name}}DtoValidator : AbstractValidator<Update{{name}}Dto>
|
|
|
3853
3893
|
const createValidatorContent = Handlebars.compile(createValidatorTemplate)(context);
|
|
3854
3894
|
const updateValidatorContent = Handlebars.compile(updateValidatorTemplate)(context);
|
|
3855
3895
|
const basePath = structure.application || config.smartstack.projectPath;
|
|
3856
|
-
const validatorsPath = path8.join(basePath, "Validators");
|
|
3896
|
+
const validatorsPath = hierarchy.domainPath ? path8.join(basePath, hierarchy.domainPath, "Validators") : path8.join(basePath, "Validators");
|
|
3857
3897
|
const createValidatorFilePath = path8.join(validatorsPath, `Create${name}DtoValidator.cs`);
|
|
3858
3898
|
const updateValidatorFilePath = path8.join(validatorsPath, `Update${name}DtoValidator.cs`);
|
|
3859
3899
|
if (!dryRun) {
|
|
@@ -3869,6 +3909,7 @@ public class Update{{name}}DtoValidator : AbstractValidator<Update{{name}}Dto>
|
|
|
3869
3909
|
result.instructions.push("Required package: FluentValidation.DependencyInjectionExtensions");
|
|
3870
3910
|
}
|
|
3871
3911
|
async function scaffoldRepository(name, options, structure, config, result, dryRun = false) {
|
|
3912
|
+
const hierarchy = resolveHierarchy(options?.navRoute);
|
|
3872
3913
|
const isSystemEntity = options?.isSystemEntity || false;
|
|
3873
3914
|
const schema = options?.schema || config.conventions.schemas.platform;
|
|
3874
3915
|
const dbContextName = schema === "extensions" ? "ExtensionsDbContext" : "CoreDbContext";
|
|
@@ -3992,11 +4033,13 @@ public class {{name}}Repository : I{{name}}Repository
|
|
|
3992
4033
|
const implementationContent = Handlebars.compile(implementationTemplate)(context);
|
|
3993
4034
|
const appPath = structure.application || config.smartstack.projectPath;
|
|
3994
4035
|
const infraPath = structure.infrastructure || path8.join(config.smartstack.projectPath, "Infrastructure");
|
|
3995
|
-
const
|
|
3996
|
-
const
|
|
4036
|
+
const appRepoDir = hierarchy.infraPath ? path8.join(appPath, "Repositories", hierarchy.infraPath) : path8.join(appPath, "Repositories");
|
|
4037
|
+
const infraRepoDir = hierarchy.infraPath ? path8.join(infraPath, "Repositories", hierarchy.infraPath) : path8.join(infraPath, "Repositories");
|
|
4038
|
+
const interfaceFilePath = path8.join(appRepoDir, `I${name}Repository.cs`);
|
|
4039
|
+
const implementationFilePath = path8.join(infraRepoDir, `${name}Repository.cs`);
|
|
3997
4040
|
if (!dryRun) {
|
|
3998
|
-
await ensureDirectory(
|
|
3999
|
-
await ensureDirectory(
|
|
4041
|
+
await ensureDirectory(appRepoDir);
|
|
4042
|
+
await ensureDirectory(infraRepoDir);
|
|
4000
4043
|
await writeText(interfaceFilePath, interfaceContent);
|
|
4001
4044
|
await writeText(implementationFilePath, implementationContent);
|
|
4002
4045
|
}
|
|
@@ -10866,7 +10909,7 @@ function formatFinding(finding, lines) {
|
|
|
10866
10909
|
import path21 from "path";
|
|
10867
10910
|
var analyzeCodeQualityTool = {
|
|
10868
10911
|
name: "analyze_code_quality",
|
|
10869
|
-
description: "Analyze code quality metrics for SmartStack projects: cognitive complexity, cyclomatic complexity, function size, nesting depth, and
|
|
10912
|
+
description: "Analyze code quality metrics for SmartStack projects: cognitive complexity, cyclomatic complexity, function size, nesting depth, unused members (S1144), duplicated strings (S1192), and unassigned fields (S3459).",
|
|
10870
10913
|
inputSchema: {
|
|
10871
10914
|
type: "object",
|
|
10872
10915
|
properties: {
|
|
@@ -10886,6 +10929,9 @@ var analyzeCodeQualityTool = {
|
|
|
10886
10929
|
"parameter-count",
|
|
10887
10930
|
"code-duplication",
|
|
10888
10931
|
"file-size",
|
|
10932
|
+
"unused-members",
|
|
10933
|
+
"duplicated-strings",
|
|
10934
|
+
"unassigned-fields",
|
|
10889
10935
|
"all"
|
|
10890
10936
|
]
|
|
10891
10937
|
},
|
|
@@ -10938,10 +10984,15 @@ async function handleAnalyzeCodeQuality(args, config) {
|
|
|
10938
10984
|
const projectPath = input.path || config.smartstack.projectPath;
|
|
10939
10985
|
const thresholdLevel = input.threshold;
|
|
10940
10986
|
const thresholds = THRESHOLDS[thresholdLevel];
|
|
10987
|
+
const requestedMetrics = input.metrics || ["all"];
|
|
10988
|
+
const analyzeAll = requestedMetrics.includes("all");
|
|
10941
10989
|
logger.info("Analyzing code quality", { projectPath, threshold: thresholdLevel });
|
|
10942
10990
|
const structure = await findSmartStackStructure(projectPath);
|
|
10943
10991
|
const allFunctionMetrics = [];
|
|
10944
10992
|
const fileMetrics = /* @__PURE__ */ new Map();
|
|
10993
|
+
const fileContents = /* @__PURE__ */ new Map();
|
|
10994
|
+
const allUnusedMembers = [];
|
|
10995
|
+
const allUnassignedFields = [];
|
|
10945
10996
|
const csFiles = await findFiles("**/*.cs", { cwd: structure.root });
|
|
10946
10997
|
const filteredCsFiles = csFiles.filter((f) => !isExcludedPath(f));
|
|
10947
10998
|
for (const file of filteredCsFiles) {
|
|
@@ -10951,6 +11002,13 @@ async function handleAnalyzeCodeQuality(args, config) {
|
|
|
10951
11002
|
const functions = extractCSharpFunctions(content, relPath);
|
|
10952
11003
|
allFunctionMetrics.push(...functions);
|
|
10953
11004
|
fileMetrics.set(relPath, { lineCount, functions: functions.length });
|
|
11005
|
+
fileContents.set(relPath, content);
|
|
11006
|
+
if (analyzeAll || requestedMetrics.includes("unused-members")) {
|
|
11007
|
+
allUnusedMembers.push(...detectUnusedMembers(content, relPath));
|
|
11008
|
+
}
|
|
11009
|
+
if (analyzeAll || requestedMetrics.includes("unassigned-fields")) {
|
|
11010
|
+
allUnassignedFields.push(...detectUnassignedFields(content, relPath));
|
|
11011
|
+
}
|
|
10954
11012
|
}
|
|
10955
11013
|
const tsFiles = await findFiles("**/*.{ts,tsx}", { cwd: structure.root });
|
|
10956
11014
|
const filteredTsFiles = tsFiles.filter((f) => !isExcludedPath(f));
|
|
@@ -10961,6 +11019,7 @@ async function handleAnalyzeCodeQuality(args, config) {
|
|
|
10961
11019
|
const functions = extractTypeScriptFunctions(content, relPath);
|
|
10962
11020
|
allFunctionMetrics.push(...functions);
|
|
10963
11021
|
fileMetrics.set(relPath, { lineCount, functions: functions.length });
|
|
11022
|
+
fileContents.set(relPath, content);
|
|
10964
11023
|
}
|
|
10965
11024
|
const metrics = calculateMetrics(allFunctionMetrics, fileMetrics, thresholds);
|
|
10966
11025
|
const hotspots = identifyHotspots(allFunctionMetrics, fileMetrics, thresholds);
|
|
@@ -10970,6 +11029,13 @@ async function handleAnalyzeCodeQuality(args, config) {
|
|
|
10970
11029
|
metrics,
|
|
10971
11030
|
hotspots
|
|
10972
11031
|
};
|
|
11032
|
+
let duplicatedStrings = [];
|
|
11033
|
+
if (analyzeAll || requestedMetrics.includes("duplicated-strings")) {
|
|
11034
|
+
duplicatedStrings = detectDuplicatedStrings(fileContents);
|
|
11035
|
+
}
|
|
11036
|
+
if (allUnusedMembers.length > 0 || duplicatedStrings.length > 0 || allUnassignedFields.length > 0) {
|
|
11037
|
+
return formatExtendedReport(result, thresholds, allUnusedMembers, duplicatedStrings, allUnassignedFields);
|
|
11038
|
+
}
|
|
10973
11039
|
return formatQualityReport(result, thresholds);
|
|
10974
11040
|
}
|
|
10975
11041
|
function extractCSharpFunctions(content, file) {
|
|
@@ -11311,6 +11377,174 @@ function formatQualityReport(result, thresholds) {
|
|
|
11311
11377
|
}
|
|
11312
11378
|
return lines.join("\n");
|
|
11313
11379
|
}
|
|
11380
|
+
function detectUnusedMembers(content, file) {
|
|
11381
|
+
const unusedMembers = [];
|
|
11382
|
+
const privateMethodPattern = /\b(private|internal)\s+(?:async\s+)?(?:static\s+)?[\w<>,\s\[\]]+\s+(\w+)\s*\(/gm;
|
|
11383
|
+
let match;
|
|
11384
|
+
while ((match = privateMethodPattern.exec(content)) !== null) {
|
|
11385
|
+
const visibility = match[1];
|
|
11386
|
+
const methodName = match[2];
|
|
11387
|
+
const line = getLineNumber2(content, match.index);
|
|
11388
|
+
if (["get", "set", "Dispose", "InitializeComponent"].includes(methodName)) continue;
|
|
11389
|
+
const regex = new RegExp(`\\b${methodName}\\b`, "g");
|
|
11390
|
+
const occurrences = (content.match(regex) || []).length;
|
|
11391
|
+
if (occurrences === 1) {
|
|
11392
|
+
unusedMembers.push({
|
|
11393
|
+
name: methodName,
|
|
11394
|
+
type: "method",
|
|
11395
|
+
visibility,
|
|
11396
|
+
file,
|
|
11397
|
+
line,
|
|
11398
|
+
rule: "S1144"
|
|
11399
|
+
});
|
|
11400
|
+
}
|
|
11401
|
+
}
|
|
11402
|
+
const privateFieldPattern = /\b(private|internal)\s+(?:readonly\s+)?(?:static\s+)?[\w<>,\[\]]+\s+_?(\w+)\s*[;=]/gm;
|
|
11403
|
+
while ((match = privateFieldPattern.exec(content)) !== null) {
|
|
11404
|
+
const visibility = match[1];
|
|
11405
|
+
const fieldName = match[2];
|
|
11406
|
+
const line = getLineNumber2(content, match.index);
|
|
11407
|
+
const regex = new RegExp(`\\b${fieldName}\\b`, "g");
|
|
11408
|
+
const occurrences = (content.match(regex) || []).length;
|
|
11409
|
+
if (occurrences === 1) {
|
|
11410
|
+
unusedMembers.push({
|
|
11411
|
+
name: fieldName,
|
|
11412
|
+
type: "field",
|
|
11413
|
+
visibility,
|
|
11414
|
+
file,
|
|
11415
|
+
line,
|
|
11416
|
+
rule: "S1144"
|
|
11417
|
+
});
|
|
11418
|
+
}
|
|
11419
|
+
}
|
|
11420
|
+
return unusedMembers;
|
|
11421
|
+
}
|
|
11422
|
+
function detectDuplicatedStrings(fileContents) {
|
|
11423
|
+
const stringOccurrences = /* @__PURE__ */ new Map();
|
|
11424
|
+
for (const [file, content] of fileContents) {
|
|
11425
|
+
const stringPattern = /"([^"\\]|\\.){5,}"/g;
|
|
11426
|
+
let match;
|
|
11427
|
+
while ((match = stringPattern.exec(content)) !== null) {
|
|
11428
|
+
const literal = match[0];
|
|
11429
|
+
const line = getLineNumber2(content, match.index);
|
|
11430
|
+
if (isIgnoredStringLiteral(literal)) continue;
|
|
11431
|
+
if (!stringOccurrences.has(literal)) {
|
|
11432
|
+
stringOccurrences.set(literal, []);
|
|
11433
|
+
}
|
|
11434
|
+
stringOccurrences.get(literal).push({ file, line });
|
|
11435
|
+
}
|
|
11436
|
+
}
|
|
11437
|
+
const duplicated = [];
|
|
11438
|
+
for (const [literal, locations] of stringOccurrences) {
|
|
11439
|
+
if (locations.length >= 3) {
|
|
11440
|
+
duplicated.push({
|
|
11441
|
+
literal,
|
|
11442
|
+
occurrences: locations.length,
|
|
11443
|
+
locations: locations.slice(0, 5),
|
|
11444
|
+
// Limit to 5 examples
|
|
11445
|
+
suggestedConstantName: suggestConstantName(literal),
|
|
11446
|
+
rule: "S1192"
|
|
11447
|
+
});
|
|
11448
|
+
}
|
|
11449
|
+
}
|
|
11450
|
+
duplicated.sort((a, b) => b.occurrences - a.occurrences);
|
|
11451
|
+
return duplicated.slice(0, 10);
|
|
11452
|
+
}
|
|
11453
|
+
function isIgnoredStringLiteral(literal) {
|
|
11454
|
+
const ignoredPatterns = [
|
|
11455
|
+
/^"\s*"$/,
|
|
11456
|
+
// Empty or whitespace
|
|
11457
|
+
/^"[,.\-:;\/\\]+"$/,
|
|
11458
|
+
// Punctuation only
|
|
11459
|
+
/^"https?:\/\//,
|
|
11460
|
+
// URLs
|
|
11461
|
+
/^"[a-z]{1,4}:"$/i,
|
|
11462
|
+
// Protocol prefixes
|
|
11463
|
+
/^"\{[0-9]+\}"$/,
|
|
11464
|
+
// Format placeholders
|
|
11465
|
+
/^"[a-z_]+:[a-z_]+"$/i
|
|
11466
|
+
// Resource keys
|
|
11467
|
+
];
|
|
11468
|
+
return ignoredPatterns.some((p) => p.test(literal));
|
|
11469
|
+
}
|
|
11470
|
+
function suggestConstantName(literal) {
|
|
11471
|
+
const content = literal.slice(1, -1);
|
|
11472
|
+
const words = content.replace(/[^a-zA-Z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 0).slice(0, 4);
|
|
11473
|
+
if (words.length === 0) return "STRING_CONSTANT";
|
|
11474
|
+
return words.map((w) => w.toUpperCase()).join("_");
|
|
11475
|
+
}
|
|
11476
|
+
function detectUnassignedFields(content, file) {
|
|
11477
|
+
const unassignedFields = [];
|
|
11478
|
+
const fieldPattern = /\b(private|protected|internal|public)\s+(?:readonly\s+)?(\w+(?:<[^>]+>)?)\s+(\w+)\s*;/gm;
|
|
11479
|
+
let match;
|
|
11480
|
+
while ((match = fieldPattern.exec(content)) !== null) {
|
|
11481
|
+
const fieldType = match[2];
|
|
11482
|
+
const fieldName = match[3];
|
|
11483
|
+
const line = getLineNumber2(content, match.index);
|
|
11484
|
+
const assignmentPattern = new RegExp(`\\b${fieldName}\\s*=`, "g");
|
|
11485
|
+
const assignments = content.match(assignmentPattern) || [];
|
|
11486
|
+
const constructorAssignmentPattern = new RegExp(`this\\.${fieldName}\\s*=`, "g");
|
|
11487
|
+
const constructorAssignments = content.match(constructorAssignmentPattern) || [];
|
|
11488
|
+
if (assignments.length === 0 && constructorAssignments.length === 0) {
|
|
11489
|
+
unassignedFields.push({
|
|
11490
|
+
name: fieldName,
|
|
11491
|
+
type: fieldType,
|
|
11492
|
+
file,
|
|
11493
|
+
line,
|
|
11494
|
+
rule: "S3459"
|
|
11495
|
+
});
|
|
11496
|
+
}
|
|
11497
|
+
}
|
|
11498
|
+
return unassignedFields;
|
|
11499
|
+
}
|
|
11500
|
+
function formatExtendedReport(result, thresholds, unusedMembers, duplicatedStrings, unassignedFields) {
|
|
11501
|
+
let report = formatQualityReport(result, thresholds);
|
|
11502
|
+
if (unusedMembers.length > 0 || duplicatedStrings.length > 0 || unassignedFields.length > 0) {
|
|
11503
|
+
report += "\n\n## SonarCloud-Style Detections\n";
|
|
11504
|
+
if (unusedMembers.length > 0) {
|
|
11505
|
+
report += "\n### \u{1F50D} Unused Private Members (S1144)\n";
|
|
11506
|
+
report += `Found ${unusedMembers.length} unused private members:
|
|
11507
|
+
|
|
11508
|
+
`;
|
|
11509
|
+
for (const member of unusedMembers.slice(0, 10)) {
|
|
11510
|
+
report += `- \`${member.name}\` (${member.type}) in \`${member.file}:${member.line}\`
|
|
11511
|
+
`;
|
|
11512
|
+
}
|
|
11513
|
+
if (unusedMembers.length > 10) {
|
|
11514
|
+
report += `
|
|
11515
|
+
... and ${unusedMembers.length - 10} more
|
|
11516
|
+
`;
|
|
11517
|
+
}
|
|
11518
|
+
}
|
|
11519
|
+
if (duplicatedStrings.length > 0) {
|
|
11520
|
+
report += "\n### \u{1F504} Duplicated String Literals (S1192)\n";
|
|
11521
|
+
report += `Found ${duplicatedStrings.length} duplicated strings (3+ occurrences):
|
|
11522
|
+
|
|
11523
|
+
`;
|
|
11524
|
+
for (const dup of duplicatedStrings) {
|
|
11525
|
+
const truncated = dup.literal.length > 50 ? dup.literal.slice(0, 47) + '..."' : dup.literal;
|
|
11526
|
+
report += `- ${truncated} (${dup.occurrences}x) \u2192 suggested: \`${dup.suggestedConstantName}\`
|
|
11527
|
+
`;
|
|
11528
|
+
}
|
|
11529
|
+
}
|
|
11530
|
+
if (unassignedFields.length > 0) {
|
|
11531
|
+
report += "\n### \u26A0\uFE0F Unassigned Fields (S3459)\n";
|
|
11532
|
+
report += `Found ${unassignedFields.length} fields that are never assigned:
|
|
11533
|
+
|
|
11534
|
+
`;
|
|
11535
|
+
for (const field of unassignedFields.slice(0, 10)) {
|
|
11536
|
+
report += `- \`${field.type} ${field.name}\` in \`${field.file}:${field.line}\`
|
|
11537
|
+
`;
|
|
11538
|
+
}
|
|
11539
|
+
if (unassignedFields.length > 10) {
|
|
11540
|
+
report += `
|
|
11541
|
+
... and ${unassignedFields.length - 10} more
|
|
11542
|
+
`;
|
|
11543
|
+
}
|
|
11544
|
+
}
|
|
11545
|
+
}
|
|
11546
|
+
return report;
|
|
11547
|
+
}
|
|
11314
11548
|
|
|
11315
11549
|
// src/tools/analyze-hierarchy-patterns.ts
|
|
11316
11550
|
import path22 from "path";
|