@atlashub/smartstack-mcp 1.19.0 → 1.21.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 +195 -1
- 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({
|
|
@@ -10866,7 +10869,7 @@ function formatFinding(finding, lines) {
|
|
|
10866
10869
|
import path21 from "path";
|
|
10867
10870
|
var analyzeCodeQualityTool = {
|
|
10868
10871
|
name: "analyze_code_quality",
|
|
10869
|
-
description: "Analyze code quality metrics for SmartStack projects: cognitive complexity, cyclomatic complexity, function size, nesting depth, and
|
|
10872
|
+
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
10873
|
inputSchema: {
|
|
10871
10874
|
type: "object",
|
|
10872
10875
|
properties: {
|
|
@@ -10886,6 +10889,9 @@ var analyzeCodeQualityTool = {
|
|
|
10886
10889
|
"parameter-count",
|
|
10887
10890
|
"code-duplication",
|
|
10888
10891
|
"file-size",
|
|
10892
|
+
"unused-members",
|
|
10893
|
+
"duplicated-strings",
|
|
10894
|
+
"unassigned-fields",
|
|
10889
10895
|
"all"
|
|
10890
10896
|
]
|
|
10891
10897
|
},
|
|
@@ -10938,10 +10944,15 @@ async function handleAnalyzeCodeQuality(args, config) {
|
|
|
10938
10944
|
const projectPath = input.path || config.smartstack.projectPath;
|
|
10939
10945
|
const thresholdLevel = input.threshold;
|
|
10940
10946
|
const thresholds = THRESHOLDS[thresholdLevel];
|
|
10947
|
+
const requestedMetrics = input.metrics || ["all"];
|
|
10948
|
+
const analyzeAll = requestedMetrics.includes("all");
|
|
10941
10949
|
logger.info("Analyzing code quality", { projectPath, threshold: thresholdLevel });
|
|
10942
10950
|
const structure = await findSmartStackStructure(projectPath);
|
|
10943
10951
|
const allFunctionMetrics = [];
|
|
10944
10952
|
const fileMetrics = /* @__PURE__ */ new Map();
|
|
10953
|
+
const fileContents = /* @__PURE__ */ new Map();
|
|
10954
|
+
const allUnusedMembers = [];
|
|
10955
|
+
const allUnassignedFields = [];
|
|
10945
10956
|
const csFiles = await findFiles("**/*.cs", { cwd: structure.root });
|
|
10946
10957
|
const filteredCsFiles = csFiles.filter((f) => !isExcludedPath(f));
|
|
10947
10958
|
for (const file of filteredCsFiles) {
|
|
@@ -10951,6 +10962,13 @@ async function handleAnalyzeCodeQuality(args, config) {
|
|
|
10951
10962
|
const functions = extractCSharpFunctions(content, relPath);
|
|
10952
10963
|
allFunctionMetrics.push(...functions);
|
|
10953
10964
|
fileMetrics.set(relPath, { lineCount, functions: functions.length });
|
|
10965
|
+
fileContents.set(relPath, content);
|
|
10966
|
+
if (analyzeAll || requestedMetrics.includes("unused-members")) {
|
|
10967
|
+
allUnusedMembers.push(...detectUnusedMembers(content, relPath));
|
|
10968
|
+
}
|
|
10969
|
+
if (analyzeAll || requestedMetrics.includes("unassigned-fields")) {
|
|
10970
|
+
allUnassignedFields.push(...detectUnassignedFields(content, relPath));
|
|
10971
|
+
}
|
|
10954
10972
|
}
|
|
10955
10973
|
const tsFiles = await findFiles("**/*.{ts,tsx}", { cwd: structure.root });
|
|
10956
10974
|
const filteredTsFiles = tsFiles.filter((f) => !isExcludedPath(f));
|
|
@@ -10961,6 +10979,7 @@ async function handleAnalyzeCodeQuality(args, config) {
|
|
|
10961
10979
|
const functions = extractTypeScriptFunctions(content, relPath);
|
|
10962
10980
|
allFunctionMetrics.push(...functions);
|
|
10963
10981
|
fileMetrics.set(relPath, { lineCount, functions: functions.length });
|
|
10982
|
+
fileContents.set(relPath, content);
|
|
10964
10983
|
}
|
|
10965
10984
|
const metrics = calculateMetrics(allFunctionMetrics, fileMetrics, thresholds);
|
|
10966
10985
|
const hotspots = identifyHotspots(allFunctionMetrics, fileMetrics, thresholds);
|
|
@@ -10970,6 +10989,13 @@ async function handleAnalyzeCodeQuality(args, config) {
|
|
|
10970
10989
|
metrics,
|
|
10971
10990
|
hotspots
|
|
10972
10991
|
};
|
|
10992
|
+
let duplicatedStrings = [];
|
|
10993
|
+
if (analyzeAll || requestedMetrics.includes("duplicated-strings")) {
|
|
10994
|
+
duplicatedStrings = detectDuplicatedStrings(fileContents);
|
|
10995
|
+
}
|
|
10996
|
+
if (allUnusedMembers.length > 0 || duplicatedStrings.length > 0 || allUnassignedFields.length > 0) {
|
|
10997
|
+
return formatExtendedReport(result, thresholds, allUnusedMembers, duplicatedStrings, allUnassignedFields);
|
|
10998
|
+
}
|
|
10973
10999
|
return formatQualityReport(result, thresholds);
|
|
10974
11000
|
}
|
|
10975
11001
|
function extractCSharpFunctions(content, file) {
|
|
@@ -11311,6 +11337,174 @@ function formatQualityReport(result, thresholds) {
|
|
|
11311
11337
|
}
|
|
11312
11338
|
return lines.join("\n");
|
|
11313
11339
|
}
|
|
11340
|
+
function detectUnusedMembers(content, file) {
|
|
11341
|
+
const unusedMembers = [];
|
|
11342
|
+
const privateMethodPattern = /\b(private|internal)\s+(?:async\s+)?(?:static\s+)?[\w<>,\s\[\]]+\s+(\w+)\s*\(/gm;
|
|
11343
|
+
let match;
|
|
11344
|
+
while ((match = privateMethodPattern.exec(content)) !== null) {
|
|
11345
|
+
const visibility = match[1];
|
|
11346
|
+
const methodName = match[2];
|
|
11347
|
+
const line = getLineNumber2(content, match.index);
|
|
11348
|
+
if (["get", "set", "Dispose", "InitializeComponent"].includes(methodName)) continue;
|
|
11349
|
+
const regex = new RegExp(`\\b${methodName}\\b`, "g");
|
|
11350
|
+
const occurrences = (content.match(regex) || []).length;
|
|
11351
|
+
if (occurrences === 1) {
|
|
11352
|
+
unusedMembers.push({
|
|
11353
|
+
name: methodName,
|
|
11354
|
+
type: "method",
|
|
11355
|
+
visibility,
|
|
11356
|
+
file,
|
|
11357
|
+
line,
|
|
11358
|
+
rule: "S1144"
|
|
11359
|
+
});
|
|
11360
|
+
}
|
|
11361
|
+
}
|
|
11362
|
+
const privateFieldPattern = /\b(private|internal)\s+(?:readonly\s+)?(?:static\s+)?[\w<>,\[\]]+\s+_?(\w+)\s*[;=]/gm;
|
|
11363
|
+
while ((match = privateFieldPattern.exec(content)) !== null) {
|
|
11364
|
+
const visibility = match[1];
|
|
11365
|
+
const fieldName = match[2];
|
|
11366
|
+
const line = getLineNumber2(content, match.index);
|
|
11367
|
+
const regex = new RegExp(`\\b${fieldName}\\b`, "g");
|
|
11368
|
+
const occurrences = (content.match(regex) || []).length;
|
|
11369
|
+
if (occurrences === 1) {
|
|
11370
|
+
unusedMembers.push({
|
|
11371
|
+
name: fieldName,
|
|
11372
|
+
type: "field",
|
|
11373
|
+
visibility,
|
|
11374
|
+
file,
|
|
11375
|
+
line,
|
|
11376
|
+
rule: "S1144"
|
|
11377
|
+
});
|
|
11378
|
+
}
|
|
11379
|
+
}
|
|
11380
|
+
return unusedMembers;
|
|
11381
|
+
}
|
|
11382
|
+
function detectDuplicatedStrings(fileContents) {
|
|
11383
|
+
const stringOccurrences = /* @__PURE__ */ new Map();
|
|
11384
|
+
for (const [file, content] of fileContents) {
|
|
11385
|
+
const stringPattern = /"([^"\\]|\\.){5,}"/g;
|
|
11386
|
+
let match;
|
|
11387
|
+
while ((match = stringPattern.exec(content)) !== null) {
|
|
11388
|
+
const literal = match[0];
|
|
11389
|
+
const line = getLineNumber2(content, match.index);
|
|
11390
|
+
if (isIgnoredStringLiteral(literal)) continue;
|
|
11391
|
+
if (!stringOccurrences.has(literal)) {
|
|
11392
|
+
stringOccurrences.set(literal, []);
|
|
11393
|
+
}
|
|
11394
|
+
stringOccurrences.get(literal).push({ file, line });
|
|
11395
|
+
}
|
|
11396
|
+
}
|
|
11397
|
+
const duplicated = [];
|
|
11398
|
+
for (const [literal, locations] of stringOccurrences) {
|
|
11399
|
+
if (locations.length >= 3) {
|
|
11400
|
+
duplicated.push({
|
|
11401
|
+
literal,
|
|
11402
|
+
occurrences: locations.length,
|
|
11403
|
+
locations: locations.slice(0, 5),
|
|
11404
|
+
// Limit to 5 examples
|
|
11405
|
+
suggestedConstantName: suggestConstantName(literal),
|
|
11406
|
+
rule: "S1192"
|
|
11407
|
+
});
|
|
11408
|
+
}
|
|
11409
|
+
}
|
|
11410
|
+
duplicated.sort((a, b) => b.occurrences - a.occurrences);
|
|
11411
|
+
return duplicated.slice(0, 10);
|
|
11412
|
+
}
|
|
11413
|
+
function isIgnoredStringLiteral(literal) {
|
|
11414
|
+
const ignoredPatterns = [
|
|
11415
|
+
/^"\s*"$/,
|
|
11416
|
+
// Empty or whitespace
|
|
11417
|
+
/^"[,.\-:;\/\\]+"$/,
|
|
11418
|
+
// Punctuation only
|
|
11419
|
+
/^"https?:\/\//,
|
|
11420
|
+
// URLs
|
|
11421
|
+
/^"[a-z]{1,4}:"$/i,
|
|
11422
|
+
// Protocol prefixes
|
|
11423
|
+
/^"\{[0-9]+\}"$/,
|
|
11424
|
+
// Format placeholders
|
|
11425
|
+
/^"[a-z_]+:[a-z_]+"$/i
|
|
11426
|
+
// Resource keys
|
|
11427
|
+
];
|
|
11428
|
+
return ignoredPatterns.some((p) => p.test(literal));
|
|
11429
|
+
}
|
|
11430
|
+
function suggestConstantName(literal) {
|
|
11431
|
+
const content = literal.slice(1, -1);
|
|
11432
|
+
const words = content.replace(/[^a-zA-Z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 0).slice(0, 4);
|
|
11433
|
+
if (words.length === 0) return "STRING_CONSTANT";
|
|
11434
|
+
return words.map((w) => w.toUpperCase()).join("_");
|
|
11435
|
+
}
|
|
11436
|
+
function detectUnassignedFields(content, file) {
|
|
11437
|
+
const unassignedFields = [];
|
|
11438
|
+
const fieldPattern = /\b(private|protected|internal|public)\s+(?:readonly\s+)?(\w+(?:<[^>]+>)?)\s+(\w+)\s*;/gm;
|
|
11439
|
+
let match;
|
|
11440
|
+
while ((match = fieldPattern.exec(content)) !== null) {
|
|
11441
|
+
const fieldType = match[2];
|
|
11442
|
+
const fieldName = match[3];
|
|
11443
|
+
const line = getLineNumber2(content, match.index);
|
|
11444
|
+
const assignmentPattern = new RegExp(`\\b${fieldName}\\s*=`, "g");
|
|
11445
|
+
const assignments = content.match(assignmentPattern) || [];
|
|
11446
|
+
const constructorAssignmentPattern = new RegExp(`this\\.${fieldName}\\s*=`, "g");
|
|
11447
|
+
const constructorAssignments = content.match(constructorAssignmentPattern) || [];
|
|
11448
|
+
if (assignments.length === 0 && constructorAssignments.length === 0) {
|
|
11449
|
+
unassignedFields.push({
|
|
11450
|
+
name: fieldName,
|
|
11451
|
+
type: fieldType,
|
|
11452
|
+
file,
|
|
11453
|
+
line,
|
|
11454
|
+
rule: "S3459"
|
|
11455
|
+
});
|
|
11456
|
+
}
|
|
11457
|
+
}
|
|
11458
|
+
return unassignedFields;
|
|
11459
|
+
}
|
|
11460
|
+
function formatExtendedReport(result, thresholds, unusedMembers, duplicatedStrings, unassignedFields) {
|
|
11461
|
+
let report = formatQualityReport(result, thresholds);
|
|
11462
|
+
if (unusedMembers.length > 0 || duplicatedStrings.length > 0 || unassignedFields.length > 0) {
|
|
11463
|
+
report += "\n\n## SonarCloud-Style Detections\n";
|
|
11464
|
+
if (unusedMembers.length > 0) {
|
|
11465
|
+
report += "\n### \u{1F50D} Unused Private Members (S1144)\n";
|
|
11466
|
+
report += `Found ${unusedMembers.length} unused private members:
|
|
11467
|
+
|
|
11468
|
+
`;
|
|
11469
|
+
for (const member of unusedMembers.slice(0, 10)) {
|
|
11470
|
+
report += `- \`${member.name}\` (${member.type}) in \`${member.file}:${member.line}\`
|
|
11471
|
+
`;
|
|
11472
|
+
}
|
|
11473
|
+
if (unusedMembers.length > 10) {
|
|
11474
|
+
report += `
|
|
11475
|
+
... and ${unusedMembers.length - 10} more
|
|
11476
|
+
`;
|
|
11477
|
+
}
|
|
11478
|
+
}
|
|
11479
|
+
if (duplicatedStrings.length > 0) {
|
|
11480
|
+
report += "\n### \u{1F504} Duplicated String Literals (S1192)\n";
|
|
11481
|
+
report += `Found ${duplicatedStrings.length} duplicated strings (3+ occurrences):
|
|
11482
|
+
|
|
11483
|
+
`;
|
|
11484
|
+
for (const dup of duplicatedStrings) {
|
|
11485
|
+
const truncated = dup.literal.length > 50 ? dup.literal.slice(0, 47) + '..."' : dup.literal;
|
|
11486
|
+
report += `- ${truncated} (${dup.occurrences}x) \u2192 suggested: \`${dup.suggestedConstantName}\`
|
|
11487
|
+
`;
|
|
11488
|
+
}
|
|
11489
|
+
}
|
|
11490
|
+
if (unassignedFields.length > 0) {
|
|
11491
|
+
report += "\n### \u26A0\uFE0F Unassigned Fields (S3459)\n";
|
|
11492
|
+
report += `Found ${unassignedFields.length} fields that are never assigned:
|
|
11493
|
+
|
|
11494
|
+
`;
|
|
11495
|
+
for (const field of unassignedFields.slice(0, 10)) {
|
|
11496
|
+
report += `- \`${field.type} ${field.name}\` in \`${field.file}:${field.line}\`
|
|
11497
|
+
`;
|
|
11498
|
+
}
|
|
11499
|
+
if (unassignedFields.length > 10) {
|
|
11500
|
+
report += `
|
|
11501
|
+
... and ${unassignedFields.length - 10} more
|
|
11502
|
+
`;
|
|
11503
|
+
}
|
|
11504
|
+
}
|
|
11505
|
+
}
|
|
11506
|
+
return report;
|
|
11507
|
+
}
|
|
11314
11508
|
|
|
11315
11509
|
// src/tools/analyze-hierarchy-patterns.ts
|
|
11316
11510
|
import path22 from "path";
|