linguist-js 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const commander_1 = require("commander");
10
10
  const index_1 = __importDefault(require("./index"));
11
+ const norm_path_1 = require("./helpers/norm-path");
11
12
  const colouredMsg = ([r, g, b], msg) => `\u001B[${38};2;${r};${g};${b}m${msg}${'\u001b[0m'}`;
12
13
  const hexToRgb = (hex) => [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];
13
14
  commander_1.program
@@ -23,11 +24,13 @@ commander_1.program
23
24
  .option('-F|--listFiles [bool]', 'Whether to list every matching file under the language results', false)
24
25
  .option('-q|--quick [bool]', 'Skip complex language analysis (alias for -{A|I|H|S}=false)', false)
25
26
  .option('-o|--offline [bool]', 'Use packaged data files instead of fetching latest from GitHub', false)
27
+ .option('-L|--calculateLines [bool]', 'Calculate lines of code totals', true)
26
28
  .option('-V|--keepVendored [bool]', 'Prevent skipping over vendored/generated files', false)
27
29
  .option('-B|--keepBinary [bool]', 'Prevent skipping over binary files', false)
28
30
  .option('-r|--relativePaths [bool]', 'Convert absolute file paths to relative', false)
29
31
  .option('-A|--checkAttributes [bool]', 'Force the checking of gitattributes files', true)
30
32
  .option('-I|--checkIgnored [bool]', 'Force the checking of gitignore files', true)
33
+ .option('-D|--checkDetected [bool]', 'Force files marked with linguist-detectable to always appear in output', true)
31
34
  .option('-H|--checkHeuristics [bool]', 'Apply heuristics to ambiguous languages', true)
32
35
  .option('-S|--checkShebang [bool]', 'Check shebang lines for explicit classification', true)
33
36
  .option('-M|--checkModeline [bool]', 'Check modelines for explicit classification', true)
@@ -78,21 +81,22 @@ if (args.analyze)
78
81
  }
79
82
  }
80
83
  // List parsed results
81
- for (const [lang, { bytes, color }] of sortedEntries) {
84
+ for (const [lang, { bytes, lines, color }] of sortedEntries) {
82
85
  const percent = (bytes) => bytes / (totalBytes || 1) * 100;
83
86
  const fmtd = {
84
87
  index: (++count).toString().padStart(2, ' '),
85
88
  lang: lang.padEnd(24, ' '),
86
89
  percent: percent(bytes).toFixed(2).padStart(5, ' '),
87
90
  bytes: bytes.toLocaleString().padStart(10, ' '),
91
+ loc: lines.code.toLocaleString().padStart(10, ' '),
88
92
  icon: colouredMsg(hexToRgb(color !== null && color !== void 0 ? color : '#ededed'), '\u2588'),
89
93
  };
90
- console.log(` ${fmtd.index}. ${fmtd.icon} ${fmtd.lang} ${fmtd.percent}% ${fmtd.bytes} B`);
94
+ console.log(` ${fmtd.index}. ${fmtd.icon} ${fmtd.lang} ${fmtd.percent}% ${fmtd.bytes} B ${fmtd.loc} LOC`);
91
95
  // If using `listFiles` option, list all files tagged as this language
92
96
  if (args.listFiles) {
93
97
  console.log(); // padding
94
98
  for (const file of filesPerLanguage[lang]) {
95
- let relFile = path_1.default.relative(path_1.default.resolve('.'), file).replace(/\\/g, '/');
99
+ let relFile = (0, norm_path_1.normPath)(path_1.default.relative(path_1.default.resolve('.'), file));
96
100
  if (!relFile.startsWith('../'))
97
101
  relFile = './' + relFile;
98
102
  const bytes = (await fs_1.default.promises.stat(file)).size;
@@ -0,0 +1,2 @@
1
+ export declare const normPath: (...inputPaths: string[]) => string;
2
+ export declare const normAbsPath: (...inputPaths: string[]) => string;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.normAbsPath = exports.normPath = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const normPath = function normalisedPath(...inputPaths) {
9
+ return path_1.default.join(...inputPaths).replace(/\\/g, '/');
10
+ };
11
+ exports.normPath = normPath;
12
+ const normAbsPath = function normalisedAbsolutePath(...inputPaths) {
13
+ return path_1.default.resolve(...inputPaths).replace(/\\/g, '/');
14
+ };
15
+ exports.normAbsPath = normAbsPath;
@@ -3,6 +3,7 @@ export type FlagAttributes = {
3
3
  'vendored': boolean | null;
4
4
  'generated': boolean | null;
5
5
  'documentation': boolean | null;
6
+ 'detectable': boolean | null;
6
7
  'binary': boolean | null;
7
8
  'language': T.LanguageResult;
8
9
  };
@@ -1,9 +1,6 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const path_1 = __importDefault(require("path"));
3
+ const norm_path_1 = require("./norm-path");
7
4
  /**
8
5
  * Parses a gitattributes file.
9
6
  */
@@ -16,7 +13,7 @@ function parseAttributes(content, folderRoot = '.') {
16
13
  continue;
17
14
  const parts = line.split(/\s+/g);
18
15
  const fileGlob = parts[0];
19
- const relFileGlob = path_1.default.join(folderRoot, fileGlob).replace(/\\/g, '/');
16
+ const relFileGlob = (0, norm_path_1.normPath)(folderRoot, fileGlob);
20
17
  const attrParts = parts.slice(1);
21
18
  const isTrue = (str) => !str.startsWith('-') && !str.endsWith('=false');
22
19
  const isFalse = (str) => str.startsWith('-') || str.endsWith('=false');
@@ -24,10 +21,12 @@ function parseAttributes(content, folderRoot = '.') {
24
21
  const falseParts = (str) => attrParts.filter(part => part.includes(str) && isFalse(part));
25
22
  const hasTrueParts = (str) => trueParts(str).length > 0;
26
23
  const hasFalseParts = (str) => falseParts(str).length > 0;
24
+ const boolOrNullVal = (str) => hasTrueParts(str) ? true : hasFalseParts(str) ? false : null;
27
25
  const attrs = {
28
- 'generated': hasTrueParts('linguist-generated') ? true : hasFalseParts('linguist-generated') ? false : null,
29
- 'vendored': hasTrueParts('linguist-vendored') ? true : hasFalseParts('linguist-vendored') ? false : null,
30
- 'documentation': hasTrueParts('linguist-documentation') ? true : hasFalseParts('linguist-documentation') ? false : null,
26
+ 'generated': boolOrNullVal('linguist-generated'),
27
+ 'vendored': boolOrNullVal('linguist-vendored'),
28
+ 'documentation': boolOrNullVal('linguist-documentation'),
29
+ 'detectable': boolOrNullVal('linguist-detectable'),
31
30
  'binary': hasTrueParts('binary') || hasFalseParts('text') ? true : hasFalseParts('binary') || hasTrueParts('text') ? false : null,
32
31
  'language': (_b = (_a = trueParts('linguist-language').at(-1)) === null || _a === void 0 ? void 0 : _a.split('=')[1]) !== null && _b !== void 0 ? _b : null,
33
32
  };
@@ -0,0 +1 @@
1
+ export default function parseGitignore(content: string): string[];
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ function parseGitignore(content) {
4
+ const readableData = content
5
+ // Remove comments unless escaped
6
+ .replace(/(?<!\\)#.+/g, '')
7
+ // Remove whitespace unless escaped
8
+ .replace(/(?:(?<!\\)\s)+$/g, '');
9
+ const arrayData = readableData.split(/\r?\n/).filter(data => data);
10
+ return arrayData;
11
+ }
12
+ exports.default = parseGitignore;
@@ -2,4 +2,4 @@
2
2
  * Read part of a file on disc.
3
3
  * @throws 'EPERM' if the file is not readable.
4
4
  */
5
- export default function readFile(filename: string, onlyFirstLine?: boolean): Promise<string>;
5
+ export default function readFileChunk(filename: string, onlyFirstLine?: boolean): Promise<string>;
@@ -8,7 +8,7 @@ const fs_1 = __importDefault(require("fs"));
8
8
  * Read part of a file on disc.
9
9
  * @throws 'EPERM' if the file is not readable.
10
10
  */
11
- async function readFile(filename, onlyFirstLine = false) {
11
+ async function readFileChunk(filename, onlyFirstLine = false) {
12
12
  const chunkSize = 100;
13
13
  const stream = fs_1.default.createReadStream(filename, { highWaterMark: chunkSize });
14
14
  let content = '';
@@ -20,4 +20,4 @@ async function readFile(filename, onlyFirstLine = false) {
20
20
  }
21
21
  return content;
22
22
  }
23
- exports.default = readFile;
23
+ exports.default = readFileChunk;
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const fs_1 = __importDefault(require("fs"));
7
7
  const path_1 = __importDefault(require("path"));
8
+ const parse_gitignore_1 = __importDefault(require("./parse-gitignore"));
9
+ const norm_path_1 = require("./norm-path");
8
10
  let allFiles;
9
11
  let allFolders;
10
12
  ;
@@ -24,26 +26,38 @@ function walk(data) {
24
26
  // Get list of files and folders inside this folder
25
27
  const files = fs_1.default.readdirSync(folder).map(file => {
26
28
  // Create path relative to root
27
- const base = path_1.default.resolve(folder, file).replace(/\\/g, '/').replace(commonRoot, '.');
29
+ const base = (0, norm_path_1.normAbsPath)(folder, file).replace(commonRoot, '.');
28
30
  // Add trailing slash to mark directories
29
31
  const isDir = fs_1.default.lstatSync(path_1.default.resolve(commonRoot, base)).isDirectory();
30
32
  return isDir ? `${base}/` : base;
31
33
  });
34
+ // Read and apply gitignores
35
+ const gitignoreFilename = (0, norm_path_1.normPath)(folder, '.gitignore');
36
+ if (fs_1.default.existsSync(gitignoreFilename)) {
37
+ const gitignoreContents = fs_1.default.readFileSync(gitignoreFilename, 'utf-8');
38
+ const ignoredPaths = (0, parse_gitignore_1.default)(gitignoreContents);
39
+ ignored.add(ignoredPaths);
40
+ }
41
+ // Add gitattributes if present
42
+ const gitattributesPath = (0, norm_path_1.normPath)(folder, '.gitattributes');
43
+ if (fs_1.default.existsSync(gitattributesPath)) {
44
+ allFiles.add(gitattributesPath);
45
+ }
32
46
  // Loop through files and folders
33
47
  for (const file of files) {
34
48
  // Create absolute path for disc operations
35
- const path = path_1.default.resolve(commonRoot, file).replace(/\\/g, '/');
49
+ const path = (0, norm_path_1.normAbsPath)(commonRoot, file);
36
50
  const localPath = localRoot ? file.replace(`./${localRoot}/`, '') : file.replace('./', '');
37
51
  // Skip if nonexistant
38
52
  const nonExistant = !fs_1.default.existsSync(path);
39
53
  if (nonExistant)
40
54
  continue;
41
- // Skip if marked as ignored
55
+ // Skip if marked in gitignore
42
56
  const isIgnored = ignored.test(localPath).ignored;
43
57
  if (isIgnored)
44
58
  continue;
45
59
  // Add absolute folder path to list
46
- allFolders.add(path_1.default.resolve(folder).replace(/\\/g, '/'));
60
+ allFolders.add((0, norm_path_1.normAbsPath)(folder));
47
61
  // Check if this is a folder or file
48
62
  if (file.endsWith('/')) {
49
63
  // Recurse into subfolders
package/dist/index.js CHANGED
@@ -37,15 +37,18 @@ const load_data_1 = __importStar(require("./helpers/load-data"));
37
37
  const read_file_1 = __importDefault(require("./helpers/read-file"));
38
38
  const parse_gitattributes_1 = __importDefault(require("./helpers/parse-gitattributes"));
39
39
  const convert_pcre_1 = __importDefault(require("./helpers/convert-pcre"));
40
+ const norm_path_1 = require("./helpers/norm-path");
40
41
  async function analyse(rawPaths, opts = {}) {
41
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
42
- var _r, _s;
42
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
43
+ var _u, _v;
43
44
  const useRawContent = opts.fileContent !== undefined;
44
45
  const input = [rawPaths !== null && rawPaths !== void 0 ? rawPaths : []].flat();
45
46
  const manualFileContent = [(_a = opts.fileContent) !== null && _a !== void 0 ? _a : []].flat();
46
47
  // Normalise input option arguments
47
48
  opts = {
49
+ calculateLines: (_b = opts.calculateLines) !== null && _b !== void 0 ? _b : true,
48
50
  checkIgnored: !opts.quick,
51
+ checkDetected: !opts.quick,
49
52
  checkAttributes: !opts.quick,
50
53
  checkHeuristics: !opts.quick,
51
54
  checkShebang: !opts.quick,
@@ -64,17 +67,15 @@ async function analyse(rawPaths, opts = {}) {
64
67
  const extensions = {};
65
68
  const globOverrides = {};
66
69
  const results = {
67
- files: { count: 0, bytes: 0, results: {}, alternatives: {} },
68
- languages: { count: 0, bytes: 0, results: {} },
69
- unknown: { count: 0, bytes: 0, extensions: {}, filenames: {} },
70
+ files: { count: 0, bytes: 0, lines: { total: 0, content: 0, code: 0 }, results: {}, alternatives: {} },
71
+ languages: { count: 0, bytes: 0, lines: { total: 0, content: 0, code: 0 }, results: {} },
72
+ unknown: { count: 0, bytes: 0, lines: { total: 0, content: 0, code: 0 }, extensions: {}, filenames: {} },
70
73
  };
71
74
  // Set a common root path so that vendor paths do not incorrectly match parent folders
72
- const normPath = (file) => file.replace(/\\/g, '/');
73
- const resolvedInput = input.map(path => normPath(path_1.default.resolve(path)));
75
+ const resolvedInput = input.map(path => (0, norm_path_1.normPath)(path_1.default.resolve(path)));
74
76
  const commonRoot = (input.length > 1 ? (0, common_path_prefix_1.default)(resolvedInput) : resolvedInput[0]).replace(/\/?$/, '');
75
- const localRoot = (folder) => folder.replace(commonRoot, '').replace(/^\//, '');
76
- const relPath = (file) => useRawContent ? file : normPath(path_1.default.relative(commonRoot, file));
77
- const unRelPath = (file) => useRawContent ? file : normPath(path_1.default.resolve(commonRoot, file));
77
+ const relPath = (file) => useRawContent ? file : (0, norm_path_1.normPath)(path_1.default.relative(commonRoot, file));
78
+ const unRelPath = (file) => useRawContent ? file : (0, norm_path_1.normPath)(path_1.default.resolve(commonRoot, file));
78
79
  // Other helper functions
79
80
  const fileMatchesGlobs = (file, ...globs) => (0, ignore_1.default)().add(globs).ignores(relPath(file));
80
81
  const filterOutIgnored = (files, ignored) => ignored.filter(files.map(relPath)).map(unRelPath);
@@ -82,47 +83,40 @@ async function analyse(rawPaths, opts = {}) {
82
83
  // Prepare list of ignored files
83
84
  const ignored = (0, ignore_1.default)();
84
85
  ignored.add('.git/');
85
- ignored.add((_b = opts.ignoredFiles) !== null && _b !== void 0 ? _b : []);
86
- const regexIgnores = [];
87
- if (!opts.keepVendored)
88
- regexIgnores.push(...vendorPaths.map(path => RegExp(path, 'i')));
86
+ ignored.add((_c = opts.ignoredFiles) !== null && _c !== void 0 ? _c : []);
87
+ const regexIgnores = opts.keepVendored ? [] : vendorPaths.map(path => RegExp(path, 'i'));
89
88
  // Load file paths and folders
90
89
  let files;
91
- let folders;
92
90
  if (useRawContent) {
93
91
  // Uses raw file content
94
92
  files = input;
95
- folders = [''];
96
93
  }
97
94
  else {
98
95
  // Uses directory on disc
99
96
  const data = (0, walk_tree_1.default)({ init: true, commonRoot, folderRoots: resolvedInput, folders: resolvedInput, ignored });
100
97
  files = data.files;
101
- folders = data.folders;
102
- }
103
- // Load gitignore data and apply ignores rules
104
- if (!useRawContent && opts.checkIgnored) {
105
- const nestedIgnoreFiles = files.filter(file => file.endsWith('.gitignore'));
106
- for (const ignoresFile of nestedIgnoreFiles) {
107
- const relIgnoresFile = relPath(ignoresFile);
108
- const relIgnoresFolder = path_1.default.dirname(relIgnoresFile);
109
- // Parse gitignores
110
- const ignoresDataRaw = await (0, read_file_1.default)(ignoresFile);
111
- const ignoresData = ignoresDataRaw.replace(/#.+|\s+$/gm, '');
112
- const absoluteIgnoresData = ignoresData
113
- // '.file' -> 'root/*/.file'
114
- .replace(/^(?=[^\s\/\\])/gm, localRoot(relIgnoresFolder) + '/*/')
115
- // '/folder' -> 'root/folder'
116
- .replace(/^[\/\\]/gm, localRoot(relIgnoresFolder) + '/');
117
- ignored.add(absoluteIgnoresData);
118
- files = filterOutIgnored(files, ignored);
119
- }
120
98
  }
121
99
  // Fetch and normalise gitattributes data of all subfolders and save to metadata
122
100
  const manualAttributes = {}; // Maps file globs to gitattribute boolean flags
123
101
  const getFlaggedGlobs = (attr, val) => {
124
102
  return Object.entries(manualAttributes).filter(([, attrs]) => attrs[attr] === val).map(([glob,]) => glob);
125
103
  };
104
+ const findAttrsForPath = (filePath) => {
105
+ const resultAttrs = {};
106
+ for (const glob in manualAttributes) {
107
+ if ((0, ignore_1.default)().add(glob).ignores(relPath(filePath))) {
108
+ const matchingAttrs = manualAttributes[glob];
109
+ for (const [attr, val] of Object.entries(matchingAttrs)) {
110
+ if (val !== null)
111
+ resultAttrs[attr] = val;
112
+ }
113
+ }
114
+ }
115
+ if (!JSON.stringify(resultAttrs)) {
116
+ return null;
117
+ }
118
+ return resultAttrs;
119
+ };
126
120
  if (!useRawContent && opts.checkAttributes) {
127
121
  const nestedAttrFiles = files.filter(file => file.endsWith('.gitattributes'));
128
122
  for (const attrFile of nestedAttrFiles) {
@@ -135,6 +129,25 @@ async function analyse(rawPaths, opts = {}) {
135
129
  }
136
130
  }
137
131
  }
132
+ // Remove files that are linguist-ignored via regex by default unless explicitly unignored in gitattributes
133
+ const filesToIgnore = [];
134
+ for (const file of files) {
135
+ const relFile = relPath(file);
136
+ const isRegexIgnored = regexIgnores.some(pattern => pattern.test(relFile));
137
+ if (!isRegexIgnored) {
138
+ // Checking overrides is moot if file is not even marked as ignored by default
139
+ continue;
140
+ }
141
+ const fileAttrs = findAttrsForPath(file);
142
+ if ((fileAttrs === null || fileAttrs === void 0 ? void 0 : fileAttrs.generated) === false || (fileAttrs === null || fileAttrs === void 0 ? void 0 : fileAttrs.vendored) === false) {
143
+ // File is explicitly marked as *not* to be ignored
144
+ // do nothing
145
+ }
146
+ else {
147
+ filesToIgnore.push(file);
148
+ }
149
+ }
150
+ files = files.filter(file => !filesToIgnore.includes(file));
138
151
  // Apply vendor file path matches and filter out vendored files
139
152
  if (!opts.keepVendored) {
140
153
  // Get data of files that have been manually marked with metadata
@@ -165,7 +178,7 @@ async function analyse(rawPaths, opts = {}) {
165
178
  files.push(...unignoredList);
166
179
  }
167
180
  // Ignore specific languages
168
- for (const lang of (_c = opts.ignoredLanguages) !== null && _c !== void 0 ? _c : []) {
181
+ for (const lang of (_d = opts.ignoredLanguages) !== null && _d !== void 0 ? _d : []) {
169
182
  for (const key in langData) {
170
183
  if (lang.toLowerCase() === key.toLowerCase()) {
171
184
  delete langData[key];
@@ -218,7 +231,7 @@ async function analyse(rawPaths, opts = {}) {
218
231
  // Check first line for readability
219
232
  let firstLine;
220
233
  if (useRawContent) {
221
- firstLine = (_e = (_d = manualFileContent[files.indexOf(file)]) === null || _d === void 0 ? void 0 : _d.split('\n')[0]) !== null && _e !== void 0 ? _e : null;
234
+ firstLine = (_f = (_e = manualFileContent[files.indexOf(file)]) === null || _e === void 0 ? void 0 : _e.split('\n')[0]) !== null && _f !== void 0 ? _f : null;
222
235
  }
223
236
  else if (fs_1.default.existsSync(file) && !fs_1.default.lstatSync(file).isDirectory()) {
224
237
  firstLine = await (0, read_file_1.default)(file, true).catch(() => null);
@@ -237,7 +250,7 @@ async function analyse(rawPaths, opts = {}) {
237
250
  const langMatcher = (lang) => `\\b${lang.toLowerCase().replace(/\W/g, '\\$&')}(?![\\w#+*]|-\*-)`;
238
251
  // Check for interpreter match
239
252
  if (opts.checkShebang && hasShebang) {
240
- const matchesInterpretor = (_f = data.interpreters) === null || _f === void 0 ? void 0 : _f.some(interpreter => firstLine.match(`\\b${interpreter}\\b`));
253
+ const matchesInterpretor = (_g = data.interpreters) === null || _g === void 0 ? void 0 : _g.some(interpreter => firstLine.match(`\\b${interpreter}\\b`));
241
254
  if (matchesInterpretor)
242
255
  matches.push(lang);
243
256
  }
@@ -245,7 +258,7 @@ async function analyse(rawPaths, opts = {}) {
245
258
  if (opts.checkModeline && hasModeline) {
246
259
  const modelineText = firstLine.toLowerCase().replace(/^.*-\*-(.+)-\*-.*$/, '$1');
247
260
  const matchesLang = modelineText.match(langMatcher(lang));
248
- const matchesAlias = (_g = data.aliases) === null || _g === void 0 ? void 0 : _g.some(lang => modelineText.match(langMatcher(lang)));
261
+ const matchesAlias = (_h = data.aliases) === null || _h === void 0 ? void 0 : _h.some(lang => modelineText.match(langMatcher(lang)));
249
262
  if (matchesLang || matchesAlias)
250
263
  matches.push(lang);
251
264
  }
@@ -264,7 +277,7 @@ async function analyse(rawPaths, opts = {}) {
264
277
  let skipExts = false;
265
278
  // Check if filename is a match
266
279
  for (const lang in langData) {
267
- const matchesName = (_h = langData[lang].filenames) === null || _h === void 0 ? void 0 : _h.some(name => path_1.default.basename(file.toLowerCase()) === name.toLowerCase());
280
+ const matchesName = (_j = langData[lang].filenames) === null || _j === void 0 ? void 0 : _j.some(name => path_1.default.basename(file.toLowerCase()) === name.toLowerCase());
268
281
  if (matchesName) {
269
282
  addResult(file, lang);
270
283
  skipExts = true;
@@ -274,7 +287,7 @@ async function analyse(rawPaths, opts = {}) {
274
287
  const possibleExts = [];
275
288
  if (!skipExts)
276
289
  for (const lang in langData) {
277
- const extMatches = (_j = langData[lang].extensions) === null || _j === void 0 ? void 0 : _j.filter(ext => file.toLowerCase().endsWith(ext.toLowerCase()));
290
+ const extMatches = (_k = langData[lang].extensions) === null || _k === void 0 ? void 0 : _k.filter(ext => file.toLowerCase().endsWith(ext.toLowerCase()));
278
291
  if (extMatches === null || extMatches === void 0 ? void 0 : extMatches.length) {
279
292
  for (const ext of extMatches)
280
293
  possibleExts.push({ ext, lang });
@@ -320,7 +333,7 @@ async function analyse(rawPaths, opts = {}) {
320
333
  heuristic.language = heuristic.language[0];
321
334
  }
322
335
  // Make sure the results includes this language
323
- const languageGroup = (_k = langData[heuristic.language]) === null || _k === void 0 ? void 0 : _k.group;
336
+ const languageGroup = (_l = langData[heuristic.language]) === null || _l === void 0 ? void 0 : _l.group;
324
337
  const matchesLang = fileAssociations[file].includes(heuristic.language);
325
338
  const matchesParent = languageGroup && fileAssociations[file].includes(languageGroup);
326
339
  if (!matchesLang && !matchesParent)
@@ -365,17 +378,23 @@ async function analyse(rawPaths, opts = {}) {
365
378
  }
366
379
  }
367
380
  // Skip specified categories
368
- if ((_l = opts.categories) === null || _l === void 0 ? void 0 : _l.length) {
381
+ if ((_m = opts.categories) === null || _m === void 0 ? void 0 : _m.length) {
369
382
  const categories = ['data', 'markup', 'programming', 'prose'];
370
383
  const hiddenCategories = categories.filter(cat => !opts.categories.includes(cat));
371
384
  for (const [file, lang] of Object.entries(results.files.results)) {
372
- if (!hiddenCategories.some(cat => { var _a; return lang && ((_a = langData[lang]) === null || _a === void 0 ? void 0 : _a.type) === cat; })) {
385
+ // Skip if language is not hidden
386
+ if (!hiddenCategories.some(cat => { var _a; return lang && ((_a = langData[lang]) === null || _a === void 0 ? void 0 : _a.type) === cat; }))
373
387
  continue;
388
+ // Skip if language is forced as detectable
389
+ if (opts.checkDetected) {
390
+ const detectable = (0, ignore_1.default)().add(getFlaggedGlobs('detectable', true));
391
+ if (detectable.ignores(relPath(file)))
392
+ continue;
374
393
  }
394
+ // Delete result otherwise
375
395
  delete results.files.results[file];
376
- if (lang) {
396
+ if (lang)
377
397
  delete results.languages.results[lang];
378
- }
379
398
  }
380
399
  for (const category of hiddenCategories) {
381
400
  for (const [lang, { type }] of Object.entries(results.languages.results)) {
@@ -389,7 +408,7 @@ async function analyse(rawPaths, opts = {}) {
389
408
  if (!useRawContent && opts.relativePaths) {
390
409
  const newMap = {};
391
410
  for (const [file, lang] of Object.entries(results.files.results)) {
392
- let relPath = path_1.default.relative(process.cwd(), file).replace(/\\/g, '/');
411
+ let relPath = (0, norm_path_1.normPath)(path_1.default.relative(process.cwd(), file));
393
412
  if (!relPath.startsWith('../')) {
394
413
  relPath = './' + relPath;
395
414
  }
@@ -401,26 +420,59 @@ async function analyse(rawPaths, opts = {}) {
401
420
  for (const [file, lang] of Object.entries(results.files.results)) {
402
421
  if (lang && !langData[lang])
403
422
  continue;
404
- const fileSize = (_o = (_m = manualFileContent[files.indexOf(file)]) === null || _m === void 0 ? void 0 : _m.length) !== null && _o !== void 0 ? _o : fs_1.default.statSync(file).size;
423
+ // Calculate file size
424
+ const fileSize = (_p = (_o = manualFileContent[files.indexOf(file)]) === null || _o === void 0 ? void 0 : _o.length) !== null && _p !== void 0 ? _p : fs_1.default.statSync(file).size;
425
+ // Calculate lines of code
426
+ const loc = { total: 0, content: 0, code: 0 };
427
+ if (opts.calculateLines) {
428
+ const fileContent = (_r = ((_q = manualFileContent[files.indexOf(file)]) !== null && _q !== void 0 ? _q : fs_1.default.readFileSync(file).toString())) !== null && _r !== void 0 ? _r : '';
429
+ const allLines = fileContent.split(/\r?\n/gm);
430
+ loc.total = allLines.length;
431
+ loc.content = allLines.filter(line => line.trim().length > 0).length;
432
+ const codeLines = fileContent
433
+ .replace(/^\s*(\/\/|# |;|--).+/gm, '')
434
+ .replace(/\/\*.+\*\/|<!--.+-->/sg, '');
435
+ loc.code = codeLines.split(/\r?\n/gm).filter(line => line.trim().length > 0).length;
436
+ }
437
+ // Apply to files totals
405
438
  results.files.bytes += fileSize;
406
- // If no language found, add extension in other section
407
- if (!lang) {
439
+ results.files.lines.total += loc.total;
440
+ results.files.lines.content += loc.content;
441
+ results.files.lines.code += loc.code;
442
+ // Add results to 'languages' section if language match found, or 'unknown' section otherwise
443
+ if (lang) {
444
+ const { type } = langData[lang];
445
+ // set default if unset
446
+ (_s = (_u = results.languages.results)[lang]) !== null && _s !== void 0 ? _s : (_u[lang] = { type, bytes: 0, lines: { total: 0, content: 0, code: 0 }, color: langData[lang].color });
447
+ // apply results to 'languages' section
448
+ if (opts.childLanguages) {
449
+ results.languages.results[lang].parent = langData[lang].group;
450
+ }
451
+ results.languages.results[lang].bytes += fileSize;
452
+ results.languages.bytes += fileSize;
453
+ results.languages.results[lang].lines.total += loc.total;
454
+ results.languages.results[lang].lines.content += loc.content;
455
+ results.languages.results[lang].lines.code += loc.code;
456
+ results.languages.lines.total += loc.total;
457
+ results.languages.lines.content += loc.content;
458
+ results.languages.lines.code += loc.code;
459
+ }
460
+ else {
408
461
  const ext = path_1.default.extname(file);
409
- const unknownType = ext === '' ? 'filenames' : 'extensions';
410
- const name = ext === '' ? path_1.default.basename(file) : ext;
411
- (_p = (_r = results.unknown[unknownType])[name]) !== null && _p !== void 0 ? _p : (_r[name] = 0);
462
+ const unknownType = ext ? 'extensions' : 'filenames';
463
+ const name = ext || path_1.default.basename(file);
464
+ // apply results to 'unknown' section
465
+ (_t = (_v = results.unknown[unknownType])[name]) !== null && _t !== void 0 ? _t : (_v[name] = 0);
412
466
  results.unknown[unknownType][name] += fileSize;
413
467
  results.unknown.bytes += fileSize;
414
- continue;
468
+ results.unknown.lines.total += loc.total;
469
+ results.unknown.lines.content += loc.content;
470
+ results.unknown.lines.code += loc.code;
415
471
  }
416
- // Add language and bytes data to corresponding section
417
- const { type } = langData[lang];
418
- (_q = (_s = results.languages.results)[lang]) !== null && _q !== void 0 ? _q : (_s[lang] = { type, bytes: 0, color: langData[lang].color });
419
- if (opts.childLanguages) {
420
- results.languages.results[lang].parent = langData[lang].group;
421
- }
422
- results.languages.results[lang].bytes += fileSize;
423
- results.languages.bytes += fileSize;
472
+ }
473
+ // Set lines output to NaN when line calculation is disabled
474
+ if (opts.calculateLines === false) {
475
+ results.files.lines = { total: NaN, content: NaN, code: NaN };
424
476
  }
425
477
  // Set counts
426
478
  results.files.count = Object.keys(results.files.results).length;
package/dist/types.d.ts CHANGED
@@ -19,7 +19,9 @@ export interface Options {
19
19
  childLanguages?: boolean;
20
20
  quick?: boolean;
21
21
  offline?: boolean;
22
+ calculateLines?: boolean;
22
23
  checkIgnored?: boolean;
24
+ checkDetected?: boolean;
23
25
  checkAttributes?: boolean;
24
26
  checkHeuristics?: boolean;
25
27
  checkShebang?: boolean;
@@ -29,6 +31,11 @@ export interface Results {
29
31
  files: {
30
32
  count: Integer;
31
33
  bytes: Bytes;
34
+ lines: {
35
+ total: Integer;
36
+ content: Integer;
37
+ code: Integer;
38
+ };
32
39
  /** Note: Results use slashes as delimiters even on Windows. */
33
40
  results: Record<FilePath, LanguageResult>;
34
41
  alternatives: Record<FilePath, LanguageResult[]>;
@@ -36,8 +43,18 @@ export interface Results {
36
43
  languages: {
37
44
  count: Integer;
38
45
  bytes: Bytes;
46
+ lines: {
47
+ total: Integer;
48
+ content: Integer;
49
+ code: Integer;
50
+ };
39
51
  results: Record<Language, {
40
52
  bytes: Bytes;
53
+ lines: {
54
+ total: Integer;
55
+ content: Integer;
56
+ code: Integer;
57
+ };
41
58
  type: Category;
42
59
  parent?: Language;
43
60
  color?: `#${string}`;
@@ -46,6 +63,11 @@ export interface Results {
46
63
  unknown: {
47
64
  count: Integer;
48
65
  bytes: Bytes;
66
+ lines: {
67
+ total: Integer;
68
+ content: Integer;
69
+ code: Integer;
70
+ };
49
71
  extensions: Record<string, Bytes>;
50
72
  filenames: Record<string, Bytes>;
51
73
  };
package/ext/generated.rb CHANGED
@@ -8,18 +8,24 @@
8
8
  - (Gopkg|glide)\.lock
9
9
  - poetry\.lock
10
10
  - pdm\.lock
11
+ - uv\.lock
11
12
  - (^|\/)(\w+\.)?esy.lock$
13
+ - deno\.lock
12
14
  - npm-shrinkwrap\.json
13
15
  - package-lock\.json
16
+ - pnpm-lock\.yaml
14
17
  - (^|\/)\.pnp\..*$
15
18
  - Godeps\/
16
19
  - composer\.lock
17
20
  - .\.zep\.(?:c|h|php)$
18
21
  - Cargo\.lock
22
+ - Cargo\.toml\.orig
19
23
  - (^|\/)flake\.lock$
24
+ - (^|\/)MODULE\.bazel\.lock$
20
25
  - Pipfile\.lock
21
26
  - (?:^|\/)\.terraform\.lock\.hcl$
22
27
  - ppport\.h$
23
28
  - __generated__\/
24
29
  - _tlb\.pas$
25
30
  - (?:^|\/)htmlcov\/
31
+ - (?:^|.*\/)\.sqlx\/query-.+\.json$