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 * as fs from 'fs'; 17import * as path from 'path'; 18import type { NapiFileStatisticInfo } from './NapiFileStatisticInfo'; 19 20const EXTENSIONS = ['.c', '.cpp', '.cc', '.cxx', '.h', '.hpp', '.hh', '.hxx']; 21 22const SINGLE_LINE_COMMENT_REGEX = /\/\/.*/g; 23const MULTI_LINE_COMMENT_REGEX = /\/\*[\s\S]*?\*\//g; 24 25const DEFAULT_STATISTICS: NapiFileStatisticInfo = { 26 totalFiles: 0, 27 totalLines: 0, 28 napiFiles: 0, 29 napiLines: 0, 30 napiFileLines: 0 31}; 32 33function removeComments(content: string): string { 34 return content.replace(MULTI_LINE_COMMENT_REGEX, '').replace(SINGLE_LINE_COMMENT_REGEX, ''); 35} 36 37async function countLines(filePath: string): Promise<number> { 38 try { 39 const content = await fs.promises.readFile(filePath, 'utf-8'); 40 const contentWithoutComments = removeComments(content); 41 const validLines = contentWithoutComments.split('\n').filter((line) => { 42 return line.trim(); 43 }); 44 return validLines.length; 45 } catch (e) { 46 console.error(`Error reading ${filePath}: ${e}`); 47 return 0; 48 } 49} 50 51async function countNapiLines(filePath: string): Promise<number> { 52 try { 53 const content = await fs.promises.readFile(filePath, 'utf-8'); 54 const lines = content.split('\n'); 55 const napiLines = new Set<string>(); 56 57 for (const line of lines) { 58 if (line.toLowerCase().includes('napi')) { 59 napiLines.add(line); 60 } 61 } 62 63 return napiLines.size; 64 } catch (e) { 65 console.error(`Error reading ${filePath}: ${e}`); 66 return 0; 67 } 68} 69 70async function analyzeDirectoryAsync(directory: string): Promise<NapiFileStatisticInfo> { 71 const dirQueue: string[] = [directory]; 72 const allResults: NapiFileStatisticInfo[] = []; 73 74 while (dirQueue.length > 0) { 75 const currentDir = dirQueue.shift()!; 76 const entries = await fs.promises.readdir(currentDir, { withFileTypes: true }); 77 const fileResults = await Promise.all( 78 entries. 79 map((entry) => { 80 const fullPath = path.join(currentDir, entry.name); 81 if (entry.isDirectory()) { 82 dirQueue.push(fullPath); 83 return null; 84 } else if (isTargetFile(entry.name)) { 85 return processFile(fullPath); 86 } 87 return null; 88 }). 89 filter(Boolean) as Promise<NapiFileStatisticInfo>[] 90 ); 91 allResults.push(...fileResults); 92 } 93 94 return allResults.reduce( 95 (acc, cur) => { 96 acc.totalFiles += cur.totalFiles; 97 acc.totalLines += cur.totalLines; 98 if (cur.napiFiles > 0) { 99 acc.napiFiles += cur.napiFiles; 100 acc.napiLines += cur.napiLines; 101 acc.napiFileLines += cur.napiFileLines; 102 } 103 return acc; 104 }, 105 { ...DEFAULT_STATISTICS } 106 ); 107} 108 109async function processFile(filePath: string): Promise<NapiFileStatisticInfo> { 110 const result: NapiFileStatisticInfo = { 111 totalFiles: 1, 112 totalLines: 0, 113 napiFiles: 0, 114 napiLines: 0, 115 napiFileLines: 0 116 }; 117 118 try { 119 const [lines, napiCount] = await Promise.all([countLines(filePath), countNapiLines(filePath)]); 120 121 result.totalLines = lines; 122 if (napiCount > 0) { 123 result.napiFiles = 1; 124 result.napiLines = napiCount; 125 result.napiFileLines = lines; 126 } 127 } catch (e) { 128 console.error(`Error processing ${filePath}: ${e}`); 129 } 130 return result; 131} 132 133function isTargetFile(filename: string): boolean { 134 return EXTENSIONS.some((ext) => { 135 return filename.endsWith(ext); 136 }); 137} 138 139export async function countNapiFiles(directory: string): Promise<NapiFileStatisticInfo> { 140 try { 141 const stat = await fs.promises.stat(directory); 142 if (!stat.isDirectory()) { 143 console.log('The provided path is not a directory!'); 144 return DEFAULT_STATISTICS; 145 } 146 return await analyzeDirectoryAsync(directory); 147 } catch (e) { 148 console.error(`Error accessing directory ${directory}: ${e}`); 149 return DEFAULT_STATISTICS; 150 } 151} 152