@m4x_7/type-debt 1.0.0

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/constants.ts ADDED
@@ -0,0 +1,15 @@
1
+ export const CONSTANTS = {
2
+
3
+ CHUNK_SIZE: 50,
4
+
5
+ TYPE_DEBT_METRICS:{
6
+ EXPLICIT_ANY_WEIGHT: 2,
7
+ IMPLICIT_ANY_WEIGHT: 3,
8
+ AS_ANY_WEIGHT: 2,
9
+ SUPPRESSION_WEIGHT: 4,
10
+ NON_NULL_ASSERTION_WEIGHT: 1,
11
+ },
12
+
13
+ } as const;
14
+
15
+
@@ -0,0 +1,27 @@
1
+ import { Project } from "ts-morph";
2
+ import { getTypeDebt } from "./typeDebt";
3
+ import { FileReport } from "../types";
4
+
5
+
6
+ export function processChunk(chunk: string[]): FileReport[] {
7
+
8
+ const project = new Project({ skipAddingFilesFromTsConfig: true });
9
+ project.addSourceFilesAtPaths(chunk);
10
+
11
+ const chunkResults: FileReport[] = [];
12
+
13
+ project.forgetNodesCreatedInBlock(() => {
14
+ for (const file of project.getSourceFiles()) {
15
+ chunkResults.push({
16
+ filePath: file.getFilePath(),
17
+ typeDebtMetrics: getTypeDebt(file)
18
+ });
19
+ }
20
+ });
21
+
22
+ for (const file of project.getSourceFiles()) {
23
+ project.removeSourceFile(file);
24
+ }
25
+
26
+ return chunkResults;
27
+ }
@@ -0,0 +1,46 @@
1
+ import { promises as fs } from "fs";
2
+ import path from "path";
3
+ import { FileReport } from "../types"
4
+
5
+
6
+ export async function generateMarkdownReport(results: FileReport[], targetDir: string, outputPath = "type-debt-report.md") {
7
+ if (results.length === 0) return;
8
+
9
+ const totalFiles = results.length;
10
+ const totalScore = results.reduce((acc, curr) => acc + curr.typeDebtMetrics.score, 0);
11
+ const averageScore = Math.round(totalScore / totalFiles);
12
+
13
+ const sortedResults = [...results].sort((a, b) =>
14
+ a.typeDebtMetrics.score - b.typeDebtMetrics.score
15
+ );
16
+
17
+ let md = `Type Debt Report\n\n`;
18
+ md += `> Generated on: ${new Date().toUTCString()}\n\n`;
19
+
20
+ md += `## Summary\n`;
21
+ md += `- Total Files Scanned: ${totalFiles}\n`;
22
+ md += `- Average Score: ${averageScore}/100\n\n`;
23
+
24
+ md += `## Top 10 Worst Offenders\n\n`;
25
+
26
+ md += `| Score | File Path | Suppressions | Implicit \`any\` | Explicit \`any\` | \`as any\` | Non-Null (\`!\`) |\n`;
27
+
28
+ md += `| :---: | :--- | :---: | :---: | :---: | :---: | :---: |\n`;
29
+
30
+ const worstOffenders = sortedResults.slice(0, 10);
31
+
32
+ const absoluteTargetDir = path.resolve(targetDir);
33
+
34
+ for (const result of worstOffenders) {
35
+ const metrics = result.typeDebtMetrics;
36
+ const cleanPath = path.relative(absoluteTargetDir, result.filePath);
37
+ md += `| ${metrics.score} | \`${cleanPath}\` | ${metrics.suppressions} | ${metrics.implicitAny} | ${metrics.explicitAny} | ${metrics.asAny} | ${metrics.nonNullAssertions} |\n`;
38
+ }
39
+
40
+ try {
41
+ await fs.writeFile(outputPath, md, "utf-8");
42
+ console.log(`\nMarkdown report successfully generated at: ${outputPath}`);
43
+ } catch (error) {
44
+ console.error(`\nFailed to write Markdown report:`, error);
45
+ }
46
+ }
@@ -0,0 +1,125 @@
1
+ import { SourceFile, SyntaxKind } from "ts-morph";
2
+ import { TypeDebtMetrics } from "../types";
3
+ import { CONSTANTS } from "../constants";
4
+
5
+
6
+ const SUPPRESSION_REGEX = /(?:\/\/|\/\*)\s*@ts-(ignore|expect-error|nocheck)/g;
7
+
8
+ export function getTypeDebt(sourceFile: SourceFile): TypeDebtMetrics {
9
+
10
+ const metrics: TypeDebtMetrics = {
11
+ explicitAny: 0,
12
+ implicitAny: 0,
13
+ asAny: 0,
14
+ suppressions: 0,
15
+ nonNullAssertions: 0,
16
+ validTypes: 0,
17
+ score: 0
18
+ }
19
+
20
+ sourceFile.forEachDescendant(node => {
21
+
22
+ const kind = node.getKind();
23
+
24
+ // explicit any
25
+ if (kind === SyntaxKind.AnyKeyword) {
26
+ const parent = node.getParent();
27
+ if (parent?.getKind() !== SyntaxKind.AsExpression) {
28
+ metrics.explicitAny++;
29
+ }
30
+ }
31
+
32
+ // as any
33
+ else if (kind === SyntaxKind.AsExpression) {
34
+
35
+ const asExpression = node.asKindOrThrow(SyntaxKind.AsExpression);
36
+
37
+ if (asExpression.getTypeNode()?.getKind() === SyntaxKind.AnyKeyword) {
38
+ metrics.asAny++;
39
+ }
40
+
41
+ }
42
+
43
+ // implicit any
44
+ if (kind === SyntaxKind.Parameter) {
45
+ const parameter = node.asKindOrThrow(SyntaxKind.Parameter);
46
+
47
+ if (!parameter.getTypeNode() && !parameter.getInitializer()) {
48
+ const parentFunction = parameter.getParent();
49
+ const grandParent = parentFunction?.getParent();
50
+
51
+ // Ignore parameters in callbacks (e.g., .map(item => ...))
52
+ const isCallback =
53
+ (parentFunction?.getKind() === SyntaxKind.ArrowFunction ||
54
+ parentFunction?.getKind() === SyntaxKind.FunctionExpression) &&
55
+ grandParent?.getKind() === SyntaxKind.CallExpression;
56
+
57
+ if (!isCallback) {
58
+ metrics.implicitAny++;
59
+ }
60
+ }
61
+ }
62
+
63
+ // not null expression
64
+ if (kind === SyntaxKind.NonNullExpression) {
65
+ metrics.nonNullAssertions++;
66
+ }
67
+
68
+
69
+ // valid types
70
+ else if (isValidType(kind)) {
71
+ metrics.validTypes++;
72
+ }
73
+
74
+ });
75
+
76
+ //suppressions
77
+ const text = sourceFile.getFullText();
78
+ const matches = text.match(SUPPRESSION_REGEX);
79
+ metrics.suppressions = matches ? matches.length : 0;
80
+
81
+ metrics.score = calculateScore(metrics);
82
+
83
+ return metrics;
84
+ }
85
+
86
+ export function calculateScore(metrics: TypeDebtMetrics): number {
87
+
88
+ const totalDebtInstances =
89
+ metrics.explicitAny +
90
+ metrics.implicitAny +
91
+ metrics.asAny +
92
+ metrics.suppressions +
93
+ metrics.nonNullAssertions;
94
+
95
+ const totalTypeNodes = metrics.validTypes + totalDebtInstances;
96
+
97
+ if (totalTypeNodes == 0) {
98
+ return 100;
99
+ }
100
+
101
+ else {
102
+ const baseScore = (metrics.validTypes / totalTypeNodes) * 100;
103
+ const rawPenalty = calculatePenalty(metrics);
104
+ const scaledPenalty = (rawPenalty / totalTypeNodes) * 100;
105
+ return Math.max(0, Math.round(baseScore - scaledPenalty));
106
+ }
107
+
108
+ }
109
+
110
+ export function calculatePenalty(metrics: TypeDebtMetrics): number {
111
+ return metrics.explicitAny * CONSTANTS.TYPE_DEBT_METRICS.EXPLICIT_ANY_WEIGHT
112
+ + metrics.implicitAny * CONSTANTS.TYPE_DEBT_METRICS.IMPLICIT_ANY_WEIGHT
113
+ + metrics.asAny * CONSTANTS.TYPE_DEBT_METRICS.AS_ANY_WEIGHT
114
+ + metrics.nonNullAssertions * CONSTANTS.TYPE_DEBT_METRICS.NON_NULL_ASSERTION_WEIGHT
115
+ + metrics.suppressions * CONSTANTS.TYPE_DEBT_METRICS.SUPPRESSION_WEIGHT;
116
+ }
117
+
118
+ function isValidType(kind: SyntaxKind): boolean {
119
+ return (kind === SyntaxKind.TypeReference ||
120
+ kind === SyntaxKind.StringKeyword ||
121
+ kind === SyntaxKind.NumberKeyword ||
122
+ kind === SyntaxKind.BooleanKeyword ||
123
+ kind === SyntaxKind.InterfaceDeclaration ||
124
+ kind === SyntaxKind.TypeAliasDeclaration);
125
+ }
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ var X=Object.defineProperty;var P=Object.getOwnPropertySymbols;var Z=Object.prototype.hasOwnProperty,V=Object.prototype.propertyIsEnumerable;var A=(t,e,s)=>e in t?X(t,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):t[e]=s,w=(t,e)=>{for(var s in e||(e={}))Z.call(e,s)&&A(t,s,e[s]);if(P)for(var s of P(e))V.call(e,s)&&A(t,s,e[s]);return t};var c=(t,e,s)=>A(t,typeof e!="symbol"?e+"":e,s);var S=(t,e,s)=>new Promise((r,i)=>{var o=u=>{try{n(s.next(u))}catch(h){i(h)}},l=u=>{try{n(s.throw(u))}catch(h){i(h)}},n=u=>u.done?r(u.value):Promise.resolve(u.value).then(o,l);n((s=s.apply(t,e)).next())});import{createRequire as J}from"module";import{basename as tt,dirname as k,normalize as et,relative as st,resolve as rt,sep as I}from"path";import*as it from"fs";var Vt={};var _=J(Vt.url);function nt(t){let e=et(t);return e.length>1&&e[e.length-1]===I&&(e=e.substring(0,e.length-1)),e}var ot=/[\\/]/g;function R(t,e){return t.replace(ot,e)}var lt=/^[a-z]:[\\/]$/i;function ut(t){return t==="/"||lt.test(t)}function F(t,e){let{resolvePaths:s,normalizePath:r,pathSeparator:i}=e,o=process.platform==="win32"&&t.includes("/")||t.startsWith(".");if(s&&(t=rt(t)),(r||o)&&(t=nt(t)),t===".")return"";let l=t[t.length-1]!==i;return R(l?t+i:t,i)}function C(t,e){return e+t}function ct(t,e){return function(s,r){return r.startsWith(t)?r.slice(t.length)+s:R(st(t,r),e.pathSeparator)+e.pathSeparator+s}}function at(t){return t}function ht(t,e,s){return e+t+s}function pt(t,e){let{relativePaths:s,includeBasePath:r}=e;return s&&t?ct(t,e):r?C:at}function ft(t){return function(e,s){s.push(e.substring(t.length)||".")}}function yt(t){return function(e,s,r){let i=e.substring(t.length)||".";r.every(o=>o(i,!0))&&s.push(i)}}var mt=(t,e)=>{e.push(t||".")},gt=(t,e,s)=>{let r=t||".";s.every(i=>i(r,!0))&&e.push(r)},dt=()=>{};function St(t,e){let{includeDirs:s,filters:r,relativePaths:i}=e;return s?i?r&&r.length?yt(t):ft(t):r&&r.length?gt:mt:dt}var wt=(t,e,s,r)=>{r.every(i=>i(t,!1))&&s.files++},Tt=(t,e,s,r)=>{r.every(i=>i(t,!1))&&e.push(t)},Et=(t,e,s,r)=>{s.files++},bt=(t,e)=>{e.push(t)},xt=()=>{};function At(t){let{excludeFiles:e,filters:s,onlyCounts:r}=t;return e?xt:s&&s.length?r?wt:Tt:r?Et:bt}var Ft=t=>t,kt=()=>[""].slice(0,0);function Dt(t){return t.group?kt:Ft}var Pt=(t,e,s)=>{t.push({directory:e,files:s,dir:e})},_t=()=>{};function vt(t){return t.group?Pt:_t}var It=function(t,e,s){let{queue:r,fs:i,options:{suppressErrors:o}}=e;r.enqueue(),i.realpath(t,(l,n)=>{if(l)return r.dequeue(o?null:l,e);i.stat(n,(u,h)=>{if(u)return r.dequeue(o?null:u,e);if(h.isDirectory()&&N(t,n,e))return r.dequeue(null,e);s(h,n),r.dequeue(null,e)})})},Rt=function(t,e,s){let{queue:r,fs:i,options:{suppressErrors:o}}=e;r.enqueue();try{let l=i.realpathSync(t),n=i.statSync(l);if(n.isDirectory()&&N(t,l,e))return;s(n,l)}catch(l){if(!o)throw l}};function Ct(t,e){return!t.resolveSymlinks||t.excludeSymlinks?null:e?Rt:It}function N(t,e,s){if(s.options.useRealPaths)return Nt(e,s);let r=k(t),i=1;for(;r!==s.root&&i<2;){let o=s.symlinks.get(r);!!o&&(o===e||o.startsWith(e)||e.startsWith(o))?i++:r=k(r)}return s.symlinks.set(t,e),i>1}function Nt(t,e){return e.visited.includes(t+e.options.pathSeparator)}var Mt=t=>t.counts,Wt=t=>t.groups,$t=t=>t.paths,Ot=t=>t.paths.slice(0,t.options.maxFiles),qt=(t,e,s)=>(T(e,s,t.counts,t.options.suppressErrors),null),Kt=(t,e,s)=>(T(e,s,t.paths,t.options.suppressErrors),null),Gt=(t,e,s)=>(T(e,s,t.paths.slice(0,t.options.maxFiles),t.options.suppressErrors),null),Bt=(t,e,s)=>(T(e,s,t.groups,t.options.suppressErrors),null);function T(t,e,s,r){e(t&&!r?t:null,s)}function Ht(t,e){let{onlyCounts:s,group:r,maxFiles:i}=t;return s?e?Mt:qt:r?e?Wt:Bt:i?e?Ot:Gt:e?$t:Kt}var M={withFileTypes:!0},jt=(t,e,s,r,i)=>{if(t.queue.enqueue(),r<0)return t.queue.dequeue(null,t);let{fs:o}=t;t.visited.push(e),t.counts.directories++,o.readdir(e||".",M,(l,n=[])=>{i(n,s,r),t.queue.dequeue(t.options.suppressErrors?null:l,t)})},Lt=(t,e,s,r,i)=>{let{fs:o}=t;if(r<0)return;t.visited.push(e),t.counts.directories++;let l=[];try{l=o.readdirSync(e||".",M)}catch(n){if(!t.options.suppressErrors)throw n}i(l,s,r)};function Yt(t){return t?Lt:jt}var Ut=class{constructor(t){c(this,"count",0);this.onQueueEmpty=t}enqueue(){return this.count++,this.count}dequeue(t,e){this.onQueueEmpty&&(--this.count<=0||t)&&(this.onQueueEmpty(t,e),t&&(e.controller.abort(),this.onQueueEmpty=void 0))}},zt=class{constructor(){c(this,"_files",0);c(this,"_directories",0)}set files(t){this._files=t}get files(){return this._files}set directories(t){this._directories=t}get directories(){return this._directories}get dirs(){return this._directories}},Qt=class{constructor(){c(this,"aborted",!1)}abort(){this.aborted=!0}},W=class{constructor(t,e,s){c(this,"root");c(this,"isSynchronous");c(this,"state");c(this,"joinPath");c(this,"pushDirectory");c(this,"pushFile");c(this,"getArray");c(this,"groupFiles");c(this,"resolveSymlink");c(this,"walkDirectory");c(this,"callbackInvoker");c(this,"walk",(t,e,s)=>{let{paths:r,options:{filters:i,resolveSymlinks:o,excludeSymlinks:l,exclude:n,maxFiles:u,signal:h,useRealPaths:p,pathSeparator:f},controller:E}=this.state;if(E.aborted||h&&h.aborted||u&&r.length>u)return;let b=this.getArray(this.state.paths);for(let x=0;x<t.length;++x){let g=t[x];if(g.isFile()||g.isSymbolicLink()&&!o&&!l){let y=this.joinPath(g.name,e);this.pushFile(y,b,this.state.counts,i)}else if(g.isDirectory()){let y=ht(g.name,e,this.state.options.pathSeparator);if(n&&n(g.name,y))continue;this.pushDirectory(y,r,i),this.walkDirectory(this.state,y,y,s-1,this.walk)}else if(this.resolveSymlink&&g.isSymbolicLink()){let y=C(g.name,e);this.resolveSymlink(y,this.state,(U,m)=>{if(U.isDirectory()){if(m=F(m,this.state.options),n&&n(g.name,p?m:y+f))return;this.walkDirectory(this.state,m,p?m:y+f,s-1,this.walk)}else{m=p?m:y;let z=tt(m),Q=F(k(m),this.state.options);m=this.joinPath(z,Q),this.pushFile(m,b,this.state.counts,i)}})}}this.groupFiles(this.state.groups,e,b)});this.isSynchronous=!s,this.callbackInvoker=Ht(e,this.isSynchronous),this.root=F(t,e),this.state={root:ut(this.root)?this.root:this.root.slice(0,-1),paths:[""].slice(0,0),groups:[],counts:new zt,options:e,queue:new Ut((r,i)=>this.callbackInvoker(i,r,s)),symlinks:new Map,visited:[""].slice(0,0),controller:new Qt,fs:e.fs||it},this.joinPath=pt(this.root,e),this.pushDirectory=St(this.root,e),this.pushFile=At(e),this.getArray=Dt(e),this.groupFiles=vt(e),this.resolveSymlink=Ct(e,this.isSynchronous),this.walkDirectory=Yt(this.isSynchronous)}start(){return this.pushDirectory(this.root,this.state.paths,this.state.options.filters),this.walkDirectory(this.state,this.root,this.root,this.state.options.maxDepth,this.walk),this.isSynchronous?this.callbackInvoker(this.state,null):null}};function Xt(t,e){return new Promise((s,r)=>{$(t,e,(i,o)=>{if(i)return r(i);s(o)})})}function $(t,e,s){new W(t,e,s).start()}function Zt(t,e){return new W(t,e).start()}var v=class{constructor(t,e){this.root=t,this.options=e}withPromise(){return Xt(this.root,this.options)}withCallback(t){$(this.root,this.options,t)}sync(){return Zt(this.root,this.options)}},O=null;try{_.resolve("picomatch"),O=_("picomatch")}catch(t){}var q=class{constructor(t){c(this,"globCache",{});c(this,"options",{maxDepth:1/0,suppressErrors:!0,pathSeparator:I,filters:[]});c(this,"globFunction");this.options=w(w({},this.options),t),this.globFunction=this.options.globFunction}group(){return this.options.group=!0,this}withPathSeparator(t){return this.options.pathSeparator=t,this}withBasePath(){return this.options.includeBasePath=!0,this}withRelativePaths(){return this.options.relativePaths=!0,this}withDirs(){return this.options.includeDirs=!0,this}withMaxDepth(t){return this.options.maxDepth=t,this}withMaxFiles(t){return this.options.maxFiles=t,this}withFullPaths(){return this.options.resolvePaths=!0,this.options.includeBasePath=!0,this}withErrors(){return this.options.suppressErrors=!1,this}withSymlinks({resolvePaths:t=!0}={}){return this.options.resolveSymlinks=!0,this.options.useRealPaths=t,this.withFullPaths()}withAbortSignal(t){return this.options.signal=t,this}normalize(){return this.options.normalizePath=!0,this}filter(t){return this.options.filters.push(t),this}onlyDirs(){return this.options.excludeFiles=!0,this.options.includeDirs=!0,this}exclude(t){return this.options.exclude=t,this}onlyCounts(){return this.options.onlyCounts=!0,this}crawl(t){return new v(t||".",this.options)}withGlobFunction(t){return this.globFunction=t,this}crawlWithOptions(t,e){return this.options=w(w({},this.options),e),new v(t||".",this.options)}glob(...t){return this.globFunction?this.globWithOptions(t):this.globWithOptions(t,{dot:!0})}globWithOptions(t,...e){let s=this.globFunction||O;if(!s)throw new Error("Please specify a glob function to use glob matching.");var r=this.globCache[t.join("\0")];return r||(r=s(t,...e),this.globCache[t.join("\0")]=r),this.options.filters.push(i=>r(i)),this}};import{stat as Jt}from"fs/promises";function K(t){return new q().withFullPaths().exclude(s=>s==="node_modules"||s==="dist"||s==="build"||s===".git").filter(s=>s.endsWith(".ts")||s.endsWith(".tsx")).crawl(t).sync()}function G(t,e){let s=[];for(let r=0;r<t.length;r+=e)s.push(t.slice(r,r+e));return s}function B(t){return S(this,null,function*(){let e;try{e=yield Jt(t)}catch(s){throw new Error(`Directory "${t}" does not exist.`)}if(!e.isDirectory())throw new Error(`"${t}" is not a directory.`)})}import{Project as ie}from"ts-morph";import{SyntaxKind as a}from"ts-morph";var d={CHUNK_SIZE:50,TYPE_DEBT_METRICS:{EXPLICIT_ANY_WEIGHT:2,IMPLICIT_ANY_WEIGHT:3,AS_ANY_WEIGHT:2,SUPPRESSION_WEIGHT:4,NON_NULL_ASSERTION_WEIGHT:1}};var te=/(?:\/\/|\/\*)\s*@ts-(ignore|expect-error|nocheck)/g;function H(t){let e={explicitAny:0,implicitAny:0,asAny:0,suppressions:0,nonNullAssertions:0,validTypes:0,score:0};t.forEachDescendant(i=>{var l;let o=i.getKind();if(o===a.AnyKeyword){let n=i.getParent();(n==null?void 0:n.getKind())!==a.AsExpression&&e.explicitAny++}else o===a.AsExpression&&((l=i.asKindOrThrow(a.AsExpression).getTypeNode())==null?void 0:l.getKind())===a.AnyKeyword&&e.asAny++;if(o===a.Parameter){let n=i.asKindOrThrow(a.Parameter);if(!n.getTypeNode()&&!n.getInitializer()){let u=n.getParent(),h=u==null?void 0:u.getParent();((u==null?void 0:u.getKind())===a.ArrowFunction||(u==null?void 0:u.getKind())===a.FunctionExpression)&&(h==null?void 0:h.getKind())===a.CallExpression||e.implicitAny++}}o===a.NonNullExpression?e.nonNullAssertions++:re(o)&&e.validTypes++});let r=t.getFullText().match(te);return e.suppressions=r?r.length:0,e.score=ee(e),e}function ee(t){let e=t.explicitAny+t.implicitAny+t.asAny+t.suppressions+t.nonNullAssertions,s=t.validTypes+e;if(s==0)return 100;{let r=t.validTypes/s*100,o=se(t)/s*100;return Math.max(0,Math.round(r-o))}}function se(t){return t.explicitAny*d.TYPE_DEBT_METRICS.EXPLICIT_ANY_WEIGHT+t.implicitAny*d.TYPE_DEBT_METRICS.IMPLICIT_ANY_WEIGHT+t.asAny*d.TYPE_DEBT_METRICS.AS_ANY_WEIGHT+t.nonNullAssertions*d.TYPE_DEBT_METRICS.NON_NULL_ASSERTION_WEIGHT+t.suppressions*d.TYPE_DEBT_METRICS.SUPPRESSION_WEIGHT}function re(t){return t===a.TypeReference||t===a.StringKeyword||t===a.NumberKeyword||t===a.BooleanKeyword||t===a.InterfaceDeclaration||t===a.TypeAliasDeclaration}function j(t){let e=new ie({skipAddingFilesFromTsConfig:!0});e.addSourceFilesAtPaths(t);let s=[];e.forgetNodesCreatedInBlock(()=>{for(let r of e.getSourceFiles())s.push({filePath:r.getFilePath(),typeDebtMetrics:H(r)})});for(let r of e.getSourceFiles())e.removeSourceFile(r);return s}import{promises as ne}from"fs";import L from"path";function Y(t,e,s="type-debt-report.md"){return S(this,null,function*(){if(t.length===0)return;let r=t.length,i=t.reduce((p,f)=>p+f.typeDebtMetrics.score,0),o=Math.round(i/r),l=[...t].sort((p,f)=>p.typeDebtMetrics.score-f.typeDebtMetrics.score),n=`Type Debt Report
3
+
4
+ `;n+=`> Generated on: ${new Date().toUTCString()}
5
+
6
+ `,n+=`## Summary
7
+ `,n+=`- Total Files Scanned: ${r}
8
+ `,n+=`- Average Score: ${o}/100
9
+
10
+ `,n+=`## Top 10 Worst Offenders
11
+
12
+ `,n+="| Score | File Path | Suppressions | Implicit `any` | Explicit `any` | `as any` | Non-Null (`!`) |\n",n+=`| :---: | :--- | :---: | :---: | :---: | :---: | :---: |
13
+ `;let u=l.slice(0,10),h=L.resolve(e);for(let p of u){let f=p.typeDebtMetrics,E=L.relative(h,p.filePath);n+=`| ${f.score} | \`${E}\` | ${f.suppressions} | ${f.implicitAny} | ${f.explicitAny} | ${f.asAny} | ${f.nonNullAssertions} |
14
+ `}try{yield ne.writeFile(s,n,"utf-8"),console.log(`
15
+ Markdown report successfully generated at: ${s}`)}catch(p){console.error(`
16
+ Failed to write Markdown report:`,p)}})}var D=process.argv.slice(2);(D.includes("--help")||D.includes("-h"))&&(ue(),process.exit(0));var oe=D[0]||".";le(oe).catch(t=>{console.error(`
17
+ \x1B[31m Error: ${t.message}\x1B[0m
18
+ `),process.exit(1)});function le(t){return S(this,null,function*(){yield B(t),console.log(`
19
+ Scanning directory: ${t}`);let e=K(t);if(e.length===0)throw new Error("No TypeScript files found to process.");let s=G(e,d.CHUNK_SIZE),r=[];for(let[i,o]of s.entries()){let l=j(o);r=r.concat(l),process.stdout.write(`\rProcessed chunk ${i+1}/${s.length}`)}yield Y(r,t)})}function ue(){console.log(`
20
+ Type Debt Analyzer
21
+
22
+ Usage: npx tsx index.ts [directory]
23
+
24
+ Examples:
25
+ npx tsx index.ts .
26
+ npx tsx index.ts ./src
27
+ `),process.exit(0)}
package/index.ts ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { getTargetFiles } from "./utils.js";
4
+ import { chunkArray, validateDirectory } from "./utils.js";
5
+ import { processChunk } from "./core/chunkProcessor.js";
6
+ import { FileReport } from "./types.js";
7
+ import { CONSTANTS } from "./constants.js";
8
+ import { generateMarkdownReport } from "./core/reporter.js";
9
+
10
+
11
+
12
+ const args = process.argv.slice(2);
13
+
14
+ if (args.includes('--help') || args.includes('-h')) {
15
+ printHelp();
16
+ process.exit(0);
17
+ }
18
+
19
+ const target = args[0] || ".";
20
+
21
+ runCLI(target).catch((err) => {
22
+ console.error(`\n\x1b[31m Error: ${err.message}\x1b[0m\n`);
23
+ process.exit(1);
24
+ });
25
+
26
+
27
+ async function runCLI(targetDir: string) {
28
+
29
+ await validateDirectory(targetDir);
30
+
31
+ console.log(`\nScanning directory: ${targetDir}`);
32
+ const allFiles = getTargetFiles(targetDir);
33
+
34
+ if (allFiles.length === 0) {
35
+ throw new Error(`No TypeScript files found to process.`);
36
+ }
37
+
38
+ const fileChunks = chunkArray(allFiles, CONSTANTS.CHUNK_SIZE);
39
+
40
+ let allResults: FileReport[] = [];
41
+
42
+ for (const [index, chunk] of fileChunks.entries()) {
43
+
44
+ const chunkResults = processChunk(chunk);
45
+ allResults = allResults.concat(chunkResults);
46
+ process.stdout.write(`\rProcessed chunk ${index + 1}/${fileChunks.length}`);
47
+ }
48
+
49
+ await generateMarkdownReport(allResults, targetDir);
50
+
51
+ }
52
+
53
+ function printHelp() {
54
+ console.log(`
55
+ Type Debt Analyzer
56
+
57
+ Usage: npx tsx index.ts [directory]
58
+
59
+ Examples:
60
+ npx tsx index.ts .
61
+ npx tsx index.ts ./src
62
+ `);
63
+ process.exit(0);
64
+ }
65
+
66
+
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@m4x_7/type-debt",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "A high-performance TypeScript static analyzer that calculates Type Debt and generates reports.",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "type-debt": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsup index.ts --format esm --minify --clean",
12
+ "test": "vitest",
13
+ "dev": "nodemon --ignore temp_repos/ --exec ts-node main.ts"
14
+ },
15
+ "keywords": [
16
+ "typescript",
17
+ "analyzer",
18
+ "type-debt",
19
+ "cli"
20
+ ],
21
+ "author": "",
22
+ "license": "ISC",
23
+ "dependencies": {
24
+ "cors": "^2.8.6",
25
+ "express": "^5.2.1",
26
+ "jscpd": "^4.0.8",
27
+ "simple-git": "^3.35.2",
28
+ "ts-morph": "^27.0.2"
29
+ },
30
+ "devDependencies": {
31
+ "@types/cors": "^2.8.19",
32
+ "@types/express": "^5.0.6",
33
+ "@types/node": "^25.5.2",
34
+ "nodemon": "^3.1.14",
35
+ "ts-node": "^10.9.2",
36
+ "tsup": "^8.5.1",
37
+ "typescript": "^6.0.2",
38
+ "vitest": "^4.1.9"
39
+ }
40
+ }
@@ -0,0 +1,93 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { Project } from "ts-morph";
3
+ import { calculatePenalty, calculateScore, getTypeDebt } from "../core/typeDebt";
4
+ import { TypeDebtMetrics } from "../types";
5
+
6
+ describe("Type Debt Analysis", () => {
7
+ it("should correctly identify all forms of type debt", () => {
8
+
9
+ const project = new Project({ useInMemoryFileSystem: true });
10
+
11
+ const sourceCode = `
12
+
13
+ interface ValidUser { id: number; }
14
+
15
+ // @ts-ignore
16
+ const id: any = 123;
17
+
18
+ const user = fetchUser() as any;
19
+
20
+ function process(payload) {
21
+ console.log(payload!.data);
22
+ }
23
+ `;
24
+
25
+ const sourceFile = project.createSourceFile("test.ts", sourceCode);
26
+
27
+ const metrics = getTypeDebt(sourceFile);
28
+
29
+ expect(metrics.explicitAny).toBe(1);
30
+ expect(metrics.asAny).toBe(1);
31
+ expect(metrics.suppressions).toBe(1);
32
+ expect(metrics.implicitAny).toBe(1);
33
+ expect(metrics.nonNullAssertions).toBe(1);
34
+ expect(metrics.validTypes).toBeGreaterThan(0);
35
+
36
+ });
37
+
38
+ it("should correctly calculate penalty", () => {
39
+
40
+ const metrics: TypeDebtMetrics = {
41
+ explicitAny: 2,
42
+ implicitAny: 3,
43
+ asAny: 2,
44
+ suppressions: 1,
45
+ nonNullAssertions: 1,
46
+ validTypes: 40,
47
+ score: 0
48
+ }
49
+
50
+ const penalty = calculatePenalty(metrics);
51
+ expect(penalty).toBe(22);
52
+
53
+ });
54
+
55
+
56
+
57
+ it("should correctly calculate score for ordinary data", () => {
58
+
59
+ let metrics: TypeDebtMetrics = {
60
+ explicitAny: 2,
61
+ implicitAny: 3,
62
+ asAny: 2,
63
+ suppressions: 1,
64
+ nonNullAssertions: 1,
65
+ validTypes: 40,
66
+ score: 0
67
+ }
68
+
69
+ metrics.score = calculateScore(metrics);
70
+ expect(metrics.score).toBe(37);
71
+
72
+ });
73
+
74
+ it("should correctly calculate score when no type nodes were found in the file", () => {
75
+
76
+ let metrics: TypeDebtMetrics = {
77
+ explicitAny: 0,
78
+ implicitAny: 0,
79
+ asAny: 0,
80
+ suppressions: 0,
81
+ nonNullAssertions: 0,
82
+ validTypes: 0,
83
+ score: 0
84
+ }
85
+
86
+ metrics.score = calculateScore(metrics);
87
+ expect(metrics.score).toBe(100);
88
+
89
+ });
90
+
91
+
92
+ })
93
+
package/tsconfig.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ // Visit https://aka.ms/tsconfig to read more about this file
3
+ "compilerOptions": {
4
+ // File Layout
5
+ // "rootDir": "./src",
6
+ // "outDir": "./dist",
7
+
8
+ // Environment Settings
9
+ // See also https://aka.ms/tsconfig/module
10
+ "module": "CommonJS",
11
+ "esModuleInterop": true,
12
+ "target": "es2016",
13
+ "types": [],
14
+ // For nodejs:
15
+ // "lib": ["esnext"],
16
+ // "types": ["node"],
17
+ // and npm install -D @types/node
18
+
19
+ // Other Outputs
20
+ "sourceMap": true,
21
+ "declaration": true,
22
+ "declarationMap": true,
23
+
24
+ // Stricter Typechecking Options
25
+ "noUncheckedIndexedAccess": true,
26
+ "exactOptionalPropertyTypes": true,
27
+
28
+
29
+ // Style Options
30
+ // "noImplicitReturns": true,
31
+ // "noImplicitOverride": true,
32
+ // "noUnusedLocals": true,
33
+ // "noUnusedParameters": true,
34
+ // "noFallthroughCasesInSwitch": true,
35
+ // "noPropertyAccessFromIndexSignature": true,
36
+
37
+ // Recommended Options
38
+ "strict": true,
39
+ "jsx": "react-jsx",
40
+ "verbatimModuleSyntax": false,
41
+ "isolatedModules": true,
42
+ "noUncheckedSideEffectImports": true,
43
+ "moduleDetection": "force",
44
+ "skipLibCheck": true,
45
+ }
46
+ }
@@ -0,0 +1,22 @@
1
+ Type Debt Report
2
+
3
+ > Generated on: Mon, 29 Jun 2026 14:06:35 GMT
4
+
5
+ ## Summary
6
+ - Total Files Scanned: 709
7
+ - Average Score: 73/100
8
+
9
+ ## Top 10 Worst Offenders
10
+
11
+ | Score | File Path | Suppressions | Implicit `any` | Explicit `any` | `as any` | Non-Null (`!`) |
12
+ | :---: | :--- | :---: | :---: | :---: | :---: | :---: |
13
+ | 0 | `compiler/visitorPublic.ts` | 0 | 810 | 1 | 0 | 4 |
14
+ | 0 | `harness/harnessGlobals.ts` | 0 | 3 | 1 | 0 | 0 |
15
+ | 0 | `lib/es2017.intl.d.ts` | 0 | 0 | 11 | 0 | 0 |
16
+ | 0 | `server/moduleSpecifierCache.ts` | 0 | 22 | 0 | 0 | 0 |
17
+ | 0 | `services/codefixes/addEmptyExportDeclaration.ts` | 0 | 1 | 0 | 0 | 0 |
18
+ | 0 | `services/codefixes/addMissingDeclareProperty.ts` | 0 | 2 | 0 | 0 | 0 |
19
+ | 0 | `services/codefixes/addMissingInvocationForDecorator.ts` | 0 | 2 | 0 | 0 | 1 |
20
+ | 0 | `services/codefixes/addMissingResolutionModeImportAttribute.ts` | 0 | 2 | 0 | 0 | 1 |
21
+ | 0 | `services/codefixes/disableJsDiagnostics.ts` | 3 | 2 | 0 | 0 | 0 |
22
+ | 0 | `services/codefixes/fixAddMissingNewOperator.ts` | 0 | 2 | 0 | 0 | 0 |
package/types.ts ADDED
@@ -0,0 +1,14 @@
1
+ export interface FileReport {
2
+ filePath: string;
3
+ typeDebtMetrics: TypeDebtMetrics;
4
+ }
5
+
6
+ export interface TypeDebtMetrics {
7
+ explicitAny: number;
8
+ implicitAny: number;
9
+ asAny: number;
10
+ suppressions: number;
11
+ nonNullAssertions: number;
12
+ validTypes: number;
13
+ score: number;
14
+ }
package/utils.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { fdir } from "fdir";
2
+ import { stat } from "fs/promises";
3
+
4
+
5
+ export function getTargetFiles(directoryPath: string): string[] {
6
+ const crawler = new fdir()
7
+ .withFullPaths()
8
+ .exclude((dirName) =>
9
+ dirName === "node_modules" ||
10
+ dirName === "dist" ||
11
+ dirName === "build" ||
12
+ dirName === ".git"
13
+ )
14
+ .filter((path) => path.endsWith(".ts") || path.endsWith(".tsx"))
15
+ .crawl(directoryPath);
16
+
17
+ return crawler.sync() as string[];
18
+ }
19
+
20
+
21
+ export function chunkArray<T>(array: T[], chunkSize: number): T[][] {
22
+ const chunks: T[][] = [];
23
+ for (let i = 0; i < array.length; i += chunkSize) {
24
+ chunks.push(array.slice(i, i + chunkSize));
25
+ }
26
+ return chunks;
27
+ }
28
+
29
+
30
+ export async function validateDirectory(path: string) {
31
+
32
+ let stats;
33
+
34
+ try {
35
+ stats = await stat(path);
36
+ } catch {
37
+ throw new Error(`Directory "${path}" does not exist.`);
38
+ }
39
+
40
+ if (!stats.isDirectory()) {
41
+ throw new Error(`"${path}" is not a directory.`);
42
+ }
43
+ }