* Copyright (c) 2025 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as fs from 'fs';
import * as path from 'path';
import { runWithIOLimit, mapWithLimit } from './IoLimiter';
import { Logger } from '../../Logger';
import type { NapiFileStatisticInfo } from './NapiFileStatisticInfo';
const EXTENSIONS = ['.c', '.cpp', '.cc', '.cxx', '.h', '.hpp', '.hh', '.hxx'];
const SINGLE_LINE_COMMENT_REGEX = /\/\/.*/g;
const MULTI_LINE_COMMENT_REGEX = /\/\*[\s\S]*?\*\//g;
const DEFAULT_STATISTICS: NapiFileStatisticInfo = {
totalFiles: 0,
totalLines: 0,
napiFiles: 0,
napiLines: 0,
napiFileLines: 0
};
function removeComments(content: string): string {
return content.replace(MULTI_LINE_COMMENT_REGEX, '').replace(SINGLE_LINE_COMMENT_REGEX, '');
}
function countLinesFromContent(content: string): number {
const contentWithoutComments = removeComments(content);
const validLines = contentWithoutComments.split('\n').filter((line) => {
return line.trim();
});
return validLines.length;
}
function countNapiLinesFromContent(content: string): number {
const lines = content.split('\n');
const napiLines = new Set<string>();
for (const line of lines) {
if (line.toLowerCase().includes('napi')) {
napiLines.add(line);
}
}
return napiLines.size;
}
async function analyzeDirectoryAsync(directory: string): Promise<NapiFileStatisticInfo> {
const dirQueue: string[] = [directory];
const allResults: NapiFileStatisticInfo[] = [];
const MAX_CONCURRENT_FILES = 32;
while (dirQueue.length > 0) {
const currentDir = dirQueue.shift()!;
const entries = await runWithIOLimit(() => {
return fs.promises.readdir(currentDir, { withFileTypes: true });
});
const filesToProcess: string[] = [];
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
dirQueue.push(fullPath);
} else if (isTargetFile(entry.name)) {
filesToProcess.push(fullPath);
}
}
const fileResults = await mapWithLimit(filesToProcess, MAX_CONCURRENT_FILES, async (filePath) => {
return processFile(filePath);
});
allResults.push(...fileResults);
}
return allResults.reduce(
(acc, cur) => {
acc.totalFiles += cur.totalFiles;
acc.totalLines += cur.totalLines;
if (cur.napiFiles > 0) {
acc.napiFiles += cur.napiFiles;
acc.napiLines += cur.napiLines;
acc.napiFileLines += cur.napiFileLines;
}
return acc;
},
{ ...DEFAULT_STATISTICS }
);
}
async function processFile(filePath: string): Promise<NapiFileStatisticInfo> {
const result: NapiFileStatisticInfo = {
totalFiles: 1,
totalLines: 0,
napiFiles: 0,
napiLines: 0,
napiFileLines: 0
};
try {
const content = await runWithIOLimit(() => {
return fs.promises.readFile(filePath, 'utf-8');
});
const [lines, napiCount] = await Promise.all([
Promise.resolve(countLinesFromContent(content)),
Promise.resolve(countNapiLinesFromContent(content))
]);
result.totalLines = lines;
if (napiCount > 0) {
result.napiFiles = 1;
result.napiLines = napiCount;
result.napiFileLines = lines;
}
} catch (e) {
Logger.error(`Error processing ${filePath}: ${e}`);
}
return result;
}
function isTargetFile(filename: string): boolean {
return EXTENSIONS.some((ext) => {
return filename.endsWith(ext);
});
}
export async function countNapiFiles(directory: string): Promise<NapiFileStatisticInfo> {
try {
const stat = await runWithIOLimit(() => {
return fs.promises.stat(directory);
});
if (!stat.isDirectory()) {
Logger.error('The provided path is not a directory!');
return DEFAULT_STATISTICS;
}
return await analyzeDirectoryAsync(directory);
} catch (e) {
Logger.error(`Error accessing directory ${directory}: ${e}`);
return DEFAULT_STATISTICS;
}
}