@solid-stack/mason 1.0.31 → 1.0.32
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 +263 -66
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +263 -66
- package/dist/bin.js.map +1 -1
- package/dist/core/lintEngine.d.ts +10 -1
- package/dist/core/lintEngine.d.ts.map +1 -1
- package/dist/index.cjs +263 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +263 -66
- package/dist/index.js.map +1 -1
- package/dist/types/lint.d.ts +8 -3
- package/dist/types/lint.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/bin.cjs
CHANGED
|
@@ -4906,22 +4906,38 @@ function scanDirFiles(dir, projectRoot) {
|
|
|
4906
4906
|
function classifyFile(fullPath, projectRoot) {
|
|
4907
4907
|
const rel = path2__default.default.relative(projectRoot, fullPath).replace(/\\/g, "/");
|
|
4908
4908
|
const featureMatch = rel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/.*)?$/);
|
|
4909
|
-
if (
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4909
|
+
if (featureMatch && featureMatch[1] && featureMatch[2]) {
|
|
4910
|
+
const feature = featureMatch[1];
|
|
4911
|
+
const subfolder = featureMatch[2];
|
|
4912
|
+
let layer;
|
|
4913
|
+
if (subfolder === "domain" || subfolder === "errors") {
|
|
4914
|
+
layer = "domain";
|
|
4915
|
+
} else if (subfolder === "useCases" || subfolder === "usecases") {
|
|
4916
|
+
layer = "useCases";
|
|
4917
|
+
} else if (subfolder === "services") {
|
|
4918
|
+
layer = "services";
|
|
4919
|
+
} else if (subfolder === "infrastructure") {
|
|
4920
|
+
layer = "infrastructure";
|
|
4921
|
+
} else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
|
|
4922
|
+
layer = "presentation";
|
|
4923
|
+
}
|
|
4924
|
+
return { moduleType: "feature", moduleName: feature, feature, layer };
|
|
4925
|
+
}
|
|
4926
|
+
const sharedMatch = rel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
|
|
4927
|
+
if (sharedMatch && sharedMatch[1]) {
|
|
4928
|
+
const moduleName = sharedMatch[1];
|
|
4929
|
+
const rest = sharedMatch[2] || "";
|
|
4930
|
+
let layer = "root";
|
|
4931
|
+
if (rest.startsWith("domain/") || rest === "domain" || rest.startsWith("errors/") || rest === "errors") {
|
|
4932
|
+
layer = "domain";
|
|
4933
|
+
} else if (rest.startsWith("ports/") || rest === "ports") {
|
|
4934
|
+
layer = "ports";
|
|
4935
|
+
} else if (rest.startsWith("infrastructure/") || rest === "infrastructure") {
|
|
4936
|
+
layer = "infrastructure";
|
|
4937
|
+
}
|
|
4938
|
+
return { moduleType: "shared", moduleName, feature: moduleName, layer };
|
|
4939
|
+
}
|
|
4940
|
+
return {};
|
|
4925
4941
|
}
|
|
4926
4942
|
function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
4927
4943
|
if (specifier === "@solid-stack/di" || specifier.startsWith("@solid-stack/di/")) {
|
|
@@ -4976,6 +4992,7 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4976
4992
|
const targetFileName = basePart.replace(/\.(js|ts|mjs|cjs)$/, "");
|
|
4977
4993
|
const isTargetPort = /^I[A-Z]/.test(targetFileName);
|
|
4978
4994
|
const isTargetRepo = isTargetPort && targetFileName.includes("Repo");
|
|
4995
|
+
const isTargetError = targetSubfolder === "errors" || rest.startsWith("errors/") || rest.includes("/errors/") || targetFileName.endsWith("Error") || targetFileName.endsWith("Errors");
|
|
4979
4996
|
return {
|
|
4980
4997
|
isExternal: false,
|
|
4981
4998
|
isDiFramework: false,
|
|
@@ -4984,7 +5001,8 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4984
5001
|
targetSubfolder,
|
|
4985
5002
|
targetFileName,
|
|
4986
5003
|
isTargetPort,
|
|
4987
|
-
isTargetRepo
|
|
5004
|
+
isTargetRepo,
|
|
5005
|
+
isTargetError
|
|
4988
5006
|
};
|
|
4989
5007
|
}
|
|
4990
5008
|
const sharedMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
|
|
@@ -5010,17 +5028,34 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
5010
5028
|
function extractImportStatements(content) {
|
|
5011
5029
|
const statements = [];
|
|
5012
5030
|
const lines = content.split(/\r?\n/);
|
|
5013
|
-
const regex = /(?:import\s+(
|
|
5031
|
+
const regex = /(?:import\s+(type\s+)?(?:([\s\w*$,{}]+)\s+from\s+)?|export\s+(type\s+)?(?:([\s\w*$,{}]+)\s+from\s+)?|import\s*\(|require\s*\()\s*["']([^"']+)["']/g;
|
|
5014
5032
|
let match;
|
|
5015
5033
|
while ((match = regex.exec(content)) !== null) {
|
|
5016
|
-
const
|
|
5034
|
+
const isTypeOnly = Boolean(match[1] || match[3]);
|
|
5035
|
+
const importClause = (match[2] || match[4] || "").trim();
|
|
5036
|
+
const specifier = match[5];
|
|
5017
5037
|
const charIndex = match.index;
|
|
5018
5038
|
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
5019
5039
|
const rawCode = lines[lineNum - 1]?.trim() || match[0].trim();
|
|
5040
|
+
const importedNames = [];
|
|
5041
|
+
if (importClause) {
|
|
5042
|
+
const cleaned = importClause.replace(/[{}]/g, "");
|
|
5043
|
+
const words = cleaned.split(",").map((s) => s.trim()).filter(Boolean);
|
|
5044
|
+
for (const w of words) {
|
|
5045
|
+
const parts = w.split(/\s+as\s+/);
|
|
5046
|
+
const name = parts[0].replace(/^type\s+/, "").trim();
|
|
5047
|
+
const alias = (parts[1] || name).trim();
|
|
5048
|
+
if (alias && alias !== "*") {
|
|
5049
|
+
importedNames.push(alias);
|
|
5050
|
+
}
|
|
5051
|
+
}
|
|
5052
|
+
}
|
|
5020
5053
|
statements.push({
|
|
5021
5054
|
specifier,
|
|
5022
5055
|
line: lineNum,
|
|
5023
|
-
rawCode
|
|
5056
|
+
rawCode,
|
|
5057
|
+
isTypeOnly,
|
|
5058
|
+
importedNames
|
|
5024
5059
|
});
|
|
5025
5060
|
}
|
|
5026
5061
|
return statements;
|
|
@@ -5119,6 +5154,15 @@ function discoverAllFeatures(projectRoot) {
|
|
|
5119
5154
|
dir: path2__default.default.join(featuresBase, e.name)
|
|
5120
5155
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
5121
5156
|
}
|
|
5157
|
+
function discoverAllShared(projectRoot) {
|
|
5158
|
+
const sharedBase = resolveSharedDir(projectRoot);
|
|
5159
|
+
if (!fs14__default.default.existsSync(sharedBase)) return [];
|
|
5160
|
+
const entries = fs14__default.default.readdirSync(sharedBase, { withFileTypes: true });
|
|
5161
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "__tests__" && e.name !== "tests").map((e) => ({
|
|
5162
|
+
name: e.name,
|
|
5163
|
+
dir: path2__default.default.join(sharedBase, e.name)
|
|
5164
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
5165
|
+
}
|
|
5122
5166
|
function extractClassMethods(content, className) {
|
|
5123
5167
|
let clean = "";
|
|
5124
5168
|
let inLineComment = false;
|
|
@@ -5468,16 +5512,21 @@ function lintCodebase(options = {}) {
|
|
|
5468
5512
|
const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
|
|
5469
5513
|
const failFast = !options.complete;
|
|
5470
5514
|
const features = discoverAllFeatures(projectRoot);
|
|
5515
|
+
const sharedModules = discoverAllShared(projectRoot);
|
|
5516
|
+
const allModules = [
|
|
5517
|
+
...features.map((f) => ({ ...f, type: "feature" })),
|
|
5518
|
+
...sharedModules.map((s) => ({ ...s, type: "shared" }))
|
|
5519
|
+
];
|
|
5471
5520
|
const graph = new FeatureDependencyGraph();
|
|
5472
5521
|
const allViolations = [];
|
|
5473
5522
|
const results = [];
|
|
5474
5523
|
const detectedCycles = [];
|
|
5475
5524
|
const addViolation = (v) => {
|
|
5476
5525
|
allViolations.push(v);
|
|
5477
|
-
const
|
|
5478
|
-
if (
|
|
5479
|
-
|
|
5480
|
-
|
|
5526
|
+
const moduleResult = results.find((r) => r.featureName === v.feature);
|
|
5527
|
+
if (moduleResult) {
|
|
5528
|
+
moduleResult.violations.push(v);
|
|
5529
|
+
moduleResult.passed = false;
|
|
5481
5530
|
}
|
|
5482
5531
|
if (failFast) {
|
|
5483
5532
|
const errorMsg = `Dependency rule violation detected in ${v.relativePath}:${v.line}:
|
|
@@ -5486,28 +5535,156 @@ Code: ${v.code}`;
|
|
|
5486
5535
|
throw new DependencyRuleError(errorMsg, [v], true);
|
|
5487
5536
|
}
|
|
5488
5537
|
};
|
|
5489
|
-
for (const
|
|
5538
|
+
for (const mod of allModules) {
|
|
5490
5539
|
results.push({
|
|
5491
|
-
|
|
5492
|
-
|
|
5540
|
+
moduleType: mod.type,
|
|
5541
|
+
moduleName: mod.name,
|
|
5542
|
+
featureName: mod.name,
|
|
5543
|
+
featureDir: mod.dir,
|
|
5544
|
+
moduleDir: mod.dir,
|
|
5493
5545
|
scannedFilesCount: 0,
|
|
5494
5546
|
passed: true,
|
|
5495
5547
|
violations: [],
|
|
5496
|
-
layersChecked: ["domain", "useCases", "services", "infrastructure", "presentation"]
|
|
5548
|
+
layersChecked: mod.type === "feature" ? ["domain", "useCases", "services", "infrastructure", "presentation"] : ["domain", "ports", "infrastructure", "root"]
|
|
5497
5549
|
});
|
|
5498
5550
|
}
|
|
5499
|
-
for (const
|
|
5500
|
-
const
|
|
5501
|
-
const allFiles = scanDirFiles(
|
|
5502
|
-
|
|
5551
|
+
for (const mod of allModules) {
|
|
5552
|
+
const modResult = results.find((r) => r.featureName === mod.name);
|
|
5553
|
+
const allFiles = scanDirFiles(mod.dir, projectRoot);
|
|
5554
|
+
modResult.scannedFilesCount = allFiles.length;
|
|
5503
5555
|
for (const file of allFiles) {
|
|
5504
5556
|
const relPath = path2__default.default.relative(projectRoot, file).replace(/\\/g, "/");
|
|
5505
|
-
const { layer } = classifyFile(file, projectRoot);
|
|
5506
|
-
if (!layer) continue;
|
|
5557
|
+
const { moduleType, moduleName, layer } = classifyFile(file, projectRoot);
|
|
5558
|
+
if (!layer || !moduleType || !moduleName) continue;
|
|
5507
5559
|
const content = fs14__default.default.readFileSync(file, "utf-8");
|
|
5508
5560
|
const statements = extractImportStatements(content);
|
|
5509
5561
|
const baseName = path2__default.default.basename(file);
|
|
5510
5562
|
const isBarrel = baseName === "index.ts" || baseName === "index.js";
|
|
5563
|
+
if (moduleType === "shared") {
|
|
5564
|
+
if (layer === "domain") {
|
|
5565
|
+
const lines = content.split(/\r?\n/);
|
|
5566
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5567
|
+
const lineText = lines[i].trim();
|
|
5568
|
+
if (lineText.includes("@MakeInjectable") || lineText.includes("public static deps =")) {
|
|
5569
|
+
addViolation({
|
|
5570
|
+
feature: moduleName,
|
|
5571
|
+
layer,
|
|
5572
|
+
file,
|
|
5573
|
+
relativePath: relPath,
|
|
5574
|
+
line: i + 1,
|
|
5575
|
+
category: "Domain Dependence Violation",
|
|
5576
|
+
code: lineText,
|
|
5577
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
5578
|
+
severity: "error",
|
|
5579
|
+
advice: "Domain layer must have zero DI dependence. Remove @MakeInjectable decorators and static deps declarations.",
|
|
5580
|
+
fixExample: {
|
|
5581
|
+
bad: lineText,
|
|
5582
|
+
good: "// Domain interfaces and value objects must remain plain TypeScript classes/interfaces."
|
|
5583
|
+
}
|
|
5584
|
+
});
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
}
|
|
5588
|
+
for (const stmt of statements) {
|
|
5589
|
+
const target = classifyTarget(stmt.specifier, file, projectRoot);
|
|
5590
|
+
if (target.targetFeature) {
|
|
5591
|
+
addViolation({
|
|
5592
|
+
feature: moduleName,
|
|
5593
|
+
layer,
|
|
5594
|
+
file,
|
|
5595
|
+
relativePath: relPath,
|
|
5596
|
+
line: stmt.line,
|
|
5597
|
+
category: "Shared Architecture Violation",
|
|
5598
|
+
code: stmt.rawCode,
|
|
5599
|
+
ruleId: "SF1_FEATURE_DEPENDENCY",
|
|
5600
|
+
severity: "error",
|
|
5601
|
+
advice: `Shared modules ('${moduleName}') are foundational and must NEVER import from feature modules ('${target.targetFeature}').`,
|
|
5602
|
+
fixExample: {
|
|
5603
|
+
bad: stmt.rawCode,
|
|
5604
|
+
good: "// Move shared logic/types to shared/ or communicate via domain events/callbacks."
|
|
5605
|
+
}
|
|
5606
|
+
});
|
|
5607
|
+
}
|
|
5608
|
+
if (target.targetSharedModule && target.targetSharedModule !== moduleName) {
|
|
5609
|
+
if (target.isSharedInfra || target.isSharedPorts) {
|
|
5610
|
+
addViolation({
|
|
5611
|
+
feature: moduleName,
|
|
5612
|
+
layer,
|
|
5613
|
+
file,
|
|
5614
|
+
relativePath: relPath,
|
|
5615
|
+
line: stmt.line,
|
|
5616
|
+
category: "Shared Architecture Violation",
|
|
5617
|
+
code: stmt.rawCode,
|
|
5618
|
+
ruleId: "SS1_SHARED_ENCAPSULATION",
|
|
5619
|
+
severity: "error",
|
|
5620
|
+
advice: `Shared module '${moduleName}' cannot import internal details (ports/infra) of shared module '${target.targetSharedModule}'. Depend only on its root classes or domain types.`,
|
|
5621
|
+
fixExample: {
|
|
5622
|
+
bad: stmt.rawCode,
|
|
5623
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
5624
|
+
}
|
|
5625
|
+
});
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
if (layer === "domain") {
|
|
5629
|
+
if (target.targetSharedModule === moduleName && (target.isSharedInfra || target.isSharedPorts || target.isSharedRoot)) {
|
|
5630
|
+
addViolation({
|
|
5631
|
+
feature: moduleName,
|
|
5632
|
+
layer,
|
|
5633
|
+
file,
|
|
5634
|
+
relativePath: relPath,
|
|
5635
|
+
line: stmt.line,
|
|
5636
|
+
category: "Domain Purity Violation",
|
|
5637
|
+
code: stmt.rawCode,
|
|
5638
|
+
ruleId: "SD1_OUTER_IMPORT",
|
|
5639
|
+
severity: "error",
|
|
5640
|
+
advice: `Shared domain layer must not import from its own ports, infrastructure, or root classes.`,
|
|
5641
|
+
fixExample: {
|
|
5642
|
+
bad: stmt.rawCode,
|
|
5643
|
+
good: "// Model pure business types and value objects directly within domain/."
|
|
5644
|
+
}
|
|
5645
|
+
});
|
|
5646
|
+
}
|
|
5647
|
+
if (target.isDiFramework) {
|
|
5648
|
+
addViolation({
|
|
5649
|
+
feature: moduleName,
|
|
5650
|
+
layer,
|
|
5651
|
+
file,
|
|
5652
|
+
relativePath: relPath,
|
|
5653
|
+
line: stmt.line,
|
|
5654
|
+
category: "Domain Dependence Violation",
|
|
5655
|
+
code: stmt.rawCode,
|
|
5656
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
5657
|
+
severity: "error",
|
|
5658
|
+
advice: "Domain layer must have zero DI dependence. Domain interfaces and value objects are plain classes/types.",
|
|
5659
|
+
fixExample: {
|
|
5660
|
+
bad: stmt.rawCode,
|
|
5661
|
+
good: "// Remove @solid-stack/di import. Domain entities are pure TypeScript."
|
|
5662
|
+
}
|
|
5663
|
+
});
|
|
5664
|
+
}
|
|
5665
|
+
}
|
|
5666
|
+
if (layer === "ports" && target.targetSharedModule === moduleName && target.isSharedInfra) {
|
|
5667
|
+
addViolation({
|
|
5668
|
+
feature: moduleName,
|
|
5669
|
+
layer,
|
|
5670
|
+
file,
|
|
5671
|
+
relativePath: relPath,
|
|
5672
|
+
line: stmt.line,
|
|
5673
|
+
category: "Shared Architecture Violation",
|
|
5674
|
+
code: stmt.rawCode,
|
|
5675
|
+
ruleId: "SP1_INFRA_IN_PORT",
|
|
5676
|
+
severity: "error",
|
|
5677
|
+
advice: `Shared ports cannot depend on infrastructure. Infrastructure implements ports, not the other way around.`,
|
|
5678
|
+
fixExample: {
|
|
5679
|
+
bad: stmt.rawCode,
|
|
5680
|
+
good: "// Ports define abstract interfaces without concrete infrastructure dependencies."
|
|
5681
|
+
}
|
|
5682
|
+
});
|
|
5683
|
+
}
|
|
5684
|
+
}
|
|
5685
|
+
continue;
|
|
5686
|
+
}
|
|
5687
|
+
const feat = mod;
|
|
5511
5688
|
if (layer === "useCases" && !isBarrel) {
|
|
5512
5689
|
checkUseCaseShape(feat.name, file, relPath, content, addViolation);
|
|
5513
5690
|
}
|
|
@@ -5576,23 +5753,33 @@ Code: ${v.code}`;
|
|
|
5576
5753
|
}
|
|
5577
5754
|
});
|
|
5578
5755
|
}
|
|
5579
|
-
if (target.targetLayer === "domain" && !target.isTargetPort) {
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5756
|
+
if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetError && !stmt.isTypeOnly) {
|
|
5757
|
+
for (const name of stmt.importedNames) {
|
|
5758
|
+
const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
|
|
5759
|
+
const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
|
|
5760
|
+
const execMatch = createRegex.exec(content) || newRegex.exec(content);
|
|
5761
|
+
if (execMatch) {
|
|
5762
|
+
const charIndex = execMatch.index;
|
|
5763
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
5764
|
+
const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
|
|
5765
|
+
addViolation({
|
|
5766
|
+
feature: feat.name,
|
|
5767
|
+
layer,
|
|
5768
|
+
file,
|
|
5769
|
+
relativePath: relPath,
|
|
5770
|
+
line: lineNum,
|
|
5771
|
+
category: "Cross-Feature Domain Violation",
|
|
5772
|
+
code: lineCode,
|
|
5773
|
+
ruleId: "F1_CROSS_FEATURE_DOMAIN",
|
|
5774
|
+
severity: "error",
|
|
5775
|
+
advice: `Cannot instantiate domain entities/factories directly from foreign feature '${target.targetFeature}' (found instantiation of '${name}'). This bypasses its use cases and breaks Bounded Context encapsulation. Importing for types or 'instanceof' checks is allowed.`,
|
|
5776
|
+
fixExample: {
|
|
5777
|
+
bad: lineCode,
|
|
5778
|
+
good: `// Define a Port in ${feat.name}/domain/ and implement the cross-feature communication in ${feat.name}/infrastructure/.`
|
|
5779
|
+
}
|
|
5780
|
+
});
|
|
5594
5781
|
}
|
|
5595
|
-
}
|
|
5782
|
+
}
|
|
5596
5783
|
}
|
|
5597
5784
|
}
|
|
5598
5785
|
if (target.isGlobalConfig) {
|
|
@@ -5980,23 +6167,33 @@ Code: ${v.code}`;
|
|
|
5980
6167
|
}
|
|
5981
6168
|
});
|
|
5982
6169
|
}
|
|
5983
|
-
if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo) {
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
6170
|
+
if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo && !target.isTargetError && !stmt.isTypeOnly) {
|
|
6171
|
+
for (const name of stmt.importedNames) {
|
|
6172
|
+
const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
|
|
6173
|
+
const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
|
|
6174
|
+
const execMatch = createRegex.exec(content) || newRegex.exec(content);
|
|
6175
|
+
if (execMatch) {
|
|
6176
|
+
const charIndex = execMatch.index;
|
|
6177
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
6178
|
+
const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
|
|
6179
|
+
addViolation({
|
|
6180
|
+
feature: feat.name,
|
|
6181
|
+
layer,
|
|
6182
|
+
file,
|
|
6183
|
+
relativePath: relPath,
|
|
6184
|
+
line: lineNum,
|
|
6185
|
+
category: "Presentation Layer Violation",
|
|
6186
|
+
code: lineCode,
|
|
6187
|
+
ruleId: "P5_DIRECT_DOMAIN_ENTITY",
|
|
6188
|
+
severity: "error",
|
|
6189
|
+
advice: `Presentation handlers must not directly instantiate domain entities (found instantiation of '${name}'). They must map requests to flat DTOs and pass them to the Use Case. Importing for types or 'instanceof' checks is allowed.`,
|
|
6190
|
+
fixExample: {
|
|
6191
|
+
bad: lineCode,
|
|
6192
|
+
good: "// Construct a plain DTO object and pass it into the injected Use Case's execute() method."
|
|
6193
|
+
}
|
|
6194
|
+
});
|
|
5998
6195
|
}
|
|
5999
|
-
}
|
|
6196
|
+
}
|
|
6000
6197
|
}
|
|
6001
6198
|
}
|
|
6002
6199
|
}
|