mcp-software-design 0.1.4 → 0.1.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 7f60e6c: `check_smells` no longer reports rows of multi-line object and array literals, or calls and throws whose arguments were all string literals, as duplicated logic. Lookup tables, message lines and state names used to account for a third of duplication findings on real code. A line that starts with a closing brace before a control keyword (`} else if (...) {`, `} catch (error) {`) is no longer mistaken for a method header, which produced phantom long-method and too-many-params findings measured from the middle of a function.
8
+
3
9
  ## 0.1.4
4
10
 
5
11
  ### Patch Changes
package/build/index.js CHANGED
@@ -31,6 +31,11 @@ braces, parentheses, and indentation, not a real parser).
31
31
  | large-file | > ${DEFAULTS.maxFileLines} lines | separation-of-concerns |
32
32
 
33
33
  All thresholds are overridable per call on the \`check_smells\` tool.
34
+
35
+ Duplication skips members of multi-line object and array literals (rows of a
36
+ lookup table are data) and calls or throws whose arguments were all string
37
+ literals (\`lines.push('...')\`, \`throw new Error('...')\`), so message and
38
+ state-name lines do not register.
34
39
  `;
35
40
  /** Render smells as a compact text report. */
36
41
  function renderSmells(smells) {
package/build/smells.js CHANGED
@@ -173,9 +173,14 @@ function leadingIdentifier(trimmedLine) {
173
173
  const match = trimmedLine.match(/^([A-Za-z_$][\w$]*)/);
174
174
  return match ? match[1] : "";
175
175
  }
176
- /** True if the (possibly multi-line-joined) text is a function/method header opening a `{`. */
176
+ /**
177
+ * True if the (possibly multi-line-joined) text is a function/method header
178
+ * opening a `{`. A closing brace that starts the line (`} else if (x) {`,
179
+ * `} catch (error) {`) is dropped first so the control keyword behind it is
180
+ * seen; otherwise such a line has no leading identifier and reads as a header.
181
+ */
177
182
  function isBraceMethodHeader(headerText) {
178
- const trimmed = headerText.trim();
183
+ const trimmed = headerText.trim().replace(/^\}\s*/, "");
179
184
  if (!trimmed.endsWith("{"))
180
185
  return false;
181
186
  if (!trimmed.includes("("))
@@ -446,17 +451,76 @@ function detectLargeClass(sanitized, options) {
446
451
  }
447
452
  return findings;
448
453
  }
454
+ /**
455
+ * Whether a `{` opens a block (function or control body, type declaration)
456
+ * rather than an object literal, judged from the sanitized text before it on
457
+ * its line. A header ends in `)` (optionally followed by a return type) or
458
+ * `=>`, a bare `else`/`try`/`do`/`finally`, or names a type; anything after
459
+ * `=`, `(`, `,`, `:`, `return` and the like is a literal. An empty prefix (a
460
+ * `{` opening the line) counts as a block.
461
+ */
462
+ function opensBlock(prefix) {
463
+ const trimmed = prefix.trim();
464
+ if (trimmed === "")
465
+ return true;
466
+ if (/(=>|\belse|\btry|\bdo|\bfinally)$/.test(trimmed))
467
+ return true;
468
+ // A header keeps its `(...)` and may add a return type after it
469
+ // (`foo(): Promise<T | null> {`, `fn f() -> T {`, `func f() error {`);
470
+ // a literal after a call sits behind `=`, `,`, `(`, `?`, `:` or `||`.
471
+ const lastParen = trimmed.lastIndexOf(")");
472
+ if (lastParen !== -1) {
473
+ const tail = trimmed.slice(lastParen + 1).trim();
474
+ if (tail === "")
475
+ return true;
476
+ if (tail === ":")
477
+ return false; // `ok ? f(a) : {`
478
+ if (/^(:|->)\s*\S/.test(tail))
479
+ return true; // typed return
480
+ return /^[\w$<>\[\]*. ]*$/.test(tail); // `throws X`, `error`, `*T`
481
+ }
482
+ if (/^(case|default)\b/.test(trimmed))
483
+ return true;
484
+ const leadingWord = leadingIdentifier(trimmed);
485
+ if (CONTROL_KEYWORDS.has(leadingWord) || TYPE_KEYWORDS.has(leadingWord))
486
+ return true;
487
+ return /\b(class|interface|enum|struct|trait|impl|namespace|module|match)\s+[\w$<>:, ]*$/.test(trimmed);
488
+ }
489
+ /**
490
+ * A call or throw whose arguments were all string literals (blanked by
491
+ * `sanitize`): `lines.push(, );`, `throw new RangeError();`, `this.status.set();`.
492
+ * Such lines are messages and state names, not repeated logic.
493
+ */
494
+ const TRIVIAL_CALL = /^(?:return\s+|await\s+|throw\s+new\s+)?[\w$.]+\s*\(\s*[,\s]*\)\s*;?$/;
449
495
  /** Detect duplicated non-trivial lines (a DRY proxy). */
450
496
  function detectDuplication(sanitized, options) {
451
497
  const lineCounts = new Map();
498
+ // Innermost open `{`/`[` per line, true when it opened an object/array
499
+ // literal: rows of a lookup table are data, not duplicated logic.
500
+ const bracketStack = [];
452
501
  for (let lineIndex = 0; lineIndex < sanitized.length; lineIndex++) {
453
- const trimmed = sanitized[lineIndex].trim();
502
+ const line = sanitized[lineIndex];
503
+ const insideLiteral = bracketStack.length > 0 && bracketStack[bracketStack.length - 1];
504
+ for (let charIndex = 0; charIndex < line.length; charIndex++) {
505
+ const char = line[charIndex];
506
+ if (char === "{")
507
+ bracketStack.push(!opensBlock(line.slice(0, charIndex)));
508
+ else if (char === "[")
509
+ bracketStack.push(true);
510
+ else if (char === "}" || char === "]")
511
+ bracketStack.pop();
512
+ }
513
+ if (insideLiteral)
514
+ continue;
515
+ const trimmed = line.trim();
454
516
  if (trimmed.length < 15)
455
517
  continue; // too short to be meaningful
456
518
  if (/^[{}()\[\];,]+$/.test(trimmed))
457
519
  continue; // pure punctuation
458
520
  if (/^(import|from|export|package|using|#include|@)/.test(trimmed))
459
521
  continue;
522
+ if (TRIVIAL_CALL.test(trimmed))
523
+ continue;
460
524
  const record = lineCounts.get(trimmed) ?? { count: 0, firstLine: lineIndex + 1 };
461
525
  record.count++;
462
526
  lineCounts.set(trimmed, record);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-software-design",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "MCP server that teaches and applies software-design guidance: SOLID/OOP/DRY principles, the 23 GoF design patterns, pattern scaffolding, and heuristic code-smell detection.",
5
5
  "type": "module",
6
6
  "license": "MIT",