@solid-stack/mason 1.0.31 → 1.0.33
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 +445 -67
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +445 -67
- package/dist/bin.js.map +1 -1
- package/dist/core/lintEngine.d.ts +11 -1
- package/dist/core/lintEngine.d.ts.map +1 -1
- package/dist/index.cjs +446 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +446 -68
- package/dist/index.js.map +1 -1
- package/dist/types/lint.d.ts +11 -3
- package/dist/types/lint.d.ts.map +1 -1
- package/dist/utils/config.d.ts +5 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -189,6 +189,16 @@ function getConfiguredSharedDir(projectRoot, flags) {
|
|
|
189
189
|
}
|
|
190
190
|
return path2.join(projectRoot, "shared");
|
|
191
191
|
}
|
|
192
|
+
function loadLintConfig(projectRoot) {
|
|
193
|
+
const configPath = path2.join(projectRoot, "mason-lint.config.json");
|
|
194
|
+
if (fs14.existsSync(configPath)) {
|
|
195
|
+
try {
|
|
196
|
+
return JSON.parse(fs14.readFileSync(configPath, "utf-8"));
|
|
197
|
+
} catch {
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { baseErrorPath: "src/errors/AppError.ts" };
|
|
201
|
+
}
|
|
192
202
|
|
|
193
203
|
// src/utils/project.ts
|
|
194
204
|
function findProjectRoot(startDir = process.cwd()) {
|
|
@@ -4877,22 +4887,47 @@ function scanDirFiles(dir, projectRoot) {
|
|
|
4877
4887
|
function classifyFile(fullPath, projectRoot) {
|
|
4878
4888
|
const rel = path2.relative(projectRoot, fullPath).replace(/\\/g, "/");
|
|
4879
4889
|
const featureMatch = rel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/.*)?$/);
|
|
4880
|
-
if (
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4890
|
+
if (featureMatch && featureMatch[1] && featureMatch[2]) {
|
|
4891
|
+
const feature = featureMatch[1];
|
|
4892
|
+
const subfolder = featureMatch[2];
|
|
4893
|
+
let layer;
|
|
4894
|
+
if (subfolder === "domain" || subfolder === "errors") {
|
|
4895
|
+
layer = "domain";
|
|
4896
|
+
} else if (subfolder === "useCases" || subfolder === "usecases") {
|
|
4897
|
+
layer = "useCases";
|
|
4898
|
+
} else if (subfolder === "services") {
|
|
4899
|
+
layer = "services";
|
|
4900
|
+
} else if (subfolder === "infrastructure") {
|
|
4901
|
+
layer = "infrastructure";
|
|
4902
|
+
} else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
|
|
4903
|
+
layer = "presentation";
|
|
4904
|
+
}
|
|
4905
|
+
return { moduleType: "feature", moduleName: feature, feature, layer };
|
|
4906
|
+
}
|
|
4907
|
+
const sharedMatch = rel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
|
|
4908
|
+
if (sharedMatch && sharedMatch[1]) {
|
|
4909
|
+
const moduleName = sharedMatch[1];
|
|
4910
|
+
const rest = sharedMatch[2] || "";
|
|
4911
|
+
let layer = "root";
|
|
4912
|
+
if (rest.startsWith("domain/") || rest === "domain" || rest.startsWith("errors/") || rest === "errors") {
|
|
4913
|
+
layer = "domain";
|
|
4914
|
+
} else if (rest.startsWith("ports/") || rest === "ports") {
|
|
4915
|
+
layer = "ports";
|
|
4916
|
+
} else if (rest.startsWith("infrastructure/") || rest === "infrastructure") {
|
|
4917
|
+
layer = "infrastructure";
|
|
4918
|
+
}
|
|
4919
|
+
return { moduleType: "shared", moduleName, feature: moduleName, layer };
|
|
4920
|
+
}
|
|
4921
|
+
const errorMatch = rel.match(/(?:^|\/)(?:src\/)?errors(?:\/(.*))?$/);
|
|
4922
|
+
if (errorMatch) {
|
|
4923
|
+
return {
|
|
4924
|
+
moduleType: "error",
|
|
4925
|
+
moduleName: "global-errors",
|
|
4926
|
+
feature: "global-errors",
|
|
4927
|
+
layer: "domain"
|
|
4928
|
+
};
|
|
4929
|
+
}
|
|
4930
|
+
return {};
|
|
4896
4931
|
}
|
|
4897
4932
|
function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
4898
4933
|
if (specifier === "@solid-stack/di" || specifier.startsWith("@solid-stack/di/")) {
|
|
@@ -4917,15 +4952,28 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4917
4952
|
resolvedRel = specifier.replace(/^[@~]\//, "src/");
|
|
4918
4953
|
} else if (specifier === "@configs" || specifier.startsWith("@configs/")) {
|
|
4919
4954
|
resolvedRel = "src/" + specifier.slice(1);
|
|
4955
|
+
} else if (specifier.startsWith("@/errors/") || specifier.startsWith("~/errors/") || specifier === "@/errors" || specifier === "~/errors") {
|
|
4956
|
+
resolvedRel = specifier.replace(/^[@~]\//, "src/");
|
|
4957
|
+
} else if (specifier === "@errors" || specifier.startsWith("@errors/")) {
|
|
4958
|
+
resolvedRel = "src/" + specifier.slice(1);
|
|
4920
4959
|
} else if (specifier.startsWith("@/") || specifier.startsWith("~/")) {
|
|
4921
4960
|
resolvedRel = "src/" + specifier.slice(2);
|
|
4922
|
-
} else if (specifier.startsWith("features/") || specifier.startsWith("shared/") || specifier.startsWith("configs/") || specifier === "configs") {
|
|
4961
|
+
} else if (specifier.startsWith("features/") || specifier.startsWith("shared/") || specifier.startsWith("configs/") || specifier === "configs" || specifier.startsWith("errors/") || specifier === "errors") {
|
|
4923
4962
|
resolvedRel = specifier;
|
|
4924
4963
|
}
|
|
4925
4964
|
if (!resolvedRel) {
|
|
4926
4965
|
return { isExternal: true, isDiFramework: false };
|
|
4927
4966
|
}
|
|
4928
4967
|
const isGlobalConfig = resolvedRel.startsWith("src/configs/") || resolvedRel === "src/configs" || resolvedRel.startsWith("configs/") || resolvedRel === "configs";
|
|
4968
|
+
const isGlobalError = resolvedRel.startsWith("src/errors/") || resolvedRel === "src/errors" || resolvedRel.startsWith("errors/") || resolvedRel === "errors";
|
|
4969
|
+
if (isGlobalError) {
|
|
4970
|
+
return {
|
|
4971
|
+
isExternal: false,
|
|
4972
|
+
isDiFramework: false,
|
|
4973
|
+
isGlobalError: true,
|
|
4974
|
+
isTargetError: true
|
|
4975
|
+
};
|
|
4976
|
+
}
|
|
4929
4977
|
const featMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/(.*))?$/);
|
|
4930
4978
|
if (featMatch && featMatch[1] && featMatch[2]) {
|
|
4931
4979
|
const targetFeature = featMatch[1];
|
|
@@ -4947,6 +4995,7 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4947
4995
|
const targetFileName = basePart.replace(/\.(js|ts|mjs|cjs)$/, "");
|
|
4948
4996
|
const isTargetPort = /^I[A-Z]/.test(targetFileName);
|
|
4949
4997
|
const isTargetRepo = isTargetPort && targetFileName.includes("Repo");
|
|
4998
|
+
const isTargetError = targetSubfolder === "errors" || rest.startsWith("errors/") || rest.includes("/errors/") || targetFileName.endsWith("Error") || targetFileName.endsWith("Errors");
|
|
4950
4999
|
return {
|
|
4951
5000
|
isExternal: false,
|
|
4952
5001
|
isDiFramework: false,
|
|
@@ -4955,7 +5004,8 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4955
5004
|
targetSubfolder,
|
|
4956
5005
|
targetFileName,
|
|
4957
5006
|
isTargetPort,
|
|
4958
|
-
isTargetRepo
|
|
5007
|
+
isTargetRepo,
|
|
5008
|
+
isTargetError
|
|
4959
5009
|
};
|
|
4960
5010
|
}
|
|
4961
5011
|
const sharedMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
|
|
@@ -4981,17 +5031,34 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4981
5031
|
function extractImportStatements(content) {
|
|
4982
5032
|
const statements = [];
|
|
4983
5033
|
const lines = content.split(/\r?\n/);
|
|
4984
|
-
const regex = /(?:import\s+(
|
|
5034
|
+
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;
|
|
4985
5035
|
let match;
|
|
4986
5036
|
while ((match = regex.exec(content)) !== null) {
|
|
4987
|
-
const
|
|
5037
|
+
const isTypeOnly = Boolean(match[1] || match[3]);
|
|
5038
|
+
const importClause = (match[2] || match[4] || "").trim();
|
|
5039
|
+
const specifier = match[5];
|
|
4988
5040
|
const charIndex = match.index;
|
|
4989
5041
|
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
4990
5042
|
const rawCode = lines[lineNum - 1]?.trim() || match[0].trim();
|
|
5043
|
+
const importedNames = [];
|
|
5044
|
+
if (importClause) {
|
|
5045
|
+
const cleaned = importClause.replace(/[{}]/g, "");
|
|
5046
|
+
const words = cleaned.split(",").map((s) => s.trim()).filter(Boolean);
|
|
5047
|
+
for (const w of words) {
|
|
5048
|
+
const parts = w.split(/\s+as\s+/);
|
|
5049
|
+
const name = parts[0].replace(/^type\s+/, "").trim();
|
|
5050
|
+
const alias = (parts[1] || name).trim();
|
|
5051
|
+
if (alias && alias !== "*") {
|
|
5052
|
+
importedNames.push(alias);
|
|
5053
|
+
}
|
|
5054
|
+
}
|
|
5055
|
+
}
|
|
4991
5056
|
statements.push({
|
|
4992
5057
|
specifier,
|
|
4993
5058
|
line: lineNum,
|
|
4994
|
-
rawCode
|
|
5059
|
+
rawCode,
|
|
5060
|
+
isTypeOnly,
|
|
5061
|
+
importedNames
|
|
4995
5062
|
});
|
|
4996
5063
|
}
|
|
4997
5064
|
return statements;
|
|
@@ -5090,6 +5157,15 @@ function discoverAllFeatures(projectRoot) {
|
|
|
5090
5157
|
dir: path2.join(featuresBase, e.name)
|
|
5091
5158
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
5092
5159
|
}
|
|
5160
|
+
function discoverAllShared(projectRoot) {
|
|
5161
|
+
const sharedBase = resolveSharedDir(projectRoot);
|
|
5162
|
+
if (!fs14.existsSync(sharedBase)) return [];
|
|
5163
|
+
const entries = fs14.readdirSync(sharedBase, { withFileTypes: true });
|
|
5164
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "__tests__" && e.name !== "tests").map((e) => ({
|
|
5165
|
+
name: e.name,
|
|
5166
|
+
dir: path2.join(sharedBase, e.name)
|
|
5167
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
5168
|
+
}
|
|
5093
5169
|
function extractClassMethods(content, className) {
|
|
5094
5170
|
let clean = "";
|
|
5095
5171
|
let inLineComment = false;
|
|
@@ -5438,17 +5514,56 @@ function lintCodebase(options = {}) {
|
|
|
5438
5514
|
const startTime = Date.now();
|
|
5439
5515
|
const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
|
|
5440
5516
|
const failFast = !options.complete;
|
|
5517
|
+
const lintConfig = loadLintConfig(projectRoot);
|
|
5518
|
+
const baseErrorRelative = lintConfig.baseErrorPath || "src/errors/AppError.ts";
|
|
5519
|
+
const baseErrorAbs = path2.resolve(projectRoot, baseErrorRelative);
|
|
5520
|
+
const featuresBase = resolveFeaturesDir(projectRoot);
|
|
5521
|
+
const sharedBase = resolveSharedDir(projectRoot);
|
|
5522
|
+
let errorsBase = path2.join(projectRoot, "src/errors");
|
|
5523
|
+
if (!fs14.existsSync(errorsBase) && fs14.existsSync(path2.join(projectRoot, "errors"))) {
|
|
5524
|
+
errorsBase = path2.join(projectRoot, "errors");
|
|
5525
|
+
}
|
|
5526
|
+
const missingFoundations = [];
|
|
5527
|
+
if (!fs14.existsSync(featuresBase)) missingFoundations.push("features/");
|
|
5528
|
+
if (!fs14.existsSync(sharedBase)) missingFoundations.push("shared/");
|
|
5529
|
+
if (!fs14.existsSync(errorsBase)) missingFoundations.push("errors/");
|
|
5530
|
+
if (missingFoundations.length > 0) {
|
|
5531
|
+
throw new DependencyRuleError(
|
|
5532
|
+
`Project Structure Violation: Missing required foundational directories: ${missingFoundations.join(", ")}. Every project must contain 'features/', 'shared/', and 'errors/'.`,
|
|
5533
|
+
[],
|
|
5534
|
+
true
|
|
5535
|
+
);
|
|
5536
|
+
}
|
|
5537
|
+
if (!fs14.existsSync(baseErrorAbs)) {
|
|
5538
|
+
throw new DependencyRuleError(
|
|
5539
|
+
`Project Structure Violation: Base error class not found at '${baseErrorRelative}'. Create this file or update 'baseErrorPath' in mason-lint.config.json.`,
|
|
5540
|
+
[],
|
|
5541
|
+
true
|
|
5542
|
+
);
|
|
5543
|
+
}
|
|
5544
|
+
const baseErrorContent = fs14.readFileSync(baseErrorAbs, "utf-8");
|
|
5545
|
+
const baseClassMatch = baseErrorContent.match(
|
|
5546
|
+
/(?:export\s+(?:default\s+)?)?(?:abstract\s+)?class\s+([A-Za-z0-9_]+)/
|
|
5547
|
+
);
|
|
5548
|
+
const baseErrorClassName = baseClassMatch ? baseClassMatch[1] : "AppError";
|
|
5441
5549
|
const features = discoverAllFeatures(projectRoot);
|
|
5550
|
+
const sharedModules = discoverAllShared(projectRoot);
|
|
5551
|
+
const errorModule = { name: "global-errors", dir: errorsBase, type: "error" };
|
|
5552
|
+
const allModules = [
|
|
5553
|
+
...features.map((f) => ({ ...f, type: "feature" })),
|
|
5554
|
+
...sharedModules.map((s) => ({ ...s, type: "shared" })),
|
|
5555
|
+
errorModule
|
|
5556
|
+
];
|
|
5442
5557
|
const graph = new FeatureDependencyGraph();
|
|
5443
5558
|
const allViolations = [];
|
|
5444
5559
|
const results = [];
|
|
5445
5560
|
const detectedCycles = [];
|
|
5446
5561
|
const addViolation = (v) => {
|
|
5447
5562
|
allViolations.push(v);
|
|
5448
|
-
const
|
|
5449
|
-
if (
|
|
5450
|
-
|
|
5451
|
-
|
|
5563
|
+
const moduleResult = results.find((r) => r.featureName === v.feature);
|
|
5564
|
+
if (moduleResult) {
|
|
5565
|
+
moduleResult.violations.push(v);
|
|
5566
|
+
moduleResult.passed = false;
|
|
5452
5567
|
}
|
|
5453
5568
|
if (failFast) {
|
|
5454
5569
|
const errorMsg = `Dependency rule violation detected in ${v.relativePath}:${v.line}:
|
|
@@ -5457,28 +5572,271 @@ Code: ${v.code}`;
|
|
|
5457
5572
|
throw new DependencyRuleError(errorMsg, [v], true);
|
|
5458
5573
|
}
|
|
5459
5574
|
};
|
|
5460
|
-
for (const
|
|
5575
|
+
for (const mod of allModules) {
|
|
5461
5576
|
results.push({
|
|
5462
|
-
|
|
5463
|
-
|
|
5577
|
+
moduleType: mod.type,
|
|
5578
|
+
moduleName: mod.name,
|
|
5579
|
+
featureName: mod.name,
|
|
5580
|
+
featureDir: mod.dir,
|
|
5581
|
+
moduleDir: mod.dir,
|
|
5464
5582
|
scannedFilesCount: 0,
|
|
5465
5583
|
passed: true,
|
|
5466
5584
|
violations: [],
|
|
5467
|
-
layersChecked: ["domain", "useCases", "services", "infrastructure", "presentation"]
|
|
5585
|
+
layersChecked: mod.type === "feature" ? ["domain", "useCases", "services", "infrastructure", "presentation"] : mod.type === "shared" ? ["domain", "ports", "infrastructure", "root"] : ["domain"]
|
|
5468
5586
|
});
|
|
5469
5587
|
}
|
|
5470
|
-
for (const
|
|
5471
|
-
const
|
|
5472
|
-
const allFiles = scanDirFiles(
|
|
5473
|
-
|
|
5588
|
+
for (const mod of allModules) {
|
|
5589
|
+
const modResult = results.find((r) => r.featureName === mod.name);
|
|
5590
|
+
const allFiles = scanDirFiles(mod.dir, projectRoot);
|
|
5591
|
+
modResult.scannedFilesCount = allFiles.length;
|
|
5474
5592
|
for (const file of allFiles) {
|
|
5475
5593
|
const relPath = path2.relative(projectRoot, file).replace(/\\/g, "/");
|
|
5476
|
-
const { layer } = classifyFile(file, projectRoot);
|
|
5477
|
-
if (!layer) continue;
|
|
5594
|
+
const { moduleType, moduleName, layer } = classifyFile(file, projectRoot);
|
|
5595
|
+
if (!layer || !moduleType || !moduleName) continue;
|
|
5478
5596
|
const content = fs14.readFileSync(file, "utf-8");
|
|
5479
5597
|
const statements = extractImportStatements(content);
|
|
5480
5598
|
const baseName = path2.basename(file);
|
|
5481
5599
|
const isBarrel = baseName === "index.ts" || baseName === "index.js";
|
|
5600
|
+
const throwErrorRegex = /\bthrow\s+(?:new\s+)?Error\s*\(/g;
|
|
5601
|
+
let throwMatch;
|
|
5602
|
+
while ((throwMatch = throwErrorRegex.exec(content)) !== null) {
|
|
5603
|
+
const charIndex = throwMatch.index;
|
|
5604
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
5605
|
+
const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || throwMatch[0];
|
|
5606
|
+
if (lineCode.startsWith("//")) continue;
|
|
5607
|
+
addViolation({
|
|
5608
|
+
feature: moduleName,
|
|
5609
|
+
layer,
|
|
5610
|
+
file,
|
|
5611
|
+
relativePath: relPath,
|
|
5612
|
+
line: lineNum,
|
|
5613
|
+
category: "Plain Error Throw Violation",
|
|
5614
|
+
code: lineCode,
|
|
5615
|
+
ruleId: "E3_PLAIN_ERROR_THROW",
|
|
5616
|
+
severity: "error",
|
|
5617
|
+
advice: `Throwing plain 'Error' is prohibited. You must throw '${baseErrorClassName}' or a domain-specific error class that inherits from it.`,
|
|
5618
|
+
fixExample: {
|
|
5619
|
+
bad: lineCode,
|
|
5620
|
+
good: `throw new ${baseErrorClassName}("Something went wrong");`
|
|
5621
|
+
}
|
|
5622
|
+
});
|
|
5623
|
+
}
|
|
5624
|
+
const isErrorFile = relPath.includes("/errors/") || relPath.startsWith("errors/") || relPath.startsWith("src/errors/") || moduleType === "error" || baseName.endsWith("Error.ts") || baseName.endsWith("Errors.ts") || baseName.endsWith("Error.js") || baseName.endsWith("Errors.js");
|
|
5625
|
+
const isBaseErrorFile = path2.resolve(file) === path2.resolve(baseErrorAbs);
|
|
5626
|
+
if (isErrorFile && !isBaseErrorFile) {
|
|
5627
|
+
const classRegex = /(?:export\s+(?:default\s+)?)?(?:abstract\s+)?class\s+([A-Za-z0-9_]+)(?:<[^>]+>)?(?:\s+extends\s+([A-Za-z0-9_]+))?/g;
|
|
5628
|
+
let classMatch;
|
|
5629
|
+
while ((classMatch = classRegex.exec(content)) !== null) {
|
|
5630
|
+
const className = classMatch[1];
|
|
5631
|
+
const extendsName = classMatch[2];
|
|
5632
|
+
const charIndex = classMatch.index;
|
|
5633
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
5634
|
+
const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || classMatch[0];
|
|
5635
|
+
if (lineCode.startsWith("//")) continue;
|
|
5636
|
+
if (!extendsName) {
|
|
5637
|
+
addViolation({
|
|
5638
|
+
feature: moduleName,
|
|
5639
|
+
layer,
|
|
5640
|
+
file,
|
|
5641
|
+
relativePath: relPath,
|
|
5642
|
+
line: lineNum,
|
|
5643
|
+
category: "Error Inheritance Violation",
|
|
5644
|
+
code: lineCode,
|
|
5645
|
+
ruleId: "E4_MISSING_INHERITANCE",
|
|
5646
|
+
severity: "error",
|
|
5647
|
+
advice: `Error class '${className}' must extend '${baseErrorClassName}' (or another class that derives from it).`,
|
|
5648
|
+
fixExample: {
|
|
5649
|
+
bad: lineCode,
|
|
5650
|
+
good: `export class ${className} extends ${baseErrorClassName} { ... }`
|
|
5651
|
+
}
|
|
5652
|
+
});
|
|
5653
|
+
} else if (extendsName === "Error") {
|
|
5654
|
+
addViolation({
|
|
5655
|
+
feature: moduleName,
|
|
5656
|
+
layer,
|
|
5657
|
+
file,
|
|
5658
|
+
relativePath: relPath,
|
|
5659
|
+
line: lineNum,
|
|
5660
|
+
category: "Error Inheritance Violation",
|
|
5661
|
+
code: lineCode,
|
|
5662
|
+
ruleId: "E5_NATIVE_ERROR_INHERITANCE",
|
|
5663
|
+
severity: "error",
|
|
5664
|
+
advice: `Error class '${className}' extends the native 'Error'. It must extend your base error class ('${baseErrorClassName}').`,
|
|
5665
|
+
fixExample: {
|
|
5666
|
+
bad: lineCode,
|
|
5667
|
+
good: `export class ${className} extends ${baseErrorClassName} { ... }`
|
|
5668
|
+
}
|
|
5669
|
+
});
|
|
5670
|
+
}
|
|
5671
|
+
}
|
|
5672
|
+
}
|
|
5673
|
+
if (moduleType === "error") {
|
|
5674
|
+
for (const stmt of statements) {
|
|
5675
|
+
const target = classifyTarget(stmt.specifier, file, projectRoot);
|
|
5676
|
+
if (target.targetFeature) {
|
|
5677
|
+
addViolation({
|
|
5678
|
+
feature: "global-errors",
|
|
5679
|
+
layer: "domain",
|
|
5680
|
+
file,
|
|
5681
|
+
relativePath: relPath,
|
|
5682
|
+
line: stmt.line,
|
|
5683
|
+
category: "Global Error Purity Violation",
|
|
5684
|
+
code: stmt.rawCode,
|
|
5685
|
+
ruleId: "E1_FEATURE_DEPENDENCY",
|
|
5686
|
+
severity: "error",
|
|
5687
|
+
advice: `Global errors must not depend on features ('${target.targetFeature}'). Global errors represent pure domain or system states.`,
|
|
5688
|
+
fixExample: {
|
|
5689
|
+
bad: stmt.rawCode,
|
|
5690
|
+
good: "// Global errors must not reference specific features. Keep error types pure."
|
|
5691
|
+
}
|
|
5692
|
+
});
|
|
5693
|
+
}
|
|
5694
|
+
if (target.targetSharedModule && !target.isSharedDomain) {
|
|
5695
|
+
addViolation({
|
|
5696
|
+
feature: "global-errors",
|
|
5697
|
+
layer: "domain",
|
|
5698
|
+
file,
|
|
5699
|
+
relativePath: relPath,
|
|
5700
|
+
line: stmt.line,
|
|
5701
|
+
category: "Global Error Purity Violation",
|
|
5702
|
+
code: stmt.rawCode,
|
|
5703
|
+
ruleId: "E2_SHARED_NON_DOMAIN",
|
|
5704
|
+
severity: "error",
|
|
5705
|
+
advice: `Global errors can only depend on shared domain elements (e.g., value objects). They cannot depend on shared ports or infrastructure.`,
|
|
5706
|
+
fixExample: {
|
|
5707
|
+
bad: stmt.rawCode,
|
|
5708
|
+
good: "// Depend only on pure domain types from shared modules."
|
|
5709
|
+
}
|
|
5710
|
+
});
|
|
5711
|
+
}
|
|
5712
|
+
}
|
|
5713
|
+
continue;
|
|
5714
|
+
}
|
|
5715
|
+
if (moduleType === "shared") {
|
|
5716
|
+
if (layer === "domain") {
|
|
5717
|
+
const lines = content.split(/\r?\n/);
|
|
5718
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5719
|
+
const lineText = lines[i].trim();
|
|
5720
|
+
if (lineText.includes("@MakeInjectable") || lineText.includes("public static deps =")) {
|
|
5721
|
+
addViolation({
|
|
5722
|
+
feature: moduleName,
|
|
5723
|
+
layer,
|
|
5724
|
+
file,
|
|
5725
|
+
relativePath: relPath,
|
|
5726
|
+
line: i + 1,
|
|
5727
|
+
category: "Domain Dependence Violation",
|
|
5728
|
+
code: lineText,
|
|
5729
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
5730
|
+
severity: "error",
|
|
5731
|
+
advice: "Domain layer must have zero DI dependence. Remove @MakeInjectable decorators and static deps declarations.",
|
|
5732
|
+
fixExample: {
|
|
5733
|
+
bad: lineText,
|
|
5734
|
+
good: "// Domain interfaces and value objects must remain plain TypeScript classes/interfaces."
|
|
5735
|
+
}
|
|
5736
|
+
});
|
|
5737
|
+
}
|
|
5738
|
+
}
|
|
5739
|
+
}
|
|
5740
|
+
for (const stmt of statements) {
|
|
5741
|
+
const target = classifyTarget(stmt.specifier, file, projectRoot);
|
|
5742
|
+
if (target.targetFeature) {
|
|
5743
|
+
addViolation({
|
|
5744
|
+
feature: moduleName,
|
|
5745
|
+
layer,
|
|
5746
|
+
file,
|
|
5747
|
+
relativePath: relPath,
|
|
5748
|
+
line: stmt.line,
|
|
5749
|
+
category: "Shared Architecture Violation",
|
|
5750
|
+
code: stmt.rawCode,
|
|
5751
|
+
ruleId: "SF1_FEATURE_DEPENDENCY",
|
|
5752
|
+
severity: "error",
|
|
5753
|
+
advice: `Shared modules ('${moduleName}') are foundational and must NEVER import from feature modules ('${target.targetFeature}').`,
|
|
5754
|
+
fixExample: {
|
|
5755
|
+
bad: stmt.rawCode,
|
|
5756
|
+
good: "// Move shared logic/types to shared/ or communicate via domain events/callbacks."
|
|
5757
|
+
}
|
|
5758
|
+
});
|
|
5759
|
+
}
|
|
5760
|
+
if (target.targetSharedModule && target.targetSharedModule !== moduleName) {
|
|
5761
|
+
if (target.isSharedInfra || target.isSharedPorts) {
|
|
5762
|
+
addViolation({
|
|
5763
|
+
feature: moduleName,
|
|
5764
|
+
layer,
|
|
5765
|
+
file,
|
|
5766
|
+
relativePath: relPath,
|
|
5767
|
+
line: stmt.line,
|
|
5768
|
+
category: "Shared Architecture Violation",
|
|
5769
|
+
code: stmt.rawCode,
|
|
5770
|
+
ruleId: "SS1_SHARED_ENCAPSULATION",
|
|
5771
|
+
severity: "error",
|
|
5772
|
+
advice: `Shared module '${moduleName}' cannot import internal details (ports/infra) of shared module '${target.targetSharedModule}'. Depend only on its root classes or domain types.`,
|
|
5773
|
+
fixExample: {
|
|
5774
|
+
bad: stmt.rawCode,
|
|
5775
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
5776
|
+
}
|
|
5777
|
+
});
|
|
5778
|
+
}
|
|
5779
|
+
}
|
|
5780
|
+
if (layer === "domain") {
|
|
5781
|
+
if (target.targetSharedModule === moduleName && (target.isSharedInfra || target.isSharedPorts || target.isSharedRoot)) {
|
|
5782
|
+
addViolation({
|
|
5783
|
+
feature: moduleName,
|
|
5784
|
+
layer,
|
|
5785
|
+
file,
|
|
5786
|
+
relativePath: relPath,
|
|
5787
|
+
line: stmt.line,
|
|
5788
|
+
category: "Domain Purity Violation",
|
|
5789
|
+
code: stmt.rawCode,
|
|
5790
|
+
ruleId: "SD1_OUTER_IMPORT",
|
|
5791
|
+
severity: "error",
|
|
5792
|
+
advice: `Shared domain layer must not import from its own ports, infrastructure, or root classes.`,
|
|
5793
|
+
fixExample: {
|
|
5794
|
+
bad: stmt.rawCode,
|
|
5795
|
+
good: "// Model pure business types and value objects directly within domain/."
|
|
5796
|
+
}
|
|
5797
|
+
});
|
|
5798
|
+
}
|
|
5799
|
+
if (target.isDiFramework) {
|
|
5800
|
+
addViolation({
|
|
5801
|
+
feature: moduleName,
|
|
5802
|
+
layer,
|
|
5803
|
+
file,
|
|
5804
|
+
relativePath: relPath,
|
|
5805
|
+
line: stmt.line,
|
|
5806
|
+
category: "Domain Dependence Violation",
|
|
5807
|
+
code: stmt.rawCode,
|
|
5808
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
5809
|
+
severity: "error",
|
|
5810
|
+
advice: "Domain layer must have zero DI dependence. Domain interfaces and value objects are plain classes/types.",
|
|
5811
|
+
fixExample: {
|
|
5812
|
+
bad: stmt.rawCode,
|
|
5813
|
+
good: "// Remove @solid-stack/di import. Domain entities are pure TypeScript."
|
|
5814
|
+
}
|
|
5815
|
+
});
|
|
5816
|
+
}
|
|
5817
|
+
}
|
|
5818
|
+
if (layer === "ports" && target.targetSharedModule === moduleName && target.isSharedInfra) {
|
|
5819
|
+
addViolation({
|
|
5820
|
+
feature: moduleName,
|
|
5821
|
+
layer,
|
|
5822
|
+
file,
|
|
5823
|
+
relativePath: relPath,
|
|
5824
|
+
line: stmt.line,
|
|
5825
|
+
category: "Shared Architecture Violation",
|
|
5826
|
+
code: stmt.rawCode,
|
|
5827
|
+
ruleId: "SP1_INFRA_IN_PORT",
|
|
5828
|
+
severity: "error",
|
|
5829
|
+
advice: `Shared ports cannot depend on infrastructure. Infrastructure implements ports, not the other way around.`,
|
|
5830
|
+
fixExample: {
|
|
5831
|
+
bad: stmt.rawCode,
|
|
5832
|
+
good: "// Ports define abstract interfaces without concrete infrastructure dependencies."
|
|
5833
|
+
}
|
|
5834
|
+
});
|
|
5835
|
+
}
|
|
5836
|
+
}
|
|
5837
|
+
continue;
|
|
5838
|
+
}
|
|
5839
|
+
const feat = mod;
|
|
5482
5840
|
if (layer === "useCases" && !isBarrel) {
|
|
5483
5841
|
checkUseCaseShape(feat.name, file, relPath, content, addViolation);
|
|
5484
5842
|
}
|
|
@@ -5547,23 +5905,33 @@ Code: ${v.code}`;
|
|
|
5547
5905
|
}
|
|
5548
5906
|
});
|
|
5549
5907
|
}
|
|
5550
|
-
if (target.targetLayer === "domain" && !target.isTargetPort) {
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5908
|
+
if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetError && !stmt.isTypeOnly) {
|
|
5909
|
+
for (const name of stmt.importedNames) {
|
|
5910
|
+
const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
|
|
5911
|
+
const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
|
|
5912
|
+
const execMatch = createRegex.exec(content) || newRegex.exec(content);
|
|
5913
|
+
if (execMatch) {
|
|
5914
|
+
const charIndex = execMatch.index;
|
|
5915
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
5916
|
+
const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
|
|
5917
|
+
addViolation({
|
|
5918
|
+
feature: feat.name,
|
|
5919
|
+
layer,
|
|
5920
|
+
file,
|
|
5921
|
+
relativePath: relPath,
|
|
5922
|
+
line: lineNum,
|
|
5923
|
+
category: "Cross-Feature Domain Violation",
|
|
5924
|
+
code: lineCode,
|
|
5925
|
+
ruleId: "F1_CROSS_FEATURE_DOMAIN",
|
|
5926
|
+
severity: "error",
|
|
5927
|
+
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.`,
|
|
5928
|
+
fixExample: {
|
|
5929
|
+
bad: lineCode,
|
|
5930
|
+
good: `// Define a Port in ${feat.name}/domain/ and implement the cross-feature communication in ${feat.name}/infrastructure/.`
|
|
5931
|
+
}
|
|
5932
|
+
});
|
|
5565
5933
|
}
|
|
5566
|
-
}
|
|
5934
|
+
}
|
|
5567
5935
|
}
|
|
5568
5936
|
}
|
|
5569
5937
|
if (target.isGlobalConfig) {
|
|
@@ -5951,23 +6319,33 @@ Code: ${v.code}`;
|
|
|
5951
6319
|
}
|
|
5952
6320
|
});
|
|
5953
6321
|
}
|
|
5954
|
-
if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo) {
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
6322
|
+
if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo && !target.isTargetError && !stmt.isTypeOnly) {
|
|
6323
|
+
for (const name of stmt.importedNames) {
|
|
6324
|
+
const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
|
|
6325
|
+
const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
|
|
6326
|
+
const execMatch = createRegex.exec(content) || newRegex.exec(content);
|
|
6327
|
+
if (execMatch) {
|
|
6328
|
+
const charIndex = execMatch.index;
|
|
6329
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
6330
|
+
const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
|
|
6331
|
+
addViolation({
|
|
6332
|
+
feature: feat.name,
|
|
6333
|
+
layer,
|
|
6334
|
+
file,
|
|
6335
|
+
relativePath: relPath,
|
|
6336
|
+
line: lineNum,
|
|
6337
|
+
category: "Presentation Layer Violation",
|
|
6338
|
+
code: lineCode,
|
|
6339
|
+
ruleId: "P5_DIRECT_DOMAIN_ENTITY",
|
|
6340
|
+
severity: "error",
|
|
6341
|
+
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.`,
|
|
6342
|
+
fixExample: {
|
|
6343
|
+
bad: lineCode,
|
|
6344
|
+
good: "// Construct a plain DTO object and pass it into the injected Use Case's execute() method."
|
|
6345
|
+
}
|
|
6346
|
+
});
|
|
5969
6347
|
}
|
|
5970
|
-
}
|
|
6348
|
+
}
|
|
5971
6349
|
}
|
|
5972
6350
|
}
|
|
5973
6351
|
}
|