@solid-stack/mason 1.0.23 → 1.0.24
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/bin.cjs +370 -0
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +370 -0
- package/dist/bin.js.map +1 -1
- package/dist/core/lintEngine.d.ts +10 -0
- package/dist/core/lintEngine.d.ts.map +1 -1
- package/dist/index.cjs +370 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +370 -0
- package/dist/index.js.map +1 -1
- package/dist/types/lint.d.ts +1 -1
- package/dist/types/lint.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/bin.cjs
CHANGED
|
@@ -4432,6 +4432,350 @@ function discoverAllFeatures(projectRoot) {
|
|
|
4432
4432
|
dir: path2__default.default.join(featuresBase, e.name)
|
|
4433
4433
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
4434
4434
|
}
|
|
4435
|
+
function extractClassMethods(content, className) {
|
|
4436
|
+
let clean = "";
|
|
4437
|
+
let inLineComment = false;
|
|
4438
|
+
let inBlockComment = false;
|
|
4439
|
+
let inString = null;
|
|
4440
|
+
let escaped = false;
|
|
4441
|
+
for (let i = 0; i < content.length; i++) {
|
|
4442
|
+
const c = content[i];
|
|
4443
|
+
const next = content[i + 1];
|
|
4444
|
+
if (inLineComment) {
|
|
4445
|
+
if (c === "\n") {
|
|
4446
|
+
inLineComment = false;
|
|
4447
|
+
clean += "\n";
|
|
4448
|
+
} else {
|
|
4449
|
+
clean += " ";
|
|
4450
|
+
}
|
|
4451
|
+
continue;
|
|
4452
|
+
}
|
|
4453
|
+
if (inBlockComment) {
|
|
4454
|
+
if (c === "*" && next === "/") {
|
|
4455
|
+
inBlockComment = false;
|
|
4456
|
+
clean += " ";
|
|
4457
|
+
i++;
|
|
4458
|
+
} else {
|
|
4459
|
+
clean += c === "\n" ? "\n" : " ";
|
|
4460
|
+
}
|
|
4461
|
+
continue;
|
|
4462
|
+
}
|
|
4463
|
+
if (inString) {
|
|
4464
|
+
clean += c === "\n" ? "\n" : " ";
|
|
4465
|
+
if (escaped) {
|
|
4466
|
+
escaped = false;
|
|
4467
|
+
} else if (c === "\\") {
|
|
4468
|
+
escaped = true;
|
|
4469
|
+
} else if (c === inString) {
|
|
4470
|
+
inString = null;
|
|
4471
|
+
}
|
|
4472
|
+
continue;
|
|
4473
|
+
}
|
|
4474
|
+
if (c === "/" && next === "/") {
|
|
4475
|
+
inLineComment = true;
|
|
4476
|
+
clean += " ";
|
|
4477
|
+
i++;
|
|
4478
|
+
continue;
|
|
4479
|
+
}
|
|
4480
|
+
if (c === "/" && next === "*") {
|
|
4481
|
+
inBlockComment = true;
|
|
4482
|
+
clean += " ";
|
|
4483
|
+
i++;
|
|
4484
|
+
continue;
|
|
4485
|
+
}
|
|
4486
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
4487
|
+
inString = c;
|
|
4488
|
+
clean += " ";
|
|
4489
|
+
continue;
|
|
4490
|
+
}
|
|
4491
|
+
clean += c;
|
|
4492
|
+
}
|
|
4493
|
+
const classDeclRegex = new RegExp(`(?:export\\s+(?:default\\s+)?)?class\\s+${className}\\b[^{]*\\{`);
|
|
4494
|
+
const match = classDeclRegex.exec(clean);
|
|
4495
|
+
if (!match) return null;
|
|
4496
|
+
const classBodyStart = match.index + match[0].length - 1;
|
|
4497
|
+
const methods = [];
|
|
4498
|
+
let depth = 1;
|
|
4499
|
+
let parenDepth = 0;
|
|
4500
|
+
let atMemberStart = true;
|
|
4501
|
+
const methodRegex = /^\s*(?:@[a-zA-Z0-9_$]+(?:\([^)]*\))?\s+)*(?:(public|private|protected|static)\s+)*(?:(async)\s+)?(?:(get|set)\s+)?(#?[a-zA-Z0-9_$]+)\s*(?:<[^>]+>)?\s*\(/;
|
|
4502
|
+
const arrowRegex = /^\s*(?:(public|private|protected|static)\s+)?(#?[a-zA-Z0-9_$]+)\s*=\s*(?:async\s*)?\(/;
|
|
4503
|
+
for (let i = classBodyStart + 1; i < clean.length && depth > 0; i++) {
|
|
4504
|
+
const c = clean[i];
|
|
4505
|
+
if (c === "{") {
|
|
4506
|
+
depth++;
|
|
4507
|
+
atMemberStart = false;
|
|
4508
|
+
continue;
|
|
4509
|
+
}
|
|
4510
|
+
if (c === "}") {
|
|
4511
|
+
depth--;
|
|
4512
|
+
if (depth === 1) {
|
|
4513
|
+
atMemberStart = true;
|
|
4514
|
+
}
|
|
4515
|
+
continue;
|
|
4516
|
+
}
|
|
4517
|
+
if (depth === 1) {
|
|
4518
|
+
if (c === ";") {
|
|
4519
|
+
atMemberStart = true;
|
|
4520
|
+
parenDepth = 0;
|
|
4521
|
+
continue;
|
|
4522
|
+
}
|
|
4523
|
+
if (c === "(") {
|
|
4524
|
+
parenDepth++;
|
|
4525
|
+
} else if (c === ")") {
|
|
4526
|
+
parenDepth = Math.max(0, parenDepth - 1);
|
|
4527
|
+
}
|
|
4528
|
+
if (atMemberStart && parenDepth === 0) {
|
|
4529
|
+
if (/\s/.test(c)) continue;
|
|
4530
|
+
const slice = clean.slice(i);
|
|
4531
|
+
const methodMatch = slice.match(methodRegex);
|
|
4532
|
+
if (methodMatch) {
|
|
4533
|
+
const fullMatch = methodMatch[0];
|
|
4534
|
+
const beforeParen = fullMatch.slice(0, fullMatch.lastIndexOf("(")).trim();
|
|
4535
|
+
const tokens = beforeParen.split(/\s+/).filter(Boolean);
|
|
4536
|
+
const rawName = tokens[tokens.length - 1] || "";
|
|
4537
|
+
const methodName = rawName.replace(/<.*$/, "");
|
|
4538
|
+
const controlKeywords = ["if", "for", "while", "switch", "catch", "with"];
|
|
4539
|
+
if (!controlKeywords.includes(methodName)) {
|
|
4540
|
+
const isPrivate = tokens.includes("private") || tokens.includes("protected") || methodName.startsWith("#");
|
|
4541
|
+
const isAsync = tokens.includes("async");
|
|
4542
|
+
const isPublic = !isPrivate;
|
|
4543
|
+
const lineNum = content.slice(0, i).split("\n").length;
|
|
4544
|
+
const lineCode = content.split("\n")[lineNum - 1]?.trim() || "";
|
|
4545
|
+
methods.push({
|
|
4546
|
+
name: methodName,
|
|
4547
|
+
isAsync,
|
|
4548
|
+
isPublic,
|
|
4549
|
+
line: lineNum,
|
|
4550
|
+
code: lineCode
|
|
4551
|
+
});
|
|
4552
|
+
i += fullMatch.length - 1;
|
|
4553
|
+
parenDepth = 1;
|
|
4554
|
+
atMemberStart = false;
|
|
4555
|
+
continue;
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
const arrowMatch = slice.match(arrowRegex);
|
|
4559
|
+
if (arrowMatch) {
|
|
4560
|
+
const fullMatch = arrowMatch[0];
|
|
4561
|
+
const beforeEquals = fullMatch.split("=")[0].trim();
|
|
4562
|
+
const tokens = beforeEquals.split(/\s+/).filter(Boolean);
|
|
4563
|
+
const rawName = tokens[tokens.length - 1] || "";
|
|
4564
|
+
const isPrivate = tokens.includes("private") || tokens.includes("protected") || rawName.startsWith("#");
|
|
4565
|
+
const isAsync = fullMatch.includes("async");
|
|
4566
|
+
const isPublic = !isPrivate;
|
|
4567
|
+
const lineNum = content.slice(0, i).split("\n").length;
|
|
4568
|
+
const lineCode = content.split("\n")[lineNum - 1]?.trim() || "";
|
|
4569
|
+
methods.push({
|
|
4570
|
+
name: rawName,
|
|
4571
|
+
isAsync,
|
|
4572
|
+
isPublic,
|
|
4573
|
+
line: lineNum,
|
|
4574
|
+
code: lineCode
|
|
4575
|
+
});
|
|
4576
|
+
i += fullMatch.length - 1;
|
|
4577
|
+
parenDepth = 1;
|
|
4578
|
+
atMemberStart = false;
|
|
4579
|
+
continue;
|
|
4580
|
+
}
|
|
4581
|
+
atMemberStart = false;
|
|
4582
|
+
}
|
|
4583
|
+
}
|
|
4584
|
+
}
|
|
4585
|
+
return methods;
|
|
4586
|
+
}
|
|
4587
|
+
function checkUseCaseShape(featureName, filePath, relativePath, content, addViolation) {
|
|
4588
|
+
const fileName = path2__default.default.basename(filePath);
|
|
4589
|
+
const lines = content.split(/\r?\n/);
|
|
4590
|
+
const classMatch = content.match(/(?:export\s+(?:default\s+)?)?class\s+([A-Za-z0-9_]+)/);
|
|
4591
|
+
if (!classMatch) {
|
|
4592
|
+
addViolation({
|
|
4593
|
+
feature: featureName,
|
|
4594
|
+
layer: "useCases",
|
|
4595
|
+
file: filePath,
|
|
4596
|
+
relativePath,
|
|
4597
|
+
line: 1,
|
|
4598
|
+
category: "Use Case Shape Violation",
|
|
4599
|
+
code: lines[0]?.trim() || "",
|
|
4600
|
+
ruleId: "U_SHAPE_CLASS",
|
|
4601
|
+
severity: "error",
|
|
4602
|
+
advice: `Use case file '${fileName}' must define and export a class.`,
|
|
4603
|
+
fixExample: {
|
|
4604
|
+
bad: "// Missing class definition",
|
|
4605
|
+
good: `export class ${fileName.replace(/\.(ts|js)$/, "")} { ... }`
|
|
4606
|
+
}
|
|
4607
|
+
});
|
|
4608
|
+
return;
|
|
4609
|
+
}
|
|
4610
|
+
const className = classMatch[1];
|
|
4611
|
+
let classLine = 1;
|
|
4612
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4613
|
+
if (new RegExp(`(?:export\\s+(?:default\\s+)?)?class\\s+${className}\\b`).test(lines[i])) {
|
|
4614
|
+
classLine = i + 1;
|
|
4615
|
+
break;
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
if (!/@MakeInjectable\b/.test(content)) {
|
|
4619
|
+
addViolation({
|
|
4620
|
+
feature: featureName,
|
|
4621
|
+
layer: "useCases",
|
|
4622
|
+
file: filePath,
|
|
4623
|
+
relativePath,
|
|
4624
|
+
line: classLine,
|
|
4625
|
+
category: "Use Case Shape Violation",
|
|
4626
|
+
code: lines[classLine - 1]?.trim() || `class ${className}`,
|
|
4627
|
+
ruleId: "U_SHAPE_DECORATOR",
|
|
4628
|
+
severity: "error",
|
|
4629
|
+
advice: `Use case class '${className}' in '${fileName}' must be decorated with @MakeInjectable.`,
|
|
4630
|
+
fixExample: {
|
|
4631
|
+
bad: lines[classLine - 1]?.trim() || `class ${className}`,
|
|
4632
|
+
good: `@MakeInjectable
|
|
4633
|
+
export class ${className} { ... }`
|
|
4634
|
+
}
|
|
4635
|
+
});
|
|
4636
|
+
}
|
|
4637
|
+
const hasAsyncExecute = /(?:public\s+)?async\s+execute\s*(?:<[^>]+>)?\s*\(/.test(content);
|
|
4638
|
+
if (!hasAsyncExecute) {
|
|
4639
|
+
const hasSyncExecute = /(?:public\s+)?execute\s*(?:<[^>]+>)?\s*\(/.test(content);
|
|
4640
|
+
addViolation({
|
|
4641
|
+
feature: featureName,
|
|
4642
|
+
layer: "useCases",
|
|
4643
|
+
file: filePath,
|
|
4644
|
+
relativePath,
|
|
4645
|
+
line: classLine,
|
|
4646
|
+
category: "Use Case Shape Violation",
|
|
4647
|
+
code: lines[classLine - 1]?.trim() || `class ${className}`,
|
|
4648
|
+
ruleId: "U_SHAPE_EXECUTE",
|
|
4649
|
+
severity: "error",
|
|
4650
|
+
advice: hasSyncExecute ? `Use case class '${className}' in '${fileName}' must have an 'async execute' method (found synchronous 'execute').` : `Use case class '${className}' in '${fileName}' must implement an 'async execute(...)' method.`,
|
|
4651
|
+
fixExample: {
|
|
4652
|
+
bad: hasSyncExecute ? "execute(props: ...) { ... }" : `class ${className} { ... }`,
|
|
4653
|
+
good: "async execute(props: ...): Promise<...> { ... }"
|
|
4654
|
+
}
|
|
4655
|
+
});
|
|
4656
|
+
}
|
|
4657
|
+
const classMethods = extractClassMethods(content, className);
|
|
4658
|
+
if (classMethods) {
|
|
4659
|
+
const extraPublicMethods = classMethods.filter(
|
|
4660
|
+
(m) => m.isPublic && m.name !== "constructor" && m.name !== "execute"
|
|
4661
|
+
);
|
|
4662
|
+
for (const m of extraPublicMethods) {
|
|
4663
|
+
addViolation({
|
|
4664
|
+
feature: featureName,
|
|
4665
|
+
layer: "useCases",
|
|
4666
|
+
file: filePath,
|
|
4667
|
+
relativePath,
|
|
4668
|
+
line: m.line,
|
|
4669
|
+
category: "Use Case Shape Violation",
|
|
4670
|
+
code: m.code || `public ${m.name}(...)`,
|
|
4671
|
+
ruleId: "U_SHAPE_ONLY_EXECUTE",
|
|
4672
|
+
severity: "error",
|
|
4673
|
+
advice: `Use case class '${className}' in '${fileName}' has disallowed public method '${m.name}'. The only allowed public methods are constructor and async execute. Extract extra methods into their own use cases, or make them private.`,
|
|
4674
|
+
fixExample: {
|
|
4675
|
+
bad: m.code || `${m.name}(...) { ... }`,
|
|
4676
|
+
good: `// Extract '${m.name}' into its own dedicated Use Case, or make it private if it is internal helper logic:
|
|
4677
|
+
private ${m.name}(...) { ... }`
|
|
4678
|
+
}
|
|
4679
|
+
});
|
|
4680
|
+
}
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
function checkServiceShape(featureName, filePath, relativePath, content, addViolation) {
|
|
4684
|
+
const fileName = path2__default.default.basename(filePath);
|
|
4685
|
+
const lines = content.split(/\r?\n/);
|
|
4686
|
+
const classMatch = content.match(/(?:export\s+(?:default\s+)?)?class\s+([A-Za-z0-9_]+)/);
|
|
4687
|
+
if (!classMatch) {
|
|
4688
|
+
addViolation({
|
|
4689
|
+
feature: featureName,
|
|
4690
|
+
layer: "services",
|
|
4691
|
+
file: filePath,
|
|
4692
|
+
relativePath,
|
|
4693
|
+
line: 1,
|
|
4694
|
+
category: "Service Shape Violation",
|
|
4695
|
+
code: lines[0]?.trim() || "",
|
|
4696
|
+
ruleId: "S_SHAPE_CLASS",
|
|
4697
|
+
severity: "error",
|
|
4698
|
+
advice: `Service file '${fileName}' must define and export a class.`,
|
|
4699
|
+
fixExample: {
|
|
4700
|
+
bad: "// Missing class definition",
|
|
4701
|
+
good: `export class ${fileName.replace(/\.(ts|js)$/, "")} { ... }`
|
|
4702
|
+
}
|
|
4703
|
+
});
|
|
4704
|
+
return;
|
|
4705
|
+
}
|
|
4706
|
+
const className = classMatch[1];
|
|
4707
|
+
let classLine = 1;
|
|
4708
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4709
|
+
if (new RegExp(`(?:export\\s+(?:default\\s+)?)?class\\s+${className}\\b`).test(lines[i])) {
|
|
4710
|
+
classLine = i + 1;
|
|
4711
|
+
break;
|
|
4712
|
+
}
|
|
4713
|
+
}
|
|
4714
|
+
if (!/@MakeInjectable\b/.test(content)) {
|
|
4715
|
+
addViolation({
|
|
4716
|
+
feature: featureName,
|
|
4717
|
+
layer: "services",
|
|
4718
|
+
file: filePath,
|
|
4719
|
+
relativePath,
|
|
4720
|
+
line: classLine,
|
|
4721
|
+
category: "Service Shape Violation",
|
|
4722
|
+
code: lines[classLine - 1]?.trim() || `class ${className}`,
|
|
4723
|
+
ruleId: "S_SHAPE_DECORATOR",
|
|
4724
|
+
severity: "error",
|
|
4725
|
+
advice: `Service class '${className}' in '${fileName}' must be decorated with @MakeInjectable.`,
|
|
4726
|
+
fixExample: {
|
|
4727
|
+
bad: lines[classLine - 1]?.trim() || `class ${className}`,
|
|
4728
|
+
good: `@MakeInjectable
|
|
4729
|
+
export class ${className} { ... }`
|
|
4730
|
+
}
|
|
4731
|
+
});
|
|
4732
|
+
}
|
|
4733
|
+
const hasAsyncExecute = /(?:public\s+)?async\s+execute\s*(?:<[^>]+>)?\s*\(/.test(content);
|
|
4734
|
+
if (!hasAsyncExecute) {
|
|
4735
|
+
const hasSyncExecute = /(?:public\s+)?execute\s*(?:<[^>]+>)?\s*\(/.test(content);
|
|
4736
|
+
addViolation({
|
|
4737
|
+
feature: featureName,
|
|
4738
|
+
layer: "services",
|
|
4739
|
+
file: filePath,
|
|
4740
|
+
relativePath,
|
|
4741
|
+
line: classLine,
|
|
4742
|
+
category: "Service Shape Violation",
|
|
4743
|
+
code: lines[classLine - 1]?.trim() || `class ${className}`,
|
|
4744
|
+
ruleId: "S_SHAPE_EXECUTE",
|
|
4745
|
+
severity: "error",
|
|
4746
|
+
advice: hasSyncExecute ? `Service class '${className}' in '${fileName}' must have an 'async execute' method (found synchronous 'execute').` : `Service class '${className}' in '${fileName}' must implement an 'async execute(...)' method.`,
|
|
4747
|
+
fixExample: {
|
|
4748
|
+
bad: hasSyncExecute ? "execute(props: ...) { ... }" : `class ${className} { ... }`,
|
|
4749
|
+
good: "async execute(props: ...): Promise<...> { ... }"
|
|
4750
|
+
}
|
|
4751
|
+
});
|
|
4752
|
+
}
|
|
4753
|
+
const classMethods = extractClassMethods(content, className);
|
|
4754
|
+
if (classMethods) {
|
|
4755
|
+
const extraPublicMethods = classMethods.filter(
|
|
4756
|
+
(m) => m.isPublic && m.name !== "constructor" && m.name !== "execute"
|
|
4757
|
+
);
|
|
4758
|
+
for (const m of extraPublicMethods) {
|
|
4759
|
+
addViolation({
|
|
4760
|
+
feature: featureName,
|
|
4761
|
+
layer: "services",
|
|
4762
|
+
file: filePath,
|
|
4763
|
+
relativePath,
|
|
4764
|
+
line: m.line,
|
|
4765
|
+
category: "Service Shape Violation",
|
|
4766
|
+
code: m.code || `public ${m.name}(...)`,
|
|
4767
|
+
ruleId: "S_SHAPE_ONLY_EXECUTE",
|
|
4768
|
+
severity: "error",
|
|
4769
|
+
advice: `Service class '${className}' in '${fileName}' has disallowed public method '${m.name}'. The only allowed public methods are constructor and async execute. If a service has multiple public methods, those should be on their own services instead of being cramped in a single service.`,
|
|
4770
|
+
fixExample: {
|
|
4771
|
+
bad: m.code || `${m.name}(...) { ... }`,
|
|
4772
|
+
good: `// Extract '${m.name}' into its own dedicated Service, or make it private if it is internal helper logic:
|
|
4773
|
+
private ${m.name}(...) { ... }`
|
|
4774
|
+
}
|
|
4775
|
+
});
|
|
4776
|
+
}
|
|
4777
|
+
}
|
|
4778
|
+
}
|
|
4435
4779
|
function lintCodebase(options = {}) {
|
|
4436
4780
|
const startTime = Date.now();
|
|
4437
4781
|
const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
|
|
@@ -4475,6 +4819,14 @@ Code: ${v.code}`;
|
|
|
4475
4819
|
if (!layer) continue;
|
|
4476
4820
|
const content = fs9__default.default.readFileSync(file, "utf-8");
|
|
4477
4821
|
const statements = extractImportStatements(content);
|
|
4822
|
+
const baseName = path2__default.default.basename(file);
|
|
4823
|
+
const isBarrel = baseName === "index.ts" || baseName === "index.js";
|
|
4824
|
+
if (layer === "useCases" && !isBarrel) {
|
|
4825
|
+
checkUseCaseShape(feat.name, file, relPath, content, addViolation);
|
|
4826
|
+
}
|
|
4827
|
+
if (layer === "services" && !isBarrel) {
|
|
4828
|
+
checkServiceShape(feat.name, file, relPath, content, addViolation);
|
|
4829
|
+
}
|
|
4478
4830
|
if (layer === "domain") {
|
|
4479
4831
|
const lines = content.split(/\r?\n/);
|
|
4480
4832
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -4885,6 +5237,24 @@ Code: ${v.code}`;
|
|
|
4885
5237
|
}
|
|
4886
5238
|
});
|
|
4887
5239
|
}
|
|
5240
|
+
if (target.targetLayer === "services") {
|
|
5241
|
+
addViolation({
|
|
5242
|
+
feature: feat.name,
|
|
5243
|
+
layer,
|
|
5244
|
+
file,
|
|
5245
|
+
relativePath: relPath,
|
|
5246
|
+
line: stmt.line,
|
|
5247
|
+
category: "Presentation Layer Violation",
|
|
5248
|
+
code: stmt.rawCode,
|
|
5249
|
+
ruleId: "P4_DIRECT_SERVICE",
|
|
5250
|
+
severity: "error",
|
|
5251
|
+
advice: "Presentation handlers must not depend on services directly. They must use use cases, and use cases will then depend on the service.",
|
|
5252
|
+
fixExample: {
|
|
5253
|
+
bad: stmt.rawCode,
|
|
5254
|
+
good: "// Inject a Use Case via static deps instead of a Service."
|
|
5255
|
+
}
|
|
5256
|
+
});
|
|
5257
|
+
}
|
|
4888
5258
|
}
|
|
4889
5259
|
}
|
|
4890
5260
|
}
|