Last active
          August 28, 2023 16:47 
        
      - 
      
- 
        Save mikaelvesavuori/5c6dab7afd951222b9dbebac1e609c77 to your computer and use it in GitHub Desktop. 
    Calculate coupling metrics and aggregated metrics from TS files.
  
        
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | import { readFileSync, readdirSync, statSync, writeFileSync } from 'fs'; | |
| import path from 'path'; | |
| // Use this before running folder metrics | |
| interface CouplingResult { | |
| abstractions: number; | |
| abstractness: number; | |
| afferent: number; | |
| concretions: number; | |
| distance: number; | |
| efferent: number; | |
| imports: string[]; | |
| instability: number; | |
| } | |
| const IMPORT_REGEX = /^\s*import\s+.*from\s+['"]([^'"]+)['"]/; | |
| const ABSTRACTION_REGEX = /(abstract class \w* {|type \w* = {|interface \w* {)/; | |
| const CONCRETION_REGEX = /const \w* = \(.*|function \w*\(.*\).* {|(?<!abstract\s)class \w* {/; | |
| function getFiles(folderPath: string): string[] { | |
| const tsFiles: string[] = []; | |
| function traverseDirectory(currentPath: string): void { | |
| const files = readdirSync(currentPath); | |
| files.forEach((file: any) => { | |
| const filePath = path.join(currentPath, file); | |
| const stats = statSync(filePath); | |
| if (stats.isFile() && path.extname(file) === '.ts') tsFiles.push(filePath); | |
| else if (stats.isDirectory()) traverseDirectory(filePath); | |
| }); | |
| } | |
| traverseDirectory(folderPath); | |
| return tsFiles; | |
| } | |
| function resolveImportPath(currentModulePath: string, importPath: string): string { | |
| if (importPath.startsWith('.') || importPath.startsWith('..')) | |
| return path.resolve(path.dirname(currentModulePath), importPath); | |
| return importPath; | |
| } | |
| function calculateCoupling(files: string[]): Record<string, CouplingResult> { | |
| const root = path.basename(path.resolve()); | |
| const results: Record<string, CouplingResult> = {}; | |
| // All files need a base object before the next step | |
| files.forEach((filePath) => { | |
| const fixedFileName = getCleanedDirectory(filePath, root); | |
| results[fixedFileName] = createBaseResults(filePath); | |
| }); | |
| // Go through all imports, if any, and calculate values | |
| files.forEach((filePath) => { | |
| const fixedFileName = getCleanedDirectory(filePath, root); | |
| const imports = results[fixedFileName].imports; | |
| if (imports.length > 0) { | |
| imports.forEach((importPath) => { | |
| const fixedImportName = getCleanedDirectory(flattenImport(importPath), root); | |
| const importReference = results[fixedImportName]; | |
| if (importReference) { | |
| importReference.afferent++; | |
| importReference.instability = calculateInstability(importReference); | |
| importReference.abstractness = calculateAbstractness(importReference); | |
| importReference.distance = calculateDistanceFromMainSequence(importReference); | |
| } | |
| }); | |
| } | |
| }); | |
| // Clean up import paths before presenting | |
| files.forEach((filePath) => { | |
| const fixedFileName = getCleanedDirectory(filePath, root); | |
| const imports = results[fixedFileName].imports; | |
| if (imports.length > 0) { | |
| const fixedImports = imports | |
| .map((importPath) => getCleanedDirectory(importPath, root)) | |
| .filter((importPath) => importPath); | |
| results[fixedFileName].imports = fixedImports; | |
| } | |
| }); | |
| return results; | |
| } | |
| function flattenImport(importPath: string) { | |
| return importPath.replace('~src', `${process.cwd()}/src`); | |
| } | |
| function getCleanedDirectory(filePath: string, root: string): string { | |
| return filePath.split(root)[1]; | |
| } | |
| function createBaseResults(filePath: string): CouplingResult { | |
| const { imports, concretions, abstractions } = extractData(filePath); | |
| return { | |
| afferent: 0, | |
| efferent: imports.length, | |
| abstractness: 0, | |
| instability: 0, | |
| distance: 0, | |
| imports, | |
| concretions, | |
| abstractions | |
| }; | |
| } | |
| function calculateInstability(item: CouplingResult): number { | |
| return parseFloat((item.efferent / (item.efferent + item.afferent)).toFixed(2)); | |
| } | |
| function calculateAbstractness(item: CouplingResult): number { | |
| if (item.abstractions && item.concretions === 0) return item.abstractions; | |
| if (!item.abstractions && item.concretions === 0) return 0; | |
| return parseFloat((item.abstractions / item.concretions).toFixed(2)); | |
| } | |
| function calculateDistanceFromMainSequence(item: CouplingResult): number { | |
| return parseFloat(Math.abs(item.abstractness + item.instability - 1).toFixed(2)); | |
| } | |
| function extractData(filePath: string): Record<string, any> { | |
| const lines = readFileSync(filePath, 'utf-8').split('\n'); | |
| const imports: string[] = []; | |
| let abstractions = 0; | |
| let concretions = 0; | |
| lines.forEach((line: any) => { | |
| const importMatch = line.match(IMPORT_REGEX); | |
| if (importMatch) imports.push(resolveImportPath(filePath, importMatch[1] + '.ts')); | |
| const abstractionMatch = line.match(ABSTRACTION_REGEX); | |
| if (abstractionMatch && !startsWithSpace(abstractionMatch.input || '')) abstractions++; | |
| const concretionMatch = line.match(CONCRETION_REGEX); | |
| if (concretionMatch && !startsWithSpace(concretionMatch.input || '')) concretions++; | |
| }); | |
| return { imports, concretions, abstractions }; | |
| } | |
| function startsWithSpace(str: string): boolean { | |
| return str.startsWith(' '); | |
| } | |
| function main() { | |
| const defaultPath = path.resolve('src'); | |
| const tsFiles = getFiles(defaultPath); | |
| const coupling = calculateCoupling(tsFiles); | |
| /* Example: | |
| const coupling = { | |
| '/bin/contracts/ApiResponse.ts': { | |
| afferent: 3, | |
| efferent: 0, | |
| abstractness: 2, | |
| instability: 0, | |
| distance: 1, | |
| imports: [], | |
| concretions: 0, | |
| abstractions: 2 | |
| } | |
| }; | |
| */ | |
| writeFileSync('coupling.json', JSON.stringify(coupling, null, 2)); | |
| for (const module in coupling) { | |
| console.table([ | |
| { | |
| Module: module, | |
| Afferent: coupling[module].afferent, | |
| Efferent: coupling[module].efferent, | |
| Instability: coupling[module].instability, | |
| Abstractness: coupling[module].abstractness, | |
| Distance: coupling[module].distance | |
| } | |
| ]); | |
| } | |
| } | |
| main(); | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | import { readFileSync, writeFileSync } from 'fs'; | |
| // Use the output from calculate-coupling.ts here (coupling.json) | |
| const data = readFileSync('coupling.json', 'utf-8'); | |
| const inputData = JSON.parse(data); | |
| interface FileMetrics { | |
| abstractions: number; | |
| abstractness: number; | |
| afferent: number; | |
| concretions: number; | |
| distance: number; | |
| efferent: number; | |
| imports: string[]; | |
| instability: number; | |
| } | |
| interface FolderMetrics { | |
| abstractions: number; | |
| afferent: number; | |
| concretions: number; | |
| efferent: number; | |
| loc: number; | |
| } | |
| interface AggregatedFolderMetrics { | |
| [directoryPath: string]: FolderMetrics; | |
| } | |
| interface InputData { | |
| [filePath: string]: FileMetrics; | |
| } | |
| interface ResultEntry { | |
| abstractions: number; | |
| afferent: number; | |
| concretions: number; | |
| directoryPath: string; | |
| efferent: number; | |
| loc: number; | |
| percent: number; | |
| } | |
| interface Results { | |
| abstractions: string; | |
| afferent: string; | |
| concretions: string; | |
| efferent: string; | |
| loc: string; | |
| results: ResultEntry[]; | |
| totalLinesOfCode: number; | |
| } | |
| /** | |
| * | |
| */ | |
| function countLinesOfCode(filePath: string): number { | |
| const fullPath = `${process.cwd()}${filePath}`; | |
| const content = readFileSync(fullPath, 'utf-8'); | |
| const lines = content.split('\n'); | |
| return lines.length; | |
| } | |
| /** | |
| * | |
| */ | |
| function calculateMetrics(data: InputData): Results { | |
| const aggregatedMetrics: AggregatedFolderMetrics = {}; | |
| let totalLoc = 0; | |
| for (const filePath in data) { | |
| const directoryPath = filePath.split('/').slice(0, -1).join('/'); | |
| const metrics: FileMetrics = data[filePath]; | |
| const base = createBaseObject(); | |
| const fileLoc = countLinesOfCode(filePath); | |
| totalLoc += fileLoc; | |
| aggregatedMetrics[directoryPath] = aggregatedMetrics[directoryPath] || base; | |
| const directoryData = aggregatedMetrics[directoryPath]; | |
| directoryData.afferent += metrics.afferent; | |
| directoryData.efferent += metrics.efferent; | |
| directoryData.abstractions += metrics.abstractions; | |
| directoryData.concretions += metrics.concretions; | |
| directoryData.loc += fileLoc; | |
| } | |
| const result: ResultEntry[] = Object.entries(aggregatedMetrics).map( | |
| ([directoryPath, directoryData]) => { | |
| const { afferent, efferent, abstractions, concretions, loc } = directoryData; | |
| const percent = parseFloat(((loc / totalLoc) * 100).toFixed(2)); | |
| return { | |
| directoryPath, | |
| afferent, | |
| efferent, | |
| abstractions, | |
| concretions, | |
| loc, | |
| percent | |
| }; | |
| } | |
| ); | |
| return { | |
| results: result, | |
| totalLinesOfCode: totalLoc, | |
| loc: maxBy('loc', result), | |
| afferent: maxBy('abstractions', result), | |
| efferent: maxBy('concretions', result), | |
| abstractions: maxBy('abstractions', result), | |
| concretions: maxBy('concretions', result) | |
| }; | |
| } | |
| const createBaseObject = () => ({ | |
| afferent: 0, | |
| efferent: 0, | |
| abstractions: 0, | |
| concretions: 0, | |
| loc: 0 | |
| }); | |
| const maxBy = (property: keyof ResultEntry, result: ResultEntry[]) => | |
| result.reduce((max, entry) => (entry[property] > max[property] ? entry : max), result[0]) | |
| .directoryPath; | |
| const results = calculateMetrics(inputData); | |
| writeFileSync('folder-metrics.json', JSON.stringify(results, null, 2)); | |
| console.log('Results:', results); | 
  
    Sign up for free
    to join this conversation on GitHub.
    Already have an account?
    Sign in to comment