mcp-software-design 0.1.4 → 0.1.6
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 +12 -0
- package/build/index.js +9 -0
- package/build/smells.js +102 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 25dab4b: `check_smells` no longer reports declarations as duplicated logic: members of `interface`, `type` and `enum` bodies, typed member and parameter lines, fields initialized with a literal or declared with a modifier such as Angular signal inputs, and callback openers whose only argument is the callback (`useEffect(() => {`). Repeated statements, and calls that pass other arguments before a callback, are still reported.
|
|
8
|
+
|
|
9
|
+
## 0.1.5
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
3
15
|
## 0.1.4
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/build/index.js
CHANGED
|
@@ -31,6 +31,15 @@ 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. It also skips declarations rather than
|
|
39
|
+
logic: members of \`interface\` / \`type\` / \`enum\` bodies, typed member and
|
|
40
|
+
parameter lines (\`depthTest?: boolean;\`), fields initialized with a literal or
|
|
41
|
+
declared with a modifier (\`readonly name = input(undefined);\`), and callback
|
|
42
|
+
openers whose only argument is the callback (\`useEffect(() => {\`).
|
|
34
43
|
`;
|
|
35
44
|
/** Render smells as a compact text report. */
|
|
36
45
|
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
|
-
/**
|
|
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,111 @@ 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*;?$/;
|
|
495
|
+
/**
|
|
496
|
+
* What a `{` opens: a `type` / `interface` / `enum` body (members are
|
|
497
|
+
* declarations, never duplicated logic), an object literal, or a block.
|
|
498
|
+
*/
|
|
499
|
+
function braceKind(prefix) {
|
|
500
|
+
if (/^(?:export\s+)?(?:declare\s+)?(?:interface|enum|type)\b/.test(prefix.trim()))
|
|
501
|
+
return "declaration";
|
|
502
|
+
return opensBlock(prefix) ? "block" : "literal";
|
|
503
|
+
}
|
|
504
|
+
const MEMBER_MODIFIERS = "(?:(?:readonly|private|protected|public|override|static|declare|abstract)\\s+)";
|
|
505
|
+
/**
|
|
506
|
+
* A member or parameter declaration: `depthTest?: boolean;`, `worldWind: T,`,
|
|
507
|
+
* `[key: string]: any;`. The type part may be a function type but never a
|
|
508
|
+
* block, so a labelled statement or a `default: return x;` is left alone.
|
|
509
|
+
*/
|
|
510
|
+
const TYPED_DECLARATION = new RegExp(`^${MEMBER_MODIFIERS}*(?:\\[[\\w$]+\\s*:\\s*[^\\]]+\\]|[\\w$#]+)\\??\\s*:\\s*([^={}]+?)\\s*[;,]?$`);
|
|
511
|
+
/** A field with an initializer: `enabled = true;`, `readonly name = input<T>(undefined);`. */
|
|
512
|
+
const INITIALIZED_MEMBER = new RegExp(`^${MEMBER_MODIFIERS}+[\\w$#]+\\??\\s*(?::\\s*[^=]+?)?\\s*=\\s*[^;]*;?$`);
|
|
513
|
+
/** A bare field whose initializer is a literal (strings are already blanked): `highlighted = false;`, `label = ;`. */
|
|
514
|
+
const LITERAL_ASSIGNMENT = /^[\w$#]+\s*(?::\s*[^=]+?)?\s*=\s*(?:true|false|null|undefined|-?\d[\d._]*|\[\s*\]|\{\s*\}|new\s+[\w$.]+\(\s*\)|)\s*;?$/;
|
|
515
|
+
/**
|
|
516
|
+
* A callback opener whose only argument is the callback: `useEffect(() => {`,
|
|
517
|
+
* `effect((onCleanup) => {`, `.then(async (layer) => {`. The logic is on the
|
|
518
|
+
* lines that follow; a call with other arguments before the callback still counts.
|
|
519
|
+
*/
|
|
520
|
+
const CALLBACK_OPENER = /^(?:(?:return|await)\s+|(?:const|let|var)\s+[\w$]+\s*=\s*)?[\w$.]+\s*\(\s*(?:async\s+)?(?:\(\s*[\w$,\s]*\)|[\w$]+)\s*=>\s*\{\s*$/;
|
|
521
|
+
/** True for lines that declare rather than do: members, parameters, literal-initialized fields, callback openers. */
|
|
522
|
+
function isDeclarationLine(trimmed) {
|
|
523
|
+
if (/^(?:case|default)\b/.test(trimmed))
|
|
524
|
+
return false;
|
|
525
|
+
const typed = trimmed.match(TYPED_DECLARATION);
|
|
526
|
+
if (typed && (!typed[1].includes("(") || typed[1].includes("=>")))
|
|
527
|
+
return true;
|
|
528
|
+
return INITIALIZED_MEMBER.test(trimmed) || LITERAL_ASSIGNMENT.test(trimmed) || CALLBACK_OPENER.test(trimmed);
|
|
529
|
+
}
|
|
449
530
|
/** Detect duplicated non-trivial lines (a DRY proxy). */
|
|
450
531
|
function detectDuplication(sanitized, options) {
|
|
451
532
|
const lineCounts = new Map();
|
|
533
|
+
// Innermost open `{`/`[` per line: rows of a lookup table and members of
|
|
534
|
+
// a type declaration are data and shape, not duplicated logic.
|
|
535
|
+
const bracketStack = [];
|
|
452
536
|
for (let lineIndex = 0; lineIndex < sanitized.length; lineIndex++) {
|
|
453
|
-
const
|
|
537
|
+
const line = sanitized[lineIndex];
|
|
538
|
+
const enclosing = bracketStack[bracketStack.length - 1] ?? "block";
|
|
539
|
+
for (let charIndex = 0; charIndex < line.length; charIndex++) {
|
|
540
|
+
const char = line[charIndex];
|
|
541
|
+
if (char === "{")
|
|
542
|
+
bracketStack.push(braceKind(line.slice(0, charIndex)));
|
|
543
|
+
else if (char === "[")
|
|
544
|
+
bracketStack.push("literal");
|
|
545
|
+
else if (char === "}" || char === "]")
|
|
546
|
+
bracketStack.pop();
|
|
547
|
+
}
|
|
548
|
+
if (enclosing !== "block")
|
|
549
|
+
continue;
|
|
550
|
+
const trimmed = line.trim();
|
|
454
551
|
if (trimmed.length < 15)
|
|
455
552
|
continue; // too short to be meaningful
|
|
456
553
|
if (/^[{}()\[\];,]+$/.test(trimmed))
|
|
457
554
|
continue; // pure punctuation
|
|
458
555
|
if (/^(import|from|export|package|using|#include|@)/.test(trimmed))
|
|
459
556
|
continue;
|
|
557
|
+
if (TRIVIAL_CALL.test(trimmed) || isDeclarationLine(trimmed))
|
|
558
|
+
continue;
|
|
460
559
|
const record = lineCounts.get(trimmed) ?? { count: 0, firstLine: lineIndex + 1 };
|
|
461
560
|
record.count++;
|
|
462
561
|
lineCounts.set(trimmed, record);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-software-design",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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",
|