Browse Source

v1.2.0: documented percentage overhaul, got rid callParent() and newInstance()

CrazyDoctor 1 month ago
parent
commit
79c853a43b

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 	"name": "doczilla_js_docs",
-	"version": "1.1.4",
+	"version": "1.2.0",
 	"dependencies": {
 		"@types/sqlite3": "^3.1.11",
 		"@types/ws": "^8.5.10",

+ 6 - 4
src/comments/CommentsManager.ts

@@ -164,11 +164,13 @@ class CommentsManager {
 		try {
 			const classDir = CommentsManager.getClassDirPath(root, className);
 
-			if(!fs.existsSync(classDir)) {
+			try {
+				await fs.promises.access(classDir);
+			} catch(e) {
 				return { success: true, comments: [] };
 			}
 
-			const commentFiles = fs.readdirSync(classDir).filter(file => file !== '.' && file !== '..');
+			const commentFiles = (await fs.promises.readdir(classDir)).filter(file => file !== '.' && file !== '..');
 			const comments: IComment[] = [];
 
 			for(const propertyName of commentFiles) {
@@ -176,10 +178,10 @@ class CommentsManager {
 					continue;
 				const filePath = `${classDir}/${propertyName}`;
 
-				if(fs.lstatSync(filePath).isDirectory())
+				if((await fs.promises.lstat(filePath)).isDirectory())
 					continue;
 
-				const [author, timestamp, ...rest] = fs.readFileSync(filePath, 'utf-8').split(':');
+				const [author, timestamp, ...rest] = (await fs.promises.readFile(filePath, 'utf-8')).split(':');
 				const text = rest.join(':');
 
 				comments.push({ root: root, className: className, propertyName: propertyName, text: text, author: author, timestamp: Number.parseInt(timestamp) });

+ 118 - 0
src/routes/PostDocumentedInfo.ts

@@ -0,0 +1,118 @@
+import { Request, Response } from "express";
+import { HttpMethod, Route, StatusCodes } from "org.crazydoctor.expressts";
+import ServerApp from "..";
+import { ClassMapEntity, PackageEntity, Sources } from "../sources/Sources";
+import { CommentsManager } from "../comments/CommentsManager";
+
+type DocumentedClassInfo = {
+	documentable: number,
+	documented: number
+};
+
+type DocumentedPackageInfo = DocumentedClassInfo & {
+	children?: { [key: string]: DocumentedPackageInfo }
+};
+
+class GetDocumentedInfo extends Route {
+	protected action = (req: Request, res: Response) => {
+		const params = req.body;
+
+		if(ServerApp.SourcesUpdating) {
+			res.status(StatusCodes.ACCEPTED).render('sources-update');
+			return;
+		}
+
+		const root = params.root;
+		const packageName = params.packageName;
+		const className = params.className;
+
+		const cls: ClassMapEntity | null = Sources.findClass(className);
+		const pck: PackageEntity | null = Sources.findPackage(root, packageName);
+		
+		if(!root && !packageName && !className) {
+			this.getRootsDocumentedInfo().then((result) => {
+				res.status(StatusCodes.OK).type('application/json').send(JSON.stringify(result));
+			});
+		} else if(!cls && !pck) {
+			res.status(StatusCodes.NOT_FOUND).type('application/json').send({ success: false });
+		} else if(cls) {
+			this.getClassDocumentedInfo(cls).then((result) => {
+				res.status(StatusCodes.OK).type('application/json').send(JSON.stringify(result));
+			});
+		} else if(pck) {
+			this.getPackageDocumentedInfo(pck).then((result) => {
+				res.status(StatusCodes.OK).type('application/json').send(JSON.stringify(result));
+			});
+		}
+
+	};
+
+	private async getRootsDocumentedInfo(): Promise<{ [key: string]: DocumentedClassInfo }> {
+		const roots = Sources.getRepoNames();
+		const result: { [key: string]: DocumentedClassInfo } = {};
+
+		for(let root of roots) {
+			let sumDocumentable = 0;
+			let sumDocumented = 0;
+			const classes = Sources.findClassesByRoot(root);
+
+			if(!classes)
+				continue;
+
+			for(let cls of classes) {
+				const res = await this.getClassDocumentedInfo(cls);
+				sumDocumentable += res.documentable;
+				sumDocumented += res.documented;
+			}
+
+			result[root] = { documented: sumDocumented, documentable: sumDocumentable };
+		}
+
+		return result;
+	}
+
+	private async getPackageDocumentedInfo(pck: PackageEntity): Promise<DocumentedPackageInfo> {
+    const results: Map<string, DocumentedPackageInfo> = new Map();
+
+		let sumDocumentable = 0;
+		let sumDocumented = 0;
+
+		const promises = pck.children.map(async (child) => {
+			const found = Sources.findClass(child);
+			if(found) {
+				const result = await this.getClassDocumentedInfo(found);
+				results.set(child, result);
+				sumDocumentable += result.documentable;
+				sumDocumented += result.documented;
+			}
+		});
+
+		await Promise.all(promises);
+
+    return { documented: sumDocumented, documentable: sumDocumentable, children: Object.fromEntries(results) };
+	}
+
+	private async getClassDocumentedInfo(cls: ClassMapEntity): Promise<DocumentedClassInfo> {
+		const excludeList = ['callParent', 'newInstance'];
+		let documentable = cls.documentableProperties;
+		let documented = 0;
+		
+		const commentsResult = await CommentsManager.getCommentsByClass(cls.root, cls.name);
+
+		if(!commentsResult.success)
+			return { documentable: 0, documented: 0 };
+
+		commentsResult.comments?.forEach((comment) => {
+			if(comment.text && comment.text.length > 0 && !excludeList.includes(comment.propertyName))
+				documented++;
+		});
+
+		return { documentable, documented }
+	}
+
+	protected method = HttpMethod.POST;
+	protected order = 10;
+	protected route = '/docinfo';
+}
+
+export default GetDocumentedInfo;

+ 3 - 1
src/sources/Analyzer.ts

@@ -143,6 +143,8 @@ class Analyzer {
 
 			cls.properties = properties;
 			cls.dynamicProperties = dynamicProperties;
+			cls.documentableProperties = [...cls.statics, ...cls.properties, ...cls.dynamicProperties].filter(prop => !prop.inherited || prop.overridden).length + 1; // +1 for __self__
+
 			classMap[className] = cls;
 
 			progress && progress.next();
@@ -193,7 +195,7 @@ class Analyzer {
 	private static getProperties(z8Cls: any): Z8ClassProperties[] {
 		const prototype = z8Cls.prototype;
 		const superclass = z8Cls.superclass;
-		const excludeList = ['self', '$class', '$className', '$shortClassName', '$previous', '$owner', 'superclass'];
+		const excludeList = ['self', '$class', '$className', '$shortClassName', '$previous', '$owner', 'superclass', 'callParent', 'newInstance'];
 		const notInherited = ['callParent', 'newInstance'];
 
 		const superclassProperties = Object.keys(superclass).filter((key) => {

+ 22 - 4
src/sources/Sources.ts

@@ -30,6 +30,7 @@ type ClassMapEntity = {
 	statics: Z8ClassProperties[],
 	properties: Z8ClassProperties[],
 	dynamicProperties: Z8ClassProperties[],
+	documentableProperties: number,
 	isPackage: false
 };
 
@@ -40,6 +41,7 @@ type ClassMap = {
 type PackageEntity = {
 	name: string,
 	root: string,
+	children: string[],
 	isPackage: true
 };
 
@@ -113,6 +115,22 @@ class Sources {
 		return fs.statSync(Sources.classMapPath).birthtime;
 	}
 
+	public static findClassesByRoot(root: string): ClassMapEntity[] | null {
+		const keys = Object.keys(Sources.ClassMap).filter((key) => {
+			return Sources.ClassMap[key].root === root;
+		});
+
+		if(keys.length === 0)
+			return null;
+
+		const classes = [];
+
+		for(let key of keys)
+			classes.push(Sources.ClassMap[key]);
+
+		return classes;
+	}
+
 	public static findClass(className: string): ClassMapEntity | null {
 		if(Sources.ClassMap[className])
 			return Sources.ClassMap[className];
@@ -132,7 +150,7 @@ class Sources {
 			return key.startsWith(packageName) && key !== packageName && Sources.ClassMap[key].root === root;
 		});
 		if(keys.length > 0)
-			return { name: packageName, root: root, isPackage: true };
+			return { name: packageName, root: root, children: keys, isPackage: true };
 		return null;
 	}
 
@@ -227,7 +245,7 @@ class Sources {
 						dynamic: true,
 						overridden: false
 					};
-				}));
+				})).filter(item => !['callParent', 'newInstance'].includes(item.key));
 
 				const dynamicConfigProperties = Sources.cleanDuplicates(Array.from(classContent.matchAll(dynamicConfigPropertiesPattern), (match) => {
 					return {
@@ -239,9 +257,9 @@ class Sources {
 						dynamic: true,
 						overridden: false
 					};
-				}).filter(item1 => !dynamicProperties.some(item2 => item2.key === item1.key)));
+				}).filter(item1 => !dynamicProperties.some(item2 => item2.key === item1.key) && !['callParent', 'newInstance'].includes(item1.key)));
 
-				Sources.ClassMap[define] = { name: define, root: pathPrefix, filePath: `${pathPrefix}/${filePath}`, children: [], extends: null, parentsBranch: [], mixins: [], mixedIn: [], properties: dynamicProperties, dynamicProperties: dynamicConfigProperties, statics: [], isPackage: false };
+				Sources.ClassMap[define] = { name: define, root: pathPrefix, filePath: `${pathPrefix}/${filePath}`, children: [], extends: null, parentsBranch: [], mixins: [], mixedIn: [], properties: dynamicProperties, dynamicProperties: dynamicConfigProperties, documentableProperties: 0, statics: [], isPackage: false };
 			}
 			fs.writeFileSync(Sources.dzAppPath, content + '\n', { flag: 'a+', encoding: 'utf8' });
 			progress && progress.next();

+ 3 - 0
src/views/class.pug

@@ -33,6 +33,9 @@ html
 								div.class-name
 									div.package-icon
 									span= Class.name
+									span= ' (Documented: '
+									span.class-documented-percentage= '0%'
+									span= ')'
 								div.display-mode-buttons
 									div.display-mode-button.mode-tabs(title='Display mode: Tabs', data-display-mode='mode-tabs')
 									div.display-mode-button.mode-list(title='Display mode: List', data-display-mode='mode-list')

+ 4 - 0
src/views/statistics.pug

@@ -27,4 +27,8 @@ html
 						div.statistics-icon
 						span= StatisticsUser ? 'Statistics for user: ' + StatisticsUser : 'Statistics'
 					div.content
+						if(!StatisticsUser)
+							div.documented-info-content
+							hr
+						div.statistics-content
 			div.context-menu.hidden

+ 32 - 6
static/page/class/script.js

@@ -110,7 +110,7 @@ class ClassPage {
 	static __self__ = '__self__';
 
 	start() {
-		if(!Class.root) {
+		if(!Class[ClassPage.ClassProperties.Root]) {
 			return this;
 		}
 
@@ -141,7 +141,7 @@ class ClassPage {
 			this.contentElements[ClassPage.TabNames.Contribution] = DOM.get('.content-tab#contribution');
 		}
 
-		this.documented = Object.keys(Comments).filter((key) => { return Comments[key].text.length > 0; }).length;
+		this.documented = 0; //Object.keys(Comments).filter((key) => { return Comments[key].text.length > 0; }).length;
 		this.documentable = 1; // __self__ property is always included
 		this.inheritedCommentsQuery = {};
 		this.inheritedCommentsFields = {};
@@ -166,6 +166,8 @@ class ClassPage {
 
 		rightContainer.addClass(mode);
 
+		this.loadDocumentedInfo();
+
 		this.renderContent();
 
 		this.markContentInEditor();
@@ -255,18 +257,16 @@ class ClassPage {
 			this.renderMixedIn();
 			this.renderProperties();
 			this.renderMethods();
-			this.renderDocumentedPercentage();
 			this.loadInheritedComments();
 		}
 
+		this.renderDocumentedPercentage();
 		this.renderInfo();
 		if(isEditor)
 			this.renderContribution();
 	}
 
 	renderDocumentedPercentage() {
-		if(Class.isPackage)
-			return;
 		this.documentedPercentage.setInnerHTML(`${Math.round(this.documented/this.documentable * 10000) / 100}%`);
 	}
 
@@ -492,7 +492,6 @@ class ClassPage {
 
 	renderInfo() {
 		const infoContainer = this.contentElements[ClassPage.TabNames.Info];
-		//infoContainer.append(DOM.create({ tag: DOM.Tags.Div, innerHTML: 'abc test' }));
 		const propertiesList = DOM.create({ tag: DOM.Tags.Div, cls: 'properties-list' }, infoContainer);
 		this.createPropertyItem(ClassPage.__self__, { value: '' }, ClassPage.PropertyType.ClassComment, propertiesList);
 	}
@@ -1199,6 +1198,33 @@ class ClassPage {
 		contextMenu.show(pos);
 	}
 	/* <<< Context menu */
+
+	loadDocumentedInfo() {
+		let requestBody;
+
+		if(Class.isPackage) {
+			requestBody = new URLSearchParams({
+				'root': Class[ClassPage.ClassProperties.Root],
+				'packageName': Class[ClassPage.ClassProperties.Name]
+			});
+		} else {
+			requestBody = new URLSearchParams({
+				'className': Class[ClassPage.ClassProperties.Name]
+			});
+		}
+
+		fetch(`/docinfo`, {
+			method: 'POST',
+			headers: {
+				'Content-Type': 'application/x-www-form-urlencoded'
+			},
+			body: requestBody
+		}).then(res => res.json()).then((res) => {
+			this.documented = res['documented'];
+			this.documentable = res['documentable'];
+			this.renderDocumentedPercentage();
+		});
+	}
 };
 
 window_.on(DOM.Events.Load, (e) => {

+ 23 - 1
static/page/statistics/script.js

@@ -11,9 +11,31 @@ class StatisticsPage {
 		this.statistics.load();
 	}
 
+	loadDocumentedInfo() {
+		fetch('/docinfo', {
+			method: 'POST'
+		}).then(res => res.json()).then((res) => {
+			const container = DOM.get('#statistics>.content>.documented-info-content');
+			const table = DOM.create({ tag: DOM.Tags.Table }, container);
+			const headers = DOM.create({ tag: DOM.Tags.Tr }, table);
+			const tbody = DOM.create({ tag: DOM.Tags.Tbody }, table);
+
+			DOM.create({ tag: DOM.Tags.Th, innerHTML: 'Repository' }, headers);
+			DOM.create({ tag: DOM.Tags.Th, innerHTML: 'Status' }, headers);
+
+			for(let root of Object.keys(res)) {
+				const row = DOM.create({ tag: DOM.Tags.Tr }, tbody);
+				DOM.create({ tag: DOM.Tags.Td, innerHTML: root }, row);
+				DOM.create({ tag: DOM.Tags.Td, innerHTML: `${Math.round(res[root]['documented']/res[root]['documentable'] * 10000) / 100}%` }, row);
+			}
+		});
+	}
+
 	start() {
 		this.openSocket();
-		this.statistics = Statistics.init(DOM.get('#statistics>.content'), this.userMode ? Statistics.Modes.USER : Statistics.Modes.ALL);
+		this.statistics = Statistics.init(DOM.get('#statistics>.content>.statistics-content'), this.userMode ? Statistics.Modes.USER : Statistics.Modes.ALL);
+		if(!this.userMode)
+			this.loadDocumentedInfo();
 		return this;
 	}
 }

+ 14 - 0
static/page/statistics/style.css

@@ -14,4 +14,18 @@
 	width: 30px;
 	height: 30px;
 	background-size: cover;
+}
+
+#statistics > .content > .documented-info-content > table {
+	width: 50%;
+	color: #d1d1d1;
+	border-collapse: collapse;
+	text-align: center;
+}
+
+#statistics > .content > .documented-info-content > table,
+#statistics > .content > .documented-info-content > table tr,
+#statistics > .content > .documented-info-content > table td,
+#statistics > .content > .documented-info-content > table th {
+	border: 1px solid #d1d1d1;
 }