UNPKG

archunit

Version:

ArchUnit TypeScript is an architecture testing library, to specify and assert architecture rules in your TypeScript app

396 lines 18.7 kB
"use strict"; 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