@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/index.js
CHANGED
|
@@ -4886,22 +4886,38 @@ function scanDirFiles(dir, projectRoot) {
|
|
|
4886
4886
|
function classifyFile(fullPath, projectRoot) {
|
|
4887
4887
|
const rel = path2.relative(projectRoot, fullPath).replace(/\\/g, "/");
|
|
4888
4888
|
const featureMatch = rel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/.*)?$/);
|
|
4889
|
-
if (
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4889
|
+
if (featureMatch && featureMatch[1] && featureMatch[2]) {
|
|
4890
|
+
const feature = featureMatch[1];
|
|
4891
|
+
const subfolder = featureMatch[2];
|
|
4892
|
+
let layer;
|
|
4893
|
+
if (subfolder === "domain" || subfolder === "errors") {
|
|
4894
|
+
layer = "domain";
|
|
4895
|
+
} else if (subfolder === "useCases" || subfolder === "usecases") {
|
|
4896
|
+
layer = "useCases";
|
|
4897
|
+
} else if (subfolder === "services") {
|
|
4898
|
+
layer = "services";
|
|
4899
|
+
} else if (subfolder === "infrastructure") {
|
|
4900
|
+
layer = "infrastructure";
|
|
4901
|
+
} else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
|
|
4902
|
+
layer = "presentation";
|
|
4903
|
+
}
|
|
4904
|
+
return { moduleType: "feature", moduleName: feature, feature, layer };
|
|
4905
|
+
}
|
|
4906
|
+
const sharedMatch = rel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
|
|
4907
|
+
if (sharedMatch && sharedMatch[1]) {
|
|
4908
|
+
const moduleName = sharedMatch[1];
|
|
4909
|
+
const rest = sharedMatch[2] || "";
|
|
4910
|
+
let layer = "root";
|
|
4911
|
+
if (rest.startsWith("domain/") || rest === "domain" || rest.startsWith("errors/") || rest === "errors") {
|
|
4912
|
+
layer = "domain";
|
|
4913
|
+
} else if (rest.startsWith("ports/") || rest === "ports") {
|
|
4914
|
+
layer = "ports";
|
|
4915
|
+
} else if (rest.startsWith("infrastructure/") || rest === "infrastructure") {
|
|
4916
|
+
layer = "infrastructure";
|
|
4917
|
+
}
|
|
4918
|
+
return { moduleType: "shared", moduleName, feature: moduleName, layer };
|
|
4919
|
+
}
|
|
4920
|
+
return {};
|
|
4905
4921
|
}
|
|
4906
4922
|
function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
4907
4923
|
if (specifier === "@solid-stack/di" || specifier.startsWith("@solid-stack/di/")) {
|
|
@@ -4956,6 +4972,7 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4956
4972
|
const targetFileName = basePart.replace(/\.(js|ts|mjs|cjs)$/, "");
|
|
4957
4973
|
const isTargetPort = /^I[A-Z]/.test(targetFileName);
|
|
4958
4974
|
const isTargetRepo = isTargetPort && targetFileName.includes("Repo");
|
|
4975
|
+
const isTargetError = targetSubfolder === "errors" || rest.startsWith("errors/") || rest.includes("/errors/") || targetFileName.endsWith("Error") || targetFileName.endsWith("Errors");
|
|
4959
4976
|
return {
|
|
4960
4977
|
isExternal: false,
|
|
4961
4978
|
isDiFramework: false,
|
|
@@ -4964,7 +4981,8 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4964
4981
|
targetSubfolder,
|
|
4965
4982
|
targetFileName,
|
|
4966
4983
|
isTargetPort,
|
|
4967
|
-
isTargetRepo
|
|
4984
|
+
isTargetRepo,
|
|
4985
|
+
isTargetError
|
|
4968
4986
|
};
|
|
4969
4987
|
}
|
|
4970
4988
|
const sharedMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
|
|
@@ -4990,17 +5008,34 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4990
5008
|
function extractImportStatements(content) {
|
|
4991
5009
|
const statements = [];
|
|
4992
5010
|
const lines = content.split(/\r?\n/);
|
|
4993
|
-
const regex = /(?:import\s+(
|
|
5011
|
+
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;
|
|
4994
5012
|
let match;
|
|
4995
5013
|
while ((match = regex.exec(content)) !== null) {
|
|
4996
|
-
const
|
|
5014
|
+
const isTypeOnly = Boolean(match[1] || match[3]);
|
|
5015
|
+
const importClause = (match[2] || match[4] || "").trim();
|
|
5016
|
+
const specifier = match[5];
|
|
4997
5017
|
const charIndex = match.index;
|
|
4998
5018
|
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
4999
5019
|
const rawCode = lines[lineNum - 1]?.trim() || match[0].trim();
|
|
5020
|
+
const importedNames = [];
|
|
5021
|
+
if (importClause) {
|
|
5022
|
+
const cleaned = importClause.replace(/[{}]/g, "");
|
|
5023
|
+
const words = cleaned.split(",").map((s) => s.trim()).filter(Boolean);
|
|
5024
|
+
for (const w of words) {
|
|
5025
|
+
const parts = w.split(/\s+as\s+/);
|
|
5026
|
+
const name = parts[0].replace(/^type\s+/, "").trim();
|
|
5027
|
+
const alias = (parts[1] || name).trim();
|
|
5028
|
+
if (alias && alias !== "*") {
|
|
5029
|
+
importedNames.push(alias);
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
5032
|
+
}
|
|
5000
5033
|
statements.push({
|
|
5001
5034
|
specifier,
|
|
5002
5035
|
line: lineNum,
|
|
5003
|
-
rawCode
|
|
5036
|
+
rawCode,
|
|
5037
|
+
isTypeOnly,
|
|
5038
|
+
importedNames
|
|
5004
5039
|
});
|
|
5005
5040
|
}
|
|
5006
5041
|
return statements;
|
|
@@ -5099,6 +5134,15 @@ function discoverAllFeatures(projectRoot) {
|
|
|
5099
5134
|
dir: path2.join(featuresBase, e.name)
|
|
5100
5135
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
5101
5136
|
}
|
|
5137
|
+
function discoverAllShared(projectRoot) {
|
|
5138
|
+
const sharedBase = resolveSharedDir(projectRoot);
|
|
5139
|
+
if (!fs4.existsSync(sharedBase)) return [];
|
|
5140
|
+
const entries = fs4.readdirSync(sharedBase, { withFileTypes: true });
|
|
5141
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "__tests__" && e.name !== "tests").map((e) => ({
|
|
5142
|
+
name: e.name,
|
|
5143
|
+
dir: path2.join(sharedBase, e.name)
|
|
5144
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
5145
|
+
}
|
|
5102
5146
|
function extractClassMethods(content, className) {
|
|
5103
5147
|
let clean = "";
|
|
5104
5148
|
let inLineComment = false;
|
|
@@ -5448,16 +5492,21 @@ function lintCodebase(options = {}) {
|
|
|
5448
5492
|
const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
|
|
5449
5493
|
const failFast = !options.complete;
|
|
5450
5494
|
const features = discoverAllFeatures(projectRoot);
|
|
5495
|
+
const sharedModules = discoverAllShared(projectRoot);
|
|
5496
|
+
const allModules = [
|
|
5497
|
+
...features.map((f) => ({ ...f, type: "feature" })),
|
|
5498
|
+
...sharedModules.map((s) => ({ ...s, type: "shared" }))
|
|
5499
|
+
];
|
|
5451
5500
|
const graph = new FeatureDependencyGraph();
|
|
5452
5501
|
const allViolations = [];
|
|
5453
5502
|
const results = [];
|
|
5454
5503
|
const detectedCycles = [];
|
|
5455
5504
|
const addViolation = (v) => {
|
|
5456
5505
|
allViolations.push(v);
|
|
5457
|
-
const
|
|
5458
|
-
if (
|
|
5459
|
-
|
|
5460
|
-
|
|
5506
|
+
const moduleResult = results.find((r) => r.featureName === v.feature);
|
|
5507
|
+
if (moduleResult) {
|
|
5508
|
+
moduleResult.violations.push(v);
|
|
5509
|
+
moduleResult.passed = false;
|
|
5461
5510
|
}
|
|
5462
5511
|
if (failFast) {
|
|
5463
5512
|
const errorMsg = `Dependency rule violation detected in ${v.relativePath}:${v.line}:
|
|
@@ -5466,28 +5515,156 @@ Code: ${v.code}`;
|
|
|
5466
5515
|
throw new DependencyRuleError(errorMsg, [v], true);
|
|
5467
5516
|
}
|
|
5468
5517
|
};
|
|
5469
|
-
for (const
|
|
5518
|
+
for (const mod of allModules) {
|
|
5470
5519
|
results.push({
|
|
5471
|
-
|
|
5472
|
-
|
|
5520
|
+
moduleType: mod.type,
|
|
5521
|
+
moduleName: mod.name,
|
|
5522
|
+
featureName: mod.name,
|
|
5523
|
+
featureDir: mod.dir,
|
|
5524
|
+
moduleDir: mod.dir,
|
|
5473
5525
|
scannedFilesCount: 0,
|
|
5474
5526
|
passed: true,
|
|
5475
5527
|
violations: [],
|
|
5476
|
-
layersChecked: ["domain", "useCases", "services", "infrastructure", "presentation"]
|
|
5528
|
+
layersChecked: mod.type === "feature" ? ["domain", "useCases", "services", "infrastructure", "presentation"] : ["domain", "ports", "infrastructure", "root"]
|
|
5477
5529
|
});
|
|
5478
5530
|
}
|
|
5479
|
-
for (const
|
|
5480
|
-
const
|
|
5481
|
-
const allFiles = scanDirFiles(
|
|
5482
|
-
|
|
5531
|
+
for (const mod of allModules) {
|
|
5532
|
+
const modResult = results.find((r) => r.featureName === mod.name);
|
|
5533
|
+
const allFiles = scanDirFiles(mod.dir, projectRoot);
|
|
5534
|
+
modResult.scannedFilesCount = allFiles.length;
|
|
5483
5535
|
for (const file of allFiles) {
|
|
5484
5536
|
const relPath = path2.relative(projectRoot, file).replace(/\\/g, "/");
|
|
5485
|
-
const { layer } = classifyFile(file, projectRoot);
|
|
5486
|
-
if (!layer) continue;
|
|
5537
|
+
const { moduleType, moduleName, layer } = classifyFile(file, projectRoot);
|
|
5538
|
+
if (!layer || !moduleType || !moduleName) continue;
|
|
5487
5539
|
const content = fs4.readFileSync(file, "utf-8");
|
|
5488
5540
|
const statements = extractImportStatements(content);
|
|
5489
5541
|
const baseName = path2.basename(file);
|
|
5490
5542
|
const isBarrel = baseName === "index.ts" || baseName === "index.js";
|
|
5543
|
+
if (moduleType === "shared") {
|
|
5544
|
+
if (layer === "domain") {
|
|
5545
|
+
const lines = content.split(/\r?\n/);
|
|
5546
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5547
|
+
const lineText = lines[i].trim();
|
|
5548
|
+
if (lineText.includes("@MakeInjectable") || lineText.includes("public static deps =")) {
|
|
5549
|
+
addViolation({
|
|
5550
|
+
feature: moduleName,
|
|
5551
|
+
layer,
|
|
5552
|
+
file,
|
|
5553
|
+
relativePath: relPath,
|
|
5554
|
+
line: i + 1,
|
|
5555
|
+
category: "Domain Dependence Violation",
|
|
5556
|
+
code: lineText,
|
|
5557
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
5558
|
+
severity: "error",
|
|
5559
|
+
advice: "Domain layer must have zero DI dependence. Remove @MakeInjectable decorators and static deps declarations.",
|
|
5560
|
+
fixExample: {
|
|
5561
|
+
bad: lineText,
|
|
5562
|
+
good: "// Domain interfaces and value objects must remain plain TypeScript classes/interfaces."
|
|
5563
|
+
}
|
|
5564
|
+
});
|
|
5565
|
+
}
|
|
5566
|
+
}
|
|
5567
|
+
}
|
|
5568
|
+
for (const stmt of statements) {
|
|
5569
|
+
const target = classifyTarget(stmt.specifier, file, projectRoot);
|
|
5570
|
+
if (target.targetFeature) {
|
|
5571
|
+
addViolation({
|
|
5572
|
+
feature: moduleName,
|
|
5573
|
+
layer,
|
|
5574
|
+
file,
|
|
5575
|
+
relativePath: relPath,
|
|
5576
|
+
line: stmt.line,
|
|
5577
|
+
category: "Shared Architecture Violation",
|
|
5578
|
+
code: stmt.rawCode,
|
|
5579
|
+
ruleId: "SF1_FEATURE_DEPENDENCY",
|
|
5580
|
+
severity: "error",
|
|
5581
|
+
advice: `Shared modules ('${moduleName}') are foundational and must NEVER import from feature modules ('${target.targetFeature}').`,
|
|
5582
|
+
fixExample: {
|
|
5583
|
+
bad: stmt.rawCode,
|
|
5584
|
+
good: "// Move shared logic/types to shared/ or communicate via domain events/callbacks."
|
|
5585
|
+
}
|
|
5586
|
+
});
|
|
5587
|
+
}
|
|
5588
|
+
if (target.targetSharedModule && target.targetSharedModule !== moduleName) {
|
|
5589
|
+
if (target.isSharedInfra || target.isSharedPorts) {
|
|
5590
|
+
addViolation({
|
|
5591
|
+
feature: moduleName,
|
|
5592
|
+
layer,
|
|
5593
|
+
file,
|
|
5594
|
+
relativePath: relPath,
|
|
5595
|
+
line: stmt.line,
|
|
5596
|
+
category: "Shared Architecture Violation",
|
|
5597
|
+
code: stmt.rawCode,
|
|
5598
|
+
ruleId: "SS1_SHARED_ENCAPSULATION",
|
|
5599
|
+
severity: "error",
|
|
5600
|
+
advice: `Shared module '${moduleName}' cannot import internal details (ports/infra) of shared module '${target.targetSharedModule}'. Depend only on its root classes or domain types.`,
|
|
5601
|
+
fixExample: {
|
|
5602
|
+
bad: stmt.rawCode,
|
|
5603
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
5604
|
+
}
|
|
5605
|
+
});
|
|
5606
|
+
}
|
|
5607
|
+
}
|
|
5608
|
+
if (layer === "domain") {
|
|
5609
|
+
if (target.targetSharedModule === moduleName && (target.isSharedInfra || target.isSharedPorts || target.isSharedRoot)) {
|
|
5610
|
+
addViolation({
|
|
5611
|
+
feature: moduleName,
|
|
5612
|
+
layer,
|
|
5613
|
+
file,
|
|
5614
|
+
relativePath: relPath,
|
|
5615
|
+
line: stmt.line,
|
|
5616
|
+
category: "Domain Purity Violation",
|
|
5617
|
+
code: stmt.rawCode,
|
|
5618
|
+
ruleId: "SD1_OUTER_IMPORT",
|
|
5619
|
+
severity: "error",
|
|
5620
|
+
advice: `Shared domain layer must not import from its own ports, infrastructure, or root classes.`,
|
|
5621
|
+
fixExample: {
|
|
5622
|
+
bad: stmt.rawCode,
|
|
5623
|
+
good: "// Model pure business types and value objects directly within domain/."
|
|
5624
|
+
}
|
|
5625
|
+
});
|
|
5626
|
+
}
|
|
5627
|
+
if (target.isDiFramework) {
|
|
5628
|
+
addViolation({
|
|
5629
|
+
feature: moduleName,
|
|
5630
|
+
layer,
|
|
5631
|
+
file,
|
|
5632
|
+
relativePath: relPath,
|
|
5633
|
+
line: stmt.line,
|
|
5634
|
+
category: "Domain Dependence Violation",
|
|
5635
|
+
code: stmt.rawCode,
|
|
5636
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
5637
|
+
severity: "error",
|
|
5638
|
+
advice: "Domain layer must have zero DI dependence. Domain interfaces and value objects are plain classes/types.",
|
|
5639
|
+
fixExample: {
|
|
5640
|
+
bad: stmt.rawCode,
|
|
5641
|
+
good: "// Remove @solid-stack/di import. Domain entities are pure TypeScript."
|
|
5642
|
+
}
|
|
5643
|
+
});
|
|
5644
|
+
}
|
|
5645
|
+
}
|
|
5646
|
+
if (layer === "ports" && target.targetSharedModule === moduleName && target.isSharedInfra) {
|
|
5647
|
+
addViolation({
|
|
5648
|
+
feature: moduleName,
|
|
5649
|
+
layer,
|
|
5650
|
+
file,
|
|
5651
|
+
relativePath: relPath,
|
|
5652
|
+
line: stmt.line,
|
|
5653
|
+
category: "Shared Architecture Violation",
|
|
5654
|
+
code: stmt.rawCode,
|
|
5655
|
+
ruleId: "SP1_INFRA_IN_PORT",
|
|
5656
|
+
severity: "error",
|
|
5657
|
+
advice: `Shared ports cannot depend on infrastructure. Infrastructure implements ports, not the other way around.`,
|
|
5658
|
+
fixExample: {
|
|
5659
|
+
bad: stmt.rawCode,
|
|
5660
|
+
good: "// Ports define abstract interfaces without concrete infrastructure dependencies."
|
|
5661
|
+
}
|
|
5662
|
+
});
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5665
|
+
continue;
|
|
5666
|
+
}
|
|
5667
|
+
const feat = mod;
|
|
5491
5668
|
if (layer === "useCases" && !isBarrel) {
|
|
5492
5669
|
checkUseCaseShape(feat.name, file, relPath, content, addViolation);
|
|
5493
5670
|
}
|
|
@@ -5556,23 +5733,33 @@ Code: ${v.code}`;
|
|
|
5556
5733
|
}
|
|
5557
5734
|
});
|
|
5558
5735
|
}
|
|
5559
|
-
if (target.targetLayer === "domain" && !target.isTargetPort) {
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5736
|
+
if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetError && !stmt.isTypeOnly) {
|
|
5737
|
+
for (const name of stmt.importedNames) {
|
|
5738
|
+
const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
|
|
5739
|
+
const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
|
|
5740
|
+
const execMatch = createRegex.exec(content) || newRegex.exec(content);
|
|
5741
|
+
if (execMatch) {
|
|
5742
|
+
const charIndex = execMatch.index;
|
|
5743
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
5744
|
+
const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
|
|
5745
|
+
addViolation({
|
|
5746
|
+
feature: feat.name,
|
|
5747
|
+
layer,
|
|
5748
|
+
file,
|
|
5749
|
+
relativePath: relPath,
|
|
5750
|
+
line: lineNum,
|
|
5751
|
+
category: "Cross-Feature Domain Violation",
|
|
5752
|
+
code: lineCode,
|
|
5753
|
+
ruleId: "F1_CROSS_FEATURE_DOMAIN",
|
|
5754
|
+
severity: "error",
|
|
5755
|
+
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.`,
|
|
5756
|
+
fixExample: {
|
|
5757
|
+
bad: lineCode,
|
|
5758
|
+
good: `// Define a Port in ${feat.name}/domain/ and implement the cross-feature communication in ${feat.name}/infrastructure/.`
|
|
5759
|
+
}
|
|
5760
|
+
});
|
|
5574
5761
|
}
|
|
5575
|
-
}
|
|
5762
|
+
}
|
|
5576
5763
|
}
|
|
5577
5764
|
}
|
|
5578
5765
|
if (target.isGlobalConfig) {
|
|
@@ -5960,23 +6147,33 @@ Code: ${v.code}`;
|
|
|
5960
6147
|
}
|
|
5961
6148
|
});
|
|
5962
6149
|
}
|
|
5963
|
-
if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo) {
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
6150
|
+
if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo && !target.isTargetError && !stmt.isTypeOnly) {
|
|
6151
|
+
for (const name of stmt.importedNames) {
|
|
6152
|
+
const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
|
|
6153
|
+
const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
|
|
6154
|
+
const execMatch = createRegex.exec(content) || newRegex.exec(content);
|
|
6155
|
+
if (execMatch) {
|
|
6156
|
+
const charIndex = execMatch.index;
|
|
6157
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
6158
|
+
const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
|
|
6159
|
+
addViolation({
|
|
6160
|
+
feature: feat.name,
|
|
6161
|
+
layer,
|
|
6162
|
+
file,
|
|
6163
|
+
relativePath: relPath,
|
|
6164
|
+
line: lineNum,
|
|
6165
|
+
category: "Presentation Layer Violation",
|
|
6166
|
+
code: lineCode,
|
|
6167
|
+
ruleId: "P5_DIRECT_DOMAIN_ENTITY",
|
|
6168
|
+
severity: "error",
|
|
6169
|
+
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.`,
|
|
6170
|
+
fixExample: {
|
|
6171
|
+
bad: lineCode,
|
|
6172
|
+
good: "// Construct a plain DTO object and pass it into the injected Use Case's execute() method."
|
|
6173
|
+
}
|
|
6174
|
+
});
|
|
5978
6175
|
}
|
|
5979
|
-
}
|
|
6176
|
+
}
|
|
5980
6177
|
}
|
|
5981
6178
|
}
|
|
5982
6179
|
}
|