@atlashub/smartstack-mcp 1.20.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 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 maintainability indicators.",
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";