• 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 * 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