Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .github/count-entities.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
const { Project, SyntaxKind } = require('ts-morph');
const fs = require('fs');
const path = require('path');

const repoRoot = path.resolve(__dirname, '..');
const tsConfigPath = path.join(repoRoot, 'tsconfig.json');
const outputPath = path.join(repoRoot, '.github', 'tsmorph-counts.json');

const project = new Project({
tsConfigFilePath: tsConfigPath
});

const sourceFiles = project
.getSourceFiles()
.filter(file => file.getFilePath().endsWith('.ts'));

const counts = {
modules: 0,
classes: 0,
interfaces: 0,
methods: 0,
functions: 0,
accesses: 0,
invocations: 0,
inheritances: 0,
concretisations: 0,
imports: 0,
exports: 0
};

for (const sourceFile of sourceFiles) {
counts.modules++;
counts.imports += sourceFile.getImportDeclarations().length;
counts.exports += sourceFile.getExportDeclarations().length;

sourceFile.forEachDescendant(node => {
switch (node.getKind()) {
case SyntaxKind.ClassDeclaration: {
counts.classes++;
const cls = node.asKindOrThrow(SyntaxKind.ClassDeclaration);
if (cls.getExtends()) {
counts.inheritances++;
}
counts.concretisations += cls.getImplements().length;
break;
}
case SyntaxKind.InterfaceDeclaration:
counts.interfaces++;
break;
case SyntaxKind.MethodDeclaration:
counts.methods++;
break;
case SyntaxKind.FunctionDeclaration:
counts.functions++;
break;
case SyntaxKind.CallExpression:
counts.invocations++;
break;
case SyntaxKind.PropertyAccessExpression:
counts.accesses++;
break;
}
});
}

fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, JSON.stringify(counts, null, 2));

console.log('ts-morph entity counts exported:');
console.log(counts);
20 changes: 20 additions & 0 deletions .github/moose-analysis.ston
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
SmalltalkCISpec {
#loading : [
SCIMetacelloLoadSpec {
#baseline : 'FamixTypeScript',
#repository : 'github://fuhrmanator/FamixTypeScript:master/src',
#platforms : [ #pharo ],
#onConflict : #useIncoming,
#onUpgrade : #useIncoming
}
],
#testing : {
#exclude : {
#packages : [ 'Famix-TypeScript-Tests' ]
},
#failOnZeroTests : false
},
#preTesting : SCICustomScript {
#path : 'pharo-metrics-export.st'
}
}
111 changes: 111 additions & 0 deletions .github/pharo-metrics-export.st
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"============================================================"
"Pharo Script - Export metrics from FamixTypeScript model to Markdown"
" it is better to test this script in a Playground if you modify it"
"============================================================"
| modelFile model classes stream writer srcDir counts json |

"--- 1. Locate the model.json file ---"
"First, search in the current directory,"
"then in the GitHub Actions workspace if not found."
"The model must be in the same location where it was generated by ts2famix."
"Otherwise, the SourceAnchors will not be correct and the metrics"
" concerning the source code, e.g. numberOfLinesOfCode"
" will not be correct."
modelFile := './model.json' asFileReference.

modelFile exists ifFalse: [
modelFile := (Smalltalk os environment at: 'GITHUB_WORKSPACE' ifAbsent: [ '.' ]), '/model.json'.
modelFile := modelFile asFileReference.
].

Transcript show: 'CI Export Script - Start'; cr.
Transcript show: 'Model file path: ', modelFile fullName; cr.
Transcript show: 'File exists: ', modelFile exists printString; cr.

modelFile exists ifFalse: [
Transcript show: 'model.json not found'; cr.
self error: 'model.json not found'.
].

"--- 2. Load the JSON model into Moose ---"
"FamixTypeScriptModel is the metamodel class loaded in the previous"
"step by Metacello (via .ston)."
"importFromJSONStream: reads the JSON and creates Famix entities in memory."
Transcript show: 'Loading model into Moose...'; cr.
modelFile readStreamDo: [ :readStream |
model := FamixTypeScriptModel new importFromJSONStream: readStream.
"To find the source code of the model"
model rootFolder: './'.
].
Transcript show: 'Model loaded.'; cr.

"--- 3. Extract Entity Counts ---"
counts := Dictionary new.


"Modules / source files"
"counts"
"at: 'modules'"
"put: model allSourceFiles size."


"Classes"
counts
at: 'classes'
put: (model allWithType: FamixTypeScriptClass) size.


"Interfaces"
counts
at: 'interfaces'
put: (model allWithType: FamixTypeScriptInterface) size.


"Methods"
counts
at: 'methods'
put: (model allWithType: FamixTypeScriptMethod) size.


"Functions"
counts
at: 'functions'
put: (model allWithType: FamixTypeScriptFunction) size.


"Accesses"
counts
at: 'accesses'
put: (model allWithType: FamixTypeScriptAccess) size.


"Invocations"
counts
at: 'invocations'
put: (model allWithType: FamixTypeScriptInvocation) size.


"Inheritance relationships"
counts
at: 'inheritances'
put: (model allWithType: FamixTypeScriptInheritance) size.

"Imports"
counts
at: 'imports'
put: (model allWithType: FamixTypeScriptImportClause) size.




json := NeoJSONWriter toString: counts.


(FileLocator workingDirectory / 'moose-counts.json')
writeStreamDo: [ :stream |
stream nextPutAll: json ].


Transcript
show: 'Moose entity counts exported';
cr.
63 changes: 63 additions & 0 deletions .github/run-count-comparison.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT_DIR"

node .github/count-entities.js

if [ ! -f model.json ]; then
echo "model.json not found" >&2
exit 1
fi

if [ ! -f moose-counts.json ]; then
echo "moose-counts.json not found" >&2
exit 1
fi

TSMORPH_COUNTS=".github/tsmorph-counts.json"
if [ ! -f "$TSMORPH_COUNTS" ]; then
echo "$TSMORPH_COUNTS not found" >&2
exit 1
fi

python3 - <<'PY'
import json, os, sys
from pathlib import Path

root = Path('.').resolve()
with open(root / '.github' / 'tsmorph-counts.json', 'r', encoding='utf-8') as f:
tsmorph = json.load(f)
with open(root / 'moose-counts.json', 'r', encoding='utf-8') as f:
famix = json.load(f)

keys = sorted(set(tsmorph) | set(famix))
missing_in_famix = [k for k in keys if k not in famix]
missing_in_tsmorph = [k for k in keys if k not in tsmorph]

print('Comparing ts-morph counts with Famix counts...')
print('ts-morph keys:', sorted(tsmorph))
print('famix keys:', sorted(famix))

if missing_in_famix:
print('Missing in Famix counts:', missing_in_famix)
if missing_in_tsmorph:
print('Missing in ts-morph counts:', missing_in_tsmorph)

for key in keys:
if key not in tsmorph or key not in famix:
continue
if tsmorph[key] != famix[key]:
print(f'{key}: ts-morph={tsmorph[key]} famix={famix[key]}')

if missing_in_famix or missing_in_tsmorph:
sys.exit(1)

# Require that every ts-morph key exists in Famix and that the counts match exactly.
if any(tsmorph[key] != famix[key] for key in keys):
print('Count mismatch detected.')
sys.exit(1)

print('All counts match.')
PY
13 changes: 13 additions & 0 deletions .github/tsmorph-counts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"modules": 62,
"classes": 47,
"interfaces": 1,
"methods": 213,
"functions": 75,
"accesses": 3395,
"invocations": 2239,
"inheritances": 42,
"concretisations": 0,
"imports": 254,
"exports": 47
}
70 changes: 70 additions & 0 deletions .github/workflows/end-to-end-entity-verification.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: TypeScript Analysis with Moose

on:
push:
branches: [ taha-dev ]
# pull_request:
# branches: [ main ]
workflow_dispatch:

jobs:
analyze:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: true

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'

- name: Install project dependencies
run: npm install

- name: Install ts2famix
run: npm install -g ts2famix

- name: Generate Famix model (JSON)
run: |
# -l 4 is log warnings only
ts2famix -l 4 -i tsconfig.json -o model.json
echo "Model generated: $(wc -c < model.json) bytes"
# Quick verification that the JSON is valid
python3 -c "import json; json.load(open('model.json')); print('JSON valid')"

# Install Pharo/Moose
- name: Install smalltalkCI (Moose image)
uses: hpi-swa/setup-smalltalkCI@v1
id: smalltalkci
with:
# Moose64-12 = Moose on Pharo 12, compatible with FamixTypeScript
smalltalk-image: Moose64-12

# Load FamixTypeScript metamodel and run analysis
- name: Load FamixTypeScript and export metrics
run: |
# the .ston file loads the metamodel and runs a script to do analysis (see .ston file fore more info)
smalltalkci -s ${{ steps.smalltalkci.outputs.smalltalk-image }} .github/moose-analysis.ston
shell: bash
timeout-minutes: 20

- name: Run ts-morph and Famix count comparison
run: bash .github/run-count-comparison.sh

- name: Upload metrics artifact
uses: actions/upload-artifact@v4
with:
name: export-metrics
path: |
moose-counts.json
.github/tsmorph-counts.json
if-no-files-found: error