• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2025 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16import fs from 'fs';
17import path from 'path';
18import { promisify } from 'util';
19
20// Define file type extensions
21const FILE_TYPES: { [key: string]: string[] } = {
22  'C/C++': ['.c', '.h', '.cpp', '.hpp'],
23  JavaScript: ['.js'],
24  TypeScript: ['.ts'],
25  JSON: ['.json', '.json5'],
26  XML: ['.xml'],
27  ArkTS: ['.ets'],
28  'ArkTS Test': ['.test.ets']
29};
30
31// Comment regex patterns by file type
32const COMMENT_REGEX1: { [key: string]: RegExp } = {
33  'C/C++': /\/\/.*/g,
34  JavaScript: /\/\/.*/g,
35  TypeScript: /\/\/.*/g,
36  ArkTS: /\/\/.*/g,
37  'ArkTS Test': /\/\/.*/g,
38  XML: /<!--.*?-->/g
39};
40
41const COMMENT_REGEX2: { [key: string]: RegExp } = {
42  'C/C++': /\/\*.*?\*\//gs,
43  JavaScript: /\/\*.*?\*\//gs,
44  TypeScript: /\/\*.*?\*\//gs,
45  ArkTS: /\/\*.*?\*\//gs,
46  'ArkTS Test': /\/\*.*?\*\//gs
47};
48
49/**
50 * Remove comments from file content
51 */
52function removeComments(content: string, fileType: string): string {
53  if (COMMENT_REGEX1[fileType]) {
54    content = content.replace(COMMENT_REGEX1[fileType], '');
55  }
56  if (COMMENT_REGEX2[fileType]) {
57    content = content.replace(COMMENT_REGEX2[fileType], '');
58  }
59  return content;
60}
61
62/**
63 * Count valid lines of code (excluding comments and empty lines)
64 */
65async function countLines(filePath: string, fileType: string): Promise<number> {
66  try {
67    const content = await promisify(fs.readFile)(filePath, 'utf-8');
68    const cleanedContent = removeComments(content, fileType);
69    const lines = cleanedContent.split('\n');
70
71    // Filter out empty lines
72    const validLines = lines.filter((line) => {
73      return line.trim();
74    });
75    return validLines.length;
76  } catch (error) {
77    console.error(`Error reading ${filePath}: ${error}`);
78    return 0;
79  }
80}
81
82/**
83 * Merge multiple result objects
84 */
85function mergeAllResults(results: { [key: string]: { fileCount: number; lineCount: number } }[]): {
86  [key: string]: { fileCount: number; lineCount: number };
87} {
88  const combined: { [key: string]: { fileCount: number; lineCount: number } } = {};
89
90  for (const result of results) {
91    for (const [type, counts] of Object.entries(result)) {
92      if (!combined[type]) {
93        combined[type] = { fileCount: 0, lineCount: 0 };
94      }
95      combined[type].fileCount += counts.fileCount;
96      combined[type].lineCount += counts.lineCount;
97    }
98  }
99
100  return combined;
101}
102
103/**
104 * Process directory entries recursively
105 */
106async function walkDir(dir: string): Promise<{ [key: string]: { fileCount: number; lineCount: number } }> {
107  try {
108    const entries = await promisify(fs.readdir)(dir, { withFileTypes: true });
109    const fileResults = await processFiles(dir, entries);
110    const dirResults = await processDirs(dir, entries);
111    return mergeAllResults([fileResults, dirResults]);
112  } catch (error) {
113    console.error(`Error reading ${dir}: ${error}`);
114    return {};
115  }
116}
117
118/**
119 * Process files in a directory
120 */
121async function processFiles(
122  dir: string,
123  entries: fs.Dirent[]
124): Promise<{ [key: string]: { fileCount: number; lineCount: number } }> {
125  const fileResults = await Promise.all(
126    entries.
127      filter((entry) => {
128        return entry.isFile();
129      }).
130      map(async(entry) => {
131        return processFileEntry(dir, entry);
132      })
133  );
134  return mergeAllResults(fileResults);
135}
136
137/**
138 * Process a single file entry
139 */
140async function processFileEntry(
141  dir: string,
142  entry: fs.Dirent
143): Promise<{ [key: string]: { fileCount: number; lineCount: number } }> {
144  const fullPath = path.join(dir, entry.name);
145
146  for (const fileType in FILE_TYPES) {
147    const extensions = FILE_TYPES[fileType];
148    const ext = path.extname(entry.name);
149
150    if (extensions.includes(ext)) {
151      const lines = await countLines(fullPath, fileType);
152      return {
153        [fileType]: {
154          fileCount: 1,
155          lineCount: lines
156        }
157      };
158    }
159  }
160
161  return {};
162}
163
164/**
165 * Process subdirectories recursively
166 */
167async function processDirs(
168  dir: string,
169  entries: fs.Dirent[]
170): Promise<{ [key: string]: { fileCount: number; lineCount: number } }> {
171  const dirEntries = entries.filter((entry) => {
172    return entry.isDirectory();
173  });
174  const dirResults = await Promise.all(
175    dirEntries.map((entry) => {
176      return walkDir(path.join(dir, entry.name));
177    })
178  );
179  return mergeAllResults(dirResults);
180}
181
182/**
183 * Analyze directory and count files/lines by type
184 */
185async function analyzeDirectory(
186  directory: string
187): Promise<{ [key: string]: { fileCount: number; lineCount: number } }> {
188  // Initialize results
189  const results: { [key: string]: { fileCount: number; lineCount: number } } = {};
190  for (const fileType in FILE_TYPES) {
191    results[fileType] = { fileCount: 0, lineCount: 0 };
192  }
193
194  const finalResults = await walkDir(directory);
195  Object.assign(results, finalResults);
196  return results;
197}
198
199/**
200 * Main export function
201 */
202export async function countFiles(
203  directory: string
204): Promise<{ [key: string]: { fileCount: number; lineCount: number } }> {
205  try {
206    const stats = await promisify(fs.stat)(directory);
207    if (stats.isDirectory()) {
208      return await analyzeDirectory(directory);
209    }
210    console.error('The directory is invalid!');
211    return {};
212  } catch (error) {
213    console.error('Read directory failed', error);
214    return {};
215  }
216}
217