archunit
Version:
ArchUnit TypeScript is an architecture testing library, to specify and assert architecture rules in your TypeScript app
396 lines • 18.7 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.extractEnhancedClassInfo = exports.extractClassInfo = void 0;
const ts = __importStar(require("typescript"));
const path = __importStar(require("path"));
const fs_1 = __importDefault(require("fs"));
const extraction_1 = require("../../common/extraction");
const common_1 = require("../../common");
/**
* Extracts class information from TypeScript source files for metrics calculation
*/
function extractClassInfo(configFileName, projectPath = process.cwd(), options) {
const logger = common_1.sharedLogger;
logger?.debug(options?.logging, `Starting class extraction with config: ${configFileName || 'auto-detected'}`);
logger?.info(options?.logging, `Project path: ${projectPath}`);
// // Get program from tsconfig or create a default one
// const configPath = tsConfigFilePath
// ? path.resolve(projectPath, tsConfigFilePath)
// : path.resolve(projectPath, 'tsconfig.json');
// logger?.info(options?.logging, `Using TypeScript config file: ${configPath}`);
// logger?.debug(options?.logging, `Reading config file: ${configPath}`);
// const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
// if (configFile.error) {
// const error = `Error reading tsconfig file: ${configFile.error.messageText}`;
// logger?.error(options?.logging, error);
// throw new Error(error);
// }
// logger?.debug(options?.logging, 'Successfully read TypeScript configuration file');
// const parsedConfig = ts.parseJsonConfigFileContent(
// configFile.config,
// ts.sys,
// path.dirname(configPath),
// {},
// configPath
// );
// if (parsedConfig.errors.length > 0) {
// const error = `Error parsing tsconfig file: ${parsedConfig.errors[0].messageText}`;
// logger?.error(options?.logging, error);
// throw new Error(error);
// }
// logger?.debug(options?.logging, 'Successfully parsed TypeScript configuration');
// logger?.debug(
// options?.logging,
// `Compiler options: ${JSON.stringify(parsedConfig.options, null, 2).slice(0, 500)}...`
// );
// logger?.info(
// options?.logging,
// `Found ${parsedConfig.fileNames.length} files in project configuration`
// );
// logger?.debug(
// options?.logging,
// `Root files: ${parsedConfig.fileNames.slice(0, 10).join(', ')}${parsedConfig.fileNames.length > 10 ? `... and ${parsedConfig.fileNames.length - 10} more` : ''}`
// );
// logger?.debug(options?.logging, 'Creating TypeScript program');
// const program = ts.createProgram({
// rootNames: parsedConfig.fileNames,
// options: parsedConfig.options,
// });
// const sourceFiles = program.getSourceFiles();
const configFile = configFileName ?? (0, extraction_1.guessLocationOfTsconfig)(options);
if (!configFile) {
const error = 'Could not find configuration path';
logger?.error(options?.logging, error);
throw new common_1.TechnicalError(error);
}
logger?.info(options?.logging, `Using TypeScript config file: ${configFile}`);
const config = ts.readConfigFile(configFile, (path) => {
logger?.debug(options?.logging, `Reading config file: ${path}`);
return fs_1.default.readFileSync(path).toString();
});
if (config.error) {
logger?.error(options?.logging, `Invalid config file: ${config.error.messageText}`);
throw new common_1.TechnicalError('invalid config path');
}
logger?.debug(options?.logging, 'Successfully parsed TypeScript configuration');
const parsedConfig = config.config;
logger?.debug(options?.logging, `Compiler options: ${JSON.stringify(parsedConfig, null, 2).slice(0, 500)}...`);
const rootDir = path.dirname(path.resolve(configFile));
logger?.info(options?.logging, `Project root directory: ${rootDir}`);
const compilerHost = ts.createCompilerHost(parsedConfig);
logger?.debug(options?.logging, 'Created TypeScript compiler host');
const files = (0, extraction_1.getProjectFiles)(rootDir, compilerHost, config?.config);
// --
logger?.debug(options?.logging, 'Creating TypeScript program');
const program = ts.createProgram({
rootNames: files ?? [],
options: parsedConfig,
host: compilerHost,
});
const sourceFiles = program.getSourceFiles();
logger?.info(options?.logging, `TypeScript program created with ${sourceFiles.length} source files`);
// Filter out files from node_modules for logging purposes
const projectFiles = sourceFiles.filter((sf) => !sf.isDeclarationFile && !sf.fileName.includes('node_modules'));
const declarationFiles = sourceFiles.filter((sf) => sf.isDeclarationFile);
const nodeModulesFiles = sourceFiles.filter((sf) => sf.fileName.includes('node_modules'));
logger?.debug(options?.logging, `Project source files: ${projectFiles.length}`);
logger?.debug(options?.logging, `Declaration files: ${declarationFiles.length}`);
logger?.debug(options?.logging, `Node modules files: ${nodeModulesFiles.length}`);
if (projectFiles.length === 0) {
logger?.warn(options?.logging, 'No project source files found - this might indicate a configuration issue');
}
const classes = [];
let processedFiles = 0;
logger?.debug(options?.logging, 'Starting class extraction from source files');
// Process each source file
for (const sourceFile of program.getSourceFiles()) {
if (!sourceFile.isDeclarationFile &&
!sourceFile.fileName.includes('node_modules')) {
processedFiles++;
logger?.debug(options?.logging, `Processing file ${processedFiles}/${projectFiles.length}: ${path.relative(projectPath, sourceFile.fileName)}`);
const classesBeforeFile = classes.length;
processSourceFile(sourceFile, program, classes);
const classesAfterFile = classes.length;
const classesFoundInFile = classesAfterFile - classesBeforeFile;
if (classesFoundInFile > 0) {
logger?.debug(options?.logging, `Found ${classesFoundInFile} class(es) in ${path.relative(projectPath, sourceFile.fileName)}`);
}
}
}
logger?.info(options?.logging, `Class extraction completed:`);
logger?.info(options?.logging, ` - Total classes found: ${classes.length}`);
logger?.info(options?.logging, ` - Files processed: ${processedFiles}`);
if (classes.length === 0) {
logger?.warn(options?.logging, 'No classes found - this might indicate a pattern matching or file discovery issue');
}
return classes;
}
exports.extractClassInfo = extractClassInfo;
function processSourceFile(sourceFile, program, classes, options) {
const logger = common_1.sharedLogger;
const relativeFileName = path.relative(process.cwd(), sourceFile.fileName);
logger?.debug(options?.logging, `Analyzing source file: ${relativeFileName}`);
let classesFoundInFile = 0;
function visit(node) {
// Find class declarations
if (ts.isClassDeclaration(node) && node.name) {
classesFoundInFile++;
const className = node.name.text;
logger?.debug(options?.logging, ` Found class: ${className}`);
const classInfo = {
name: className,
filePath: relativeFileName,
methods: [],
fields: [],
sourceFile: sourceFile,
};
let methodCount = 0;
let fieldCount = 0;
// Process class members
node.members.forEach((member) => {
// Find class properties/fields
if (ts.isPropertyDeclaration(member) && member.name) {
fieldCount++;
const fieldName = member.name.getText(sourceFile);
classInfo.fields.push({
name: fieldName,
accessedBy: [],
});
}
// Find class methods
if (ts.isMethodDeclaration(member) && member.name) {
methodCount++;
const methodName = member.name.getText(sourceFile);
const accessedFields = [];
// Analyze method body to find field accesses
if (member.body) {
logger?.debug(options?.logging, ` Analyzing method: ${methodName}`);
findFieldAccesses(member.body, classInfo.fields.map((f) => f.name), accessedFields);
if (accessedFields.length > 0) {
logger?.debug(options?.logging, ` Method ${methodName} accesses fields: ${accessedFields.join(', ')}`);
}
}
classInfo.methods.push({
name: methodName,
accessedFields: accessedFields,
});
// Update fields with methods that access them
accessedFields.forEach((field) => {
const fieldInfo = classInfo.fields.find((f) => f.name === field);
if (fieldInfo && !fieldInfo.accessedBy.includes(methodName)) {
fieldInfo.accessedBy.push(methodName);
}
});
}
});
logger?.debug(options?.logging, ` Class ${className} has ${methodCount} methods and ${fieldCount} fields`);
classes.push(classInfo);
}
ts.forEachChild(node, visit);
}
function findFieldAccesses(node, fields, accessedFields) {
if (ts.isPropertyAccessExpression(node)) {
const propertyName = node.name.text;
if (fields.includes(propertyName)) {
if (!accessedFields.includes(propertyName)) {
accessedFields.push(propertyName);
}
}
}
ts.isInterfaceDeclaration(node);
ts.forEachChild(node, (child) => findFieldAccesses(child, fields, accessedFields));
}
visit(sourceFile);
if (classesFoundInFile > 0) {
logger?.debug(options?.logging, ` Total classes found in ${relativeFileName}: ${classesFoundInFile}`);
}
else {
logger?.debug(options?.logging, ` No classes found in ${relativeFileName}`);
}
}
/**
* Enhanced class information extraction with abstractness and dependency analysis
*/
async function extractEnhancedClassInfo(tsConfigFilePath, projectPath = process.cwd(), options) {
// Get program from tsconfig or create a default one
const configPath = tsConfigFilePath
? path.resolve(projectPath, tsConfigFilePath)
: path.resolve(projectPath, 'tsconfig.json');
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
if (configFile.error) {
throw new Error(`Error reading tsconfig file: ${configFile.error.messageText}`);
}
const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(configPath), {}, configPath);
if (parsedConfig.errors.length > 0) {
throw new Error(`Error parsing tsconfig file: ${parsedConfig.errors[0].messageText}`);
}
const program = ts.createProgram({
rootNames: parsedConfig.fileNames,
options: parsedConfig.options,
});
// Extract dependency graph
const dependencyGraph = await (0, extraction_1.extractGraph)(tsConfigFilePath || configPath, options);
const fileResults = [];
// Process each source file
for (const sourceFile of program.getSourceFiles()) {
if (!sourceFile.isDeclarationFile &&
!sourceFile.fileName.includes('node_modules')) {
const result = processSourceFileEnhanced(sourceFile, program, dependencyGraph);
if (result.totalTypes > 0) {
// Only include files with classes/interfaces
fileResults.push(result);
}
}
}
return fileResults;
}
exports.extractEnhancedClassInfo = extractEnhancedClassInfo;
function processSourceFileEnhanced(sourceFile, program, dependencyGraph) {
const classes = [];
let interfaces = 0;
let abstractClasses = 0;
let concreteClasses = 0;
function visit(node) {
// Find interface declarations
if (ts.isInterfaceDeclaration(node) && node.name) {
interfaces++;
}
// Find class declarations
if (ts.isClassDeclaration(node) && node.name) {
const className = node.name.text;
const isAbstract = node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AbstractKeyword) || false;
if (isAbstract) {
abstractClasses++;
}
else {
concreteClasses++;
}
const classInfo = {
name: className,
filePath: sourceFile.fileName,
methods: [],
fields: [],
isAbstract: isAbstract,
isInterface: false,
abstractMethods: [],
dependencies: calculateClassDependencies(sourceFile.fileName, dependencyGraph),
sourceFile: sourceFile,
};
// Process class members
node.members.forEach((member) => {
// Find class properties/fields
if (ts.isPropertyDeclaration(member) && member.name) {
const fieldName = member.name.getText(sourceFile);
classInfo.fields.push({
name: fieldName,
accessedBy: [],
});
}
// Find class methods
if (ts.isMethodDeclaration(member) && member.name) {
const methodName = member.name.getText(sourceFile);
const isAbstractMethod = member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AbstractKeyword) || false;
if (isAbstractMethod) {
classInfo.abstractMethods.push(methodName);
}
const accessedFields = [];
// Analyze method body to find field accesses
if (member.body) {
findFieldAccessesInMethod(member.body, classInfo.fields.map((f) => f.name), accessedFields);
}
classInfo.methods.push({
name: methodName,
accessedFields: accessedFields,
});
// Update fields with methods that access them
accessedFields.forEach((field) => {
const fieldInfo = classInfo.fields.find((f) => f.name === field);
if (fieldInfo && !fieldInfo.accessedBy.includes(methodName)) {
fieldInfo.accessedBy.push(methodName);
}
});
}
});
classes.push(classInfo);
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
const totalTypes = interfaces + abstractClasses + concreteClasses;
const fileDependencies = calculateClassDependencies(sourceFile.fileName, dependencyGraph);
return {
filePath: sourceFile.fileName,
classes,
interfaces,
abstractClasses,
concreteClasses,
totalTypes,
dependencies: fileDependencies,
sourceFile, // Include the sourceFile for file-wise analysis
};
}
function calculateClassDependencies(filePath, dependencyGraph) {
const normalizedPath = path.normalize(filePath);
// Find outgoing dependencies (efferent coupling)
const outgoingDependencies = dependencyGraph
.filter((edge) => {
const normalizedSource = path.normalize(edge.source);
return (normalizedSource === normalizedPath ||
normalizedSource.endsWith(normalizedPath));
})
.filter((edge) => !edge.external) // Only internal dependencies
.map((edge) => edge.target);
// Find incoming dependencies (afferent coupling)
const incomingDependencies = dependencyGraph
.filter((edge) => {
const normalizedTarget = path.normalize(edge.target);
return (normalizedTarget === normalizedPath ||
normalizedTarget.endsWith(normalizedPath));
})
.filter((edge) => !edge.external) // Only internal dependencies
.map((edge) => edge.source);
return {
efferentCoupling: outgoingDependencies.length,
afferentCoupling: incomingDependencies.length,
outgoingDependencies: [...new Set(outgoingDependencies)], // Remove duplicates
incomingDependencies: [...new Set(incomingDependencies)], // Remove duplicates
};
}
function findFieldAccessesInMethod(node, fields, accessedFields) {
if (ts.isPropertyAccessExpression(node)) {
const propertyName = node.name.text;
if (fields.includes(propertyName)) {
if (!accessedFields.includes(propertyName)) {
accessedFields.push(propertyName);
}
}
}
ts.forEachChild(node, (child) => findFieldAccessesInMethod(child, fields, accessedFields));
}
//# sourceMappingURL=extract-class-info.js.map
;