linguist-js 2.5.5 → 2.5.6
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.d.ts +1 -1
- package/dist/cli.js +107 -107
- package/dist/helpers/convert-pcre.js +38 -38
- package/dist/helpers/load-data.js +31 -30
- package/dist/helpers/read-file.js +23 -23
- package/dist/helpers/walk-tree.js +64 -64
- package/dist/index.d.ts +4 -4
- package/dist/index.js +375 -375
- package/dist/schema.d.ts +37 -37
- package/dist/schema.js +2 -2
- package/dist/types.d.ts +47 -47
- package/dist/types.js +2 -2
- package/ext/heuristics.yml +804 -791
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,375 +1,375 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
const fs_1 = __importDefault(require("fs"));
|
|
6
|
-
const path_1 = __importDefault(require("path"));
|
|
7
|
-
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
8
|
-
const ignore_1 = __importDefault(require("ignore"));
|
|
9
|
-
const common_path_prefix_1 = __importDefault(require("common-path-prefix"));
|
|
10
|
-
const binary_extensions_1 = __importDefault(require("binary-extensions"));
|
|
11
|
-
const isbinaryfile_1 = require("isbinaryfile");
|
|
12
|
-
const walk_tree_1 = __importDefault(require("./helpers/walk-tree"));
|
|
13
|
-
const load_data_1 = __importDefault(require("./helpers/load-data"));
|
|
14
|
-
const read_file_1 = __importDefault(require("./helpers/read-file"));
|
|
15
|
-
const convert_pcre_1 = __importDefault(require("./helpers/convert-pcre"));
|
|
16
|
-
async function analyse(input, opts = {}) {
|
|
17
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
|
|
18
|
-
var _u, _v, _w;
|
|
19
|
-
const useRawContent = opts.fileContent !== undefined;
|
|
20
|
-
input = [input !== null && input !== void 0 ? input : []].flat();
|
|
21
|
-
opts.fileContent = [(_a = opts.fileContent) !== null && _a !== void 0 ? _a : []].flat();
|
|
22
|
-
// Load data from github-linguist web repo
|
|
23
|
-
const langData = await (0, load_data_1.default)('languages.yml', opts.offline).then(js_yaml_1.default.load);
|
|
24
|
-
const vendorData = await (0, load_data_1.default)('vendor.yml', opts.offline).then(js_yaml_1.default.load);
|
|
25
|
-
const docData = await (0, load_data_1.default)('documentation.yml', opts.offline).then(js_yaml_1.default.load);
|
|
26
|
-
const heuristicsData = await (0, load_data_1.default)('heuristics.yml', opts.offline).then(js_yaml_1.default.load);
|
|
27
|
-
const generatedData = await (0, load_data_1.default)('generated.rb', opts.offline).then(text => { var _a; return (_a = text.match(/(?<=name\.match\(\/).+?(?=(?<!\\)\/)/gm)) !== null && _a !== void 0 ? _a : []; });
|
|
28
|
-
const vendorPaths = [...vendorData, ...docData, ...generatedData];
|
|
29
|
-
// Setup main variables
|
|
30
|
-
const fileAssociations = {};
|
|
31
|
-
const extensions = {};
|
|
32
|
-
const overrides = {};
|
|
33
|
-
const results = {
|
|
34
|
-
files: { count: 0, bytes: 0, results: {} },
|
|
35
|
-
languages: { count: 0, bytes: 0, results: {} },
|
|
36
|
-
unknown: { count: 0, bytes: 0, extensions: {}, filenames: {} },
|
|
37
|
-
};
|
|
38
|
-
// Prepare list of ignored files
|
|
39
|
-
const gitignores = (0, ignore_1.default)();
|
|
40
|
-
const regexIgnores = [];
|
|
41
|
-
gitignores.add('/.git');
|
|
42
|
-
if (!opts.keepVendored)
|
|
43
|
-
regexIgnores.push(...vendorPaths.map(path => RegExp(path, 'i')));
|
|
44
|
-
if (opts.ignoredFiles)
|
|
45
|
-
gitignores.add(opts.ignoredFiles);
|
|
46
|
-
// Set a common root path so that vendor paths do not incorrectly match parent folders
|
|
47
|
-
const resolvedInput = input.map(path => path_1.default.resolve(path).replace(/\\/g, '/'));
|
|
48
|
-
const commonRoot = (input.length > 1 ? (0, common_path_prefix_1.default)(resolvedInput) : resolvedInput[0]).replace(/\/?$/, '');
|
|
49
|
-
const relPath = (file) => path_1.default.relative(commonRoot, file).replace(/\\/g, '/');
|
|
50
|
-
const unRelPath = (file) => path_1.default.resolve(commonRoot, file).replace(/\\/g, '/');
|
|
51
|
-
// Load file paths and folders
|
|
52
|
-
let files, folders;
|
|
53
|
-
if (useRawContent) {
|
|
54
|
-
// Uses raw file content
|
|
55
|
-
files = input;
|
|
56
|
-
folders = [''];
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
// Uses directory on disc
|
|
60
|
-
const data = (0, walk_tree_1.default)(true, commonRoot, input, gitignores, regexIgnores);
|
|
61
|
-
files = data.files;
|
|
62
|
-
folders = data.folders;
|
|
63
|
-
}
|
|
64
|
-
// Apply aliases
|
|
65
|
-
opts = {
|
|
66
|
-
checkIgnored: !opts.quick,
|
|
67
|
-
checkAttributes: !opts.quick,
|
|
68
|
-
checkHeuristics: !opts.quick,
|
|
69
|
-
checkShebang: !opts.quick,
|
|
70
|
-
checkModeline: !opts.quick,
|
|
71
|
-
...opts
|
|
72
|
-
};
|
|
73
|
-
// Ignore specific languages
|
|
74
|
-
for (const lang of (_b = opts.ignoredLanguages) !== null && _b !== void 0 ? _b : []) {
|
|
75
|
-
for (const key in langData) {
|
|
76
|
-
if (lang.toLowerCase() === key.toLowerCase()) {
|
|
77
|
-
delete langData[key];
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
// Load gitignores and gitattributes
|
|
83
|
-
const customBinary = (0, ignore_1.default)();
|
|
84
|
-
const customText = (0, ignore_1.default)();
|
|
85
|
-
if (!useRawContent && opts.checkAttributes) {
|
|
86
|
-
for (const folder of folders) {
|
|
87
|
-
// Skip if folder is marked in gitattributes
|
|
88
|
-
if (relPath(folder) && gitignores.ignores(relPath(folder))) {
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
// Parse gitignores
|
|
92
|
-
const ignoresFile = path_1.default.join(folder, '.gitignore');
|
|
93
|
-
if (opts.checkIgnored && fs_1.default.existsSync(ignoresFile)) {
|
|
94
|
-
const ignoresData = await (0, read_file_1.default)(ignoresFile);
|
|
95
|
-
gitignores.add(ignoresData);
|
|
96
|
-
}
|
|
97
|
-
// Parse gitattributes
|
|
98
|
-
const attributesFile = path_1.default.join(folder, '.gitattributes');
|
|
99
|
-
if (opts.checkAttributes && fs_1.default.existsSync(attributesFile)) {
|
|
100
|
-
const attributesData = await (0, read_file_1.default)(attributesFile);
|
|
101
|
-
// Explicit text/binary associations
|
|
102
|
-
const contentTypeMatches = attributesData.matchAll(/^(\S+).*?(-?binary|-?text)(?!=auto)/gm);
|
|
103
|
-
for (const [_line, path, type] of contentTypeMatches) {
|
|
104
|
-
if (['text', '-binary'].includes(type)) {
|
|
105
|
-
customText.add(path);
|
|
106
|
-
}
|
|
107
|
-
if (['-text', 'binary'].includes(type)) {
|
|
108
|
-
customBinary.add(path);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
// Custom vendor options
|
|
112
|
-
const vendorMatches = attributesData.matchAll(/^(\S+).*[^-]linguist-(vendored|generated|documentation)(?!=false)/gm);
|
|
113
|
-
for (const [_line, path] of vendorMatches) {
|
|
114
|
-
gitignores.add(path);
|
|
115
|
-
}
|
|
116
|
-
// Custom file associations
|
|
117
|
-
const customLangMatches = attributesData.matchAll(/^(\S+).*[^-]linguist-language=(\S+)/gm);
|
|
118
|
-
for (let [_line, path, forcedLang] of customLangMatches) {
|
|
119
|
-
// If specified language is an alias, associate it with its full name
|
|
120
|
-
if (!langData[forcedLang]) {
|
|
121
|
-
const overrideLang = Object.entries(langData).find(entry => { var _a; return (_a = entry[1].aliases) === null || _a === void 0 ? void 0 : _a.includes(forcedLang.toLowerCase()); });
|
|
122
|
-
if (overrideLang) {
|
|
123
|
-
forcedLang = overrideLang[0];
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
const fullPath = path_1.default.join(relPath(folder), path);
|
|
127
|
-
overrides[fullPath] = forcedLang;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
// Check vendored files
|
|
133
|
-
if (!opts.keepVendored) {
|
|
134
|
-
// Filter out any files that match a vendor file path
|
|
135
|
-
if (useRawContent) {
|
|
136
|
-
files = gitignores.filter(files);
|
|
137
|
-
files = files.filter(file => !regexIgnores.find(match => match.test(file)));
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
files = gitignores.filter(files.map(relPath)).map(unRelPath);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
// Load all files and parse languages
|
|
144
|
-
const addResult = (file, result) => {
|
|
145
|
-
if (!fileAssociations[file]) {
|
|
146
|
-
fileAssociations[file] = [];
|
|
147
|
-
extensions[file] = '';
|
|
148
|
-
}
|
|
149
|
-
const parent = !opts.childLanguages && result && langData[result].group || false;
|
|
150
|
-
fileAssociations[file].push(parent || result);
|
|
151
|
-
extensions[file] = path_1.default.extname(file).toLowerCase();
|
|
152
|
-
};
|
|
153
|
-
const overridesArray = Object.entries(overrides);
|
|
154
|
-
// List all languages that could be associated with a given file
|
|
155
|
-
const definiteness = {};
|
|
156
|
-
const fromShebang = {};
|
|
157
|
-
for (const file of files) {
|
|
158
|
-
let firstLine;
|
|
159
|
-
if (useRawContent) {
|
|
160
|
-
firstLine = (_e = (_d = (_c = opts.fileContent) === null || _c === void 0 ? void 0 : _c[files.indexOf(file)]) === null || _d === void 0 ? void 0 : _d.split('\n')[0]) !== null && _e !== void 0 ? _e : null;
|
|
161
|
-
}
|
|
162
|
-
else if (fs_1.default.existsSync(file) && !fs_1.default.lstatSync(file).isDirectory()) {
|
|
163
|
-
firstLine = await (0, read_file_1.default)(file, true).catch(() => null);
|
|
164
|
-
}
|
|
165
|
-
else
|
|
166
|
-
continue;
|
|
167
|
-
// Skip if file is unreadable
|
|
168
|
-
if (firstLine === null)
|
|
169
|
-
continue;
|
|
170
|
-
// Check first line for explicit classification
|
|
171
|
-
const hasShebang = opts.checkShebang && /^#!/.test(firstLine);
|
|
172
|
-
const hasModeline = opts.checkModeline && /-\*-|(syntax|filetype|ft)\s*=/.test(firstLine);
|
|
173
|
-
if (!opts.quick && (hasShebang || hasModeline)) {
|
|
174
|
-
const matches = [];
|
|
175
|
-
for (const [lang, data] of Object.entries(langData)) {
|
|
176
|
-
const langMatcher = (lang) => `\\b${lang.toLowerCase().replace(/\W/g, '\\$&')}(?![\\w#+*]|-\*-)`;
|
|
177
|
-
// Check for interpreter match
|
|
178
|
-
if (opts.checkShebang && hasShebang) {
|
|
179
|
-
const matchesInterpretor = (_f = data.interpreters) === null || _f === void 0 ? void 0 : _f.some(interpreter => firstLine.match(`\\b${interpreter}\\b`));
|
|
180
|
-
if (matchesInterpretor)
|
|
181
|
-
matches.push(lang);
|
|
182
|
-
}
|
|
183
|
-
// Check modeline declaration
|
|
184
|
-
if (opts.checkModeline && hasModeline) {
|
|
185
|
-
const modelineText = firstLine.toLowerCase().replace(/^.*-\*-(.+)-\*-.*$/, '$1');
|
|
186
|
-
const matchesLang = modelineText.match(langMatcher(lang));
|
|
187
|
-
const matchesAlias = (_g = data.aliases) === null || _g === void 0 ? void 0 : _g.some(lang => modelineText.match(langMatcher(lang)));
|
|
188
|
-
if (matchesLang || matchesAlias)
|
|
189
|
-
matches.push(lang);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
// Add identified language(s)
|
|
193
|
-
if (matches.length) {
|
|
194
|
-
for (const match of matches)
|
|
195
|
-
addResult(file, match);
|
|
196
|
-
if (matches.length === 1)
|
|
197
|
-
definiteness[file] = true;
|
|
198
|
-
fromShebang[file] = true;
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
// Check override for manual language classification
|
|
203
|
-
if (!useRawContent && !opts.quick && opts.checkAttributes) {
|
|
204
|
-
const isOverridden = (path) => (0, ignore_1.default)().add(path).ignores(relPath(file));
|
|
205
|
-
const match = overridesArray.find(item => isOverridden(item[0]));
|
|
206
|
-
if (match) {
|
|
207
|
-
const forcedLang = match[1];
|
|
208
|
-
addResult(file, forcedLang);
|
|
209
|
-
definiteness[file] = true;
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
// Search each language
|
|
214
|
-
let skipExts = false;
|
|
215
|
-
// Check if filename is a match
|
|
216
|
-
for (const lang in langData) {
|
|
217
|
-
const matchesName = (_h = langData[lang].filenames) === null || _h === void 0 ? void 0 : _h.some(name => path_1.default.basename(file.toLowerCase()) === name.toLowerCase());
|
|
218
|
-
if (matchesName) {
|
|
219
|
-
addResult(file, lang);
|
|
220
|
-
skipExts = true;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
// Check if extension is a match
|
|
224
|
-
const possibleExts = [];
|
|
225
|
-
if (!skipExts)
|
|
226
|
-
for (const lang in langData) {
|
|
227
|
-
const extMatches = (_j = langData[lang].extensions) === null || _j === void 0 ? void 0 : _j.filter(ext => file.toLowerCase().endsWith(ext.toLowerCase()));
|
|
228
|
-
if (extMatches === null || extMatches === void 0 ? void 0 : extMatches.length) {
|
|
229
|
-
for (const ext of extMatches)
|
|
230
|
-
possibleExts.push({ ext, lang });
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
// Apply more specific extension if available
|
|
234
|
-
const isComplexExt = (ext) => /\..+\./.test(ext);
|
|
235
|
-
const hasComplexExt = possibleExts.some(data => isComplexExt(data.ext));
|
|
236
|
-
for (const { ext, lang } of possibleExts) {
|
|
237
|
-
if (hasComplexExt && !isComplexExt(ext))
|
|
238
|
-
continue;
|
|
239
|
-
if (!hasComplexExt && isComplexExt(ext))
|
|
240
|
-
continue;
|
|
241
|
-
addResult(file, lang);
|
|
242
|
-
}
|
|
243
|
-
// Fallback to null if no language matches
|
|
244
|
-
if (!fileAssociations[file]) {
|
|
245
|
-
addResult(file, null);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
// Narrow down file associations to the best fit
|
|
249
|
-
for (const file in fileAssociations) {
|
|
250
|
-
// Skip if file has explicit association
|
|
251
|
-
if (definiteness[file]) {
|
|
252
|
-
results.files.results[file] = fileAssociations[file][0];
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
// Skip binary files
|
|
256
|
-
if (!useRawContent && !opts.keepBinary) {
|
|
257
|
-
const isCustomText = customText.ignores(relPath(file));
|
|
258
|
-
const isCustomBinary = customBinary.ignores(relPath(file));
|
|
259
|
-
const isBinaryExt = binary_extensions_1.default.some(ext => file.endsWith('.' + ext));
|
|
260
|
-
if (!isCustomText && (isCustomBinary || isBinaryExt || await (0, isbinaryfile_1.isBinaryFile)(file))) {
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
// Parse heuristics if applicable
|
|
265
|
-
if (opts.checkHeuristics)
|
|
266
|
-
for (const heuristics of heuristicsData.disambiguations) {
|
|
267
|
-
// Make sure the extension matches the current file
|
|
268
|
-
if (!fromShebang[file] && !heuristics.extensions.includes(extensions[file]))
|
|
269
|
-
continue;
|
|
270
|
-
// Load heuristic rules
|
|
271
|
-
for (const heuristic of heuristics.rules) {
|
|
272
|
-
// Make sure the language is not an array
|
|
273
|
-
if (Array.isArray(heuristic.language)) {
|
|
274
|
-
heuristic.language = heuristic.language[0];
|
|
275
|
-
}
|
|
276
|
-
// Make sure the results includes this language
|
|
277
|
-
const languageGroup = (_k = langData[heuristic.language]) === null || _k === void 0 ? void 0 : _k.group;
|
|
278
|
-
const matchesLang = fileAssociations[file].includes(heuristic.language);
|
|
279
|
-
const matchesParent = languageGroup && fileAssociations[file].includes(languageGroup);
|
|
280
|
-
if (!matchesLang && !matchesParent)
|
|
281
|
-
continue;
|
|
282
|
-
// Normalise heuristic data
|
|
283
|
-
const patterns = [];
|
|
284
|
-
const normalise = (contents) => patterns.push(...[contents].flat());
|
|
285
|
-
if (heuristic.pattern)
|
|
286
|
-
normalise(heuristic.pattern);
|
|
287
|
-
if (heuristic.named_pattern)
|
|
288
|
-
normalise(heuristicsData.named_patterns[heuristic.named_pattern]);
|
|
289
|
-
if (heuristic.and) {
|
|
290
|
-
for (const data of heuristic.and) {
|
|
291
|
-
if (data.pattern)
|
|
292
|
-
normalise(data.pattern);
|
|
293
|
-
if (data.named_pattern)
|
|
294
|
-
normalise(heuristicsData.named_patterns[data.named_pattern]);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
// Check file contents and apply heuristic patterns
|
|
298
|
-
const fileContent = ((_l = opts.fileContent) === null || _l === void 0 ? void 0 : _l.length) ? opts.fileContent[files.indexOf(file)] : await (0, read_file_1.default)(file).catch(() => null);
|
|
299
|
-
if (fileContent === null)
|
|
300
|
-
continue;
|
|
301
|
-
if (!patterns.length || patterns.some(pattern => (0, convert_pcre_1.default)(pattern).test(fileContent))) {
|
|
302
|
-
results.files.results[file] = heuristic.language;
|
|
303
|
-
break;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
// If no heuristics, assign a language
|
|
308
|
-
(_m = (_u = results.files.results)[file]) !== null && _m !== void 0 ? _m : (_u[file] = fileAssociations[file][0]);
|
|
309
|
-
}
|
|
310
|
-
// Skip specified categories
|
|
311
|
-
if ((_o = opts.categories) === null || _o === void 0 ? void 0 : _o.length) {
|
|
312
|
-
const categories = ['data', 'markup', 'programming', 'prose'];
|
|
313
|
-
const hiddenCategories = categories.filter(cat => !opts.categories.includes(cat));
|
|
314
|
-
for (const [file, lang] of Object.entries(results.files.results)) {
|
|
315
|
-
if (!hiddenCategories.some(cat => { var _a; return lang && ((_a = langData[lang]) === null || _a === void 0 ? void 0 : _a.type) === cat; })) {
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
delete results.files.results[file];
|
|
319
|
-
if (lang) {
|
|
320
|
-
delete results.languages.results[lang];
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
for (const category of hiddenCategories) {
|
|
324
|
-
for (const [lang, { type }] of Object.entries(results.languages.results)) {
|
|
325
|
-
if (type === category) {
|
|
326
|
-
delete results.languages.results[lang];
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
// Convert paths to relative
|
|
332
|
-
if (!useRawContent && opts.relativePaths) {
|
|
333
|
-
const newMap = {};
|
|
334
|
-
for (const [file, lang] of Object.entries(results.files.results)) {
|
|
335
|
-
let relPath = path_1.default.relative(process.cwd(), file).replace(/\\/g, '/');
|
|
336
|
-
if (!relPath.startsWith('../')) {
|
|
337
|
-
relPath = './' + relPath;
|
|
338
|
-
}
|
|
339
|
-
newMap[relPath] = lang;
|
|
340
|
-
}
|
|
341
|
-
results.files.results = newMap;
|
|
342
|
-
}
|
|
343
|
-
// Load language bytes size
|
|
344
|
-
for (const [file, lang] of Object.entries(results.files.results)) {
|
|
345
|
-
if (lang && !langData[lang])
|
|
346
|
-
continue;
|
|
347
|
-
const fileSize = (_r = (_q = (_p = opts.fileContent) === null || _p === void 0 ? void 0 : _p[files.indexOf(file)]) === null || _q === void 0 ? void 0 : _q.length) !== null && _r !== void 0 ? _r : fs_1.default.statSync(file).size;
|
|
348
|
-
results.files.bytes += fileSize;
|
|
349
|
-
// If no language found, add extension in other section
|
|
350
|
-
if (!lang) {
|
|
351
|
-
const ext = path_1.default.extname(file);
|
|
352
|
-
const unknownType = ext === '' ? 'filenames' : 'extensions';
|
|
353
|
-
const name = ext === '' ? path_1.default.basename(file) : ext;
|
|
354
|
-
(_s = (_v = results.unknown[unknownType])[name]) !== null && _s !== void 0 ? _s : (_v[name] = 0);
|
|
355
|
-
results.unknown[unknownType][name] += fileSize;
|
|
356
|
-
results.unknown.bytes += fileSize;
|
|
357
|
-
continue;
|
|
358
|
-
}
|
|
359
|
-
// Add language and bytes data to corresponding section
|
|
360
|
-
const { type } = langData[lang];
|
|
361
|
-
(_t = (_w = results.languages.results)[lang]) !== null && _t !== void 0 ? _t : (_w[lang] = { type, bytes: 0, color: langData[lang].color });
|
|
362
|
-
if (opts.childLanguages) {
|
|
363
|
-
results.languages.results[lang].parent = langData[lang].group;
|
|
364
|
-
}
|
|
365
|
-
results.languages.results[lang].bytes += fileSize;
|
|
366
|
-
results.languages.bytes += fileSize;
|
|
367
|
-
}
|
|
368
|
-
// Set counts
|
|
369
|
-
results.files.count = Object.keys(results.files.results).length;
|
|
370
|
-
results.languages.count = Object.keys(results.languages.results).length;
|
|
371
|
-
results.unknown.count = Object.keys({ ...results.unknown.extensions, ...results.unknown.filenames }).length;
|
|
372
|
-
// Return
|
|
373
|
-
return results;
|
|
374
|
-
}
|
|
375
|
-
module.exports = analyse;
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
const fs_1 = __importDefault(require("fs"));
|
|
6
|
+
const path_1 = __importDefault(require("path"));
|
|
7
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
8
|
+
const ignore_1 = __importDefault(require("ignore"));
|
|
9
|
+
const common_path_prefix_1 = __importDefault(require("common-path-prefix"));
|
|
10
|
+
const binary_extensions_1 = __importDefault(require("binary-extensions"));
|
|
11
|
+
const isbinaryfile_1 = require("isbinaryfile");
|
|
12
|
+
const walk_tree_1 = __importDefault(require("./helpers/walk-tree"));
|
|
13
|
+
const load_data_1 = __importDefault(require("./helpers/load-data"));
|
|
14
|
+
const read_file_1 = __importDefault(require("./helpers/read-file"));
|
|
15
|
+
const convert_pcre_1 = __importDefault(require("./helpers/convert-pcre"));
|
|
16
|
+
async function analyse(input, opts = {}) {
|
|
17
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
|
|
18
|
+
var _u, _v, _w;
|
|
19
|
+
const useRawContent = opts.fileContent !== undefined;
|
|
20
|
+
input = [input !== null && input !== void 0 ? input : []].flat();
|
|
21
|
+
opts.fileContent = [(_a = opts.fileContent) !== null && _a !== void 0 ? _a : []].flat();
|
|
22
|
+
// Load data from github-linguist web repo
|
|
23
|
+
const langData = await (0, load_data_1.default)('languages.yml', opts.offline).then(js_yaml_1.default.load);
|
|
24
|
+
const vendorData = await (0, load_data_1.default)('vendor.yml', opts.offline).then(js_yaml_1.default.load);
|
|
25
|
+
const docData = await (0, load_data_1.default)('documentation.yml', opts.offline).then(js_yaml_1.default.load);
|
|
26
|
+
const heuristicsData = await (0, load_data_1.default)('heuristics.yml', opts.offline).then(js_yaml_1.default.load);
|
|
27
|
+
const generatedData = await (0, load_data_1.default)('generated.rb', opts.offline).then(text => { var _a; return (_a = text.match(/(?<=name\.match\(\/).+?(?=(?<!\\)\/)/gm)) !== null && _a !== void 0 ? _a : []; });
|
|
28
|
+
const vendorPaths = [...vendorData, ...docData, ...generatedData];
|
|
29
|
+
// Setup main variables
|
|
30
|
+
const fileAssociations = {};
|
|
31
|
+
const extensions = {};
|
|
32
|
+
const overrides = {};
|
|
33
|
+
const results = {
|
|
34
|
+
files: { count: 0, bytes: 0, results: {} },
|
|
35
|
+
languages: { count: 0, bytes: 0, results: {} },
|
|
36
|
+
unknown: { count: 0, bytes: 0, extensions: {}, filenames: {} },
|
|
37
|
+
};
|
|
38
|
+
// Prepare list of ignored files
|
|
39
|
+
const gitignores = (0, ignore_1.default)();
|
|
40
|
+
const regexIgnores = [];
|
|
41
|
+
gitignores.add('/.git');
|
|
42
|
+
if (!opts.keepVendored)
|
|
43
|
+
regexIgnores.push(...vendorPaths.map(path => RegExp(path, 'i')));
|
|
44
|
+
if (opts.ignoredFiles)
|
|
45
|
+
gitignores.add(opts.ignoredFiles);
|
|
46
|
+
// Set a common root path so that vendor paths do not incorrectly match parent folders
|
|
47
|
+
const resolvedInput = input.map(path => path_1.default.resolve(path).replace(/\\/g, '/'));
|
|
48
|
+
const commonRoot = (input.length > 1 ? (0, common_path_prefix_1.default)(resolvedInput) : resolvedInput[0]).replace(/\/?$/, '');
|
|
49
|
+
const relPath = (file) => path_1.default.relative(commonRoot, file).replace(/\\/g, '/');
|
|
50
|
+
const unRelPath = (file) => path_1.default.resolve(commonRoot, file).replace(/\\/g, '/');
|
|
51
|
+
// Load file paths and folders
|
|
52
|
+
let files, folders;
|
|
53
|
+
if (useRawContent) {
|
|
54
|
+
// Uses raw file content
|
|
55
|
+
files = input;
|
|
56
|
+
folders = [''];
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// Uses directory on disc
|
|
60
|
+
const data = (0, walk_tree_1.default)(true, commonRoot, input, gitignores, regexIgnores);
|
|
61
|
+
files = data.files;
|
|
62
|
+
folders = data.folders;
|
|
63
|
+
}
|
|
64
|
+
// Apply aliases
|
|
65
|
+
opts = {
|
|
66
|
+
checkIgnored: !opts.quick,
|
|
67
|
+
checkAttributes: !opts.quick,
|
|
68
|
+
checkHeuristics: !opts.quick,
|
|
69
|
+
checkShebang: !opts.quick,
|
|
70
|
+
checkModeline: !opts.quick,
|
|
71
|
+
...opts
|
|
72
|
+
};
|
|
73
|
+
// Ignore specific languages
|
|
74
|
+
for (const lang of (_b = opts.ignoredLanguages) !== null && _b !== void 0 ? _b : []) {
|
|
75
|
+
for (const key in langData) {
|
|
76
|
+
if (lang.toLowerCase() === key.toLowerCase()) {
|
|
77
|
+
delete langData[key];
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Load gitignores and gitattributes
|
|
83
|
+
const customBinary = (0, ignore_1.default)();
|
|
84
|
+
const customText = (0, ignore_1.default)();
|
|
85
|
+
if (!useRawContent && opts.checkAttributes) {
|
|
86
|
+
for (const folder of folders) {
|
|
87
|
+
// Skip if folder is marked in gitattributes
|
|
88
|
+
if (relPath(folder) && gitignores.ignores(relPath(folder))) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// Parse gitignores
|
|
92
|
+
const ignoresFile = path_1.default.join(folder, '.gitignore');
|
|
93
|
+
if (opts.checkIgnored && fs_1.default.existsSync(ignoresFile)) {
|
|
94
|
+
const ignoresData = await (0, read_file_1.default)(ignoresFile);
|
|
95
|
+
gitignores.add(ignoresData);
|
|
96
|
+
}
|
|
97
|
+
// Parse gitattributes
|
|
98
|
+
const attributesFile = path_1.default.join(folder, '.gitattributes');
|
|
99
|
+
if (opts.checkAttributes && fs_1.default.existsSync(attributesFile)) {
|
|
100
|
+
const attributesData = await (0, read_file_1.default)(attributesFile);
|
|
101
|
+
// Explicit text/binary associations
|
|
102
|
+
const contentTypeMatches = attributesData.matchAll(/^(\S+).*?(-?binary|-?text)(?!=auto)/gm);
|
|
103
|
+
for (const [_line, path, type] of contentTypeMatches) {
|
|
104
|
+
if (['text', '-binary'].includes(type)) {
|
|
105
|
+
customText.add(path);
|
|
106
|
+
}
|
|
107
|
+
if (['-text', 'binary'].includes(type)) {
|
|
108
|
+
customBinary.add(path);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Custom vendor options
|
|
112
|
+
const vendorMatches = attributesData.matchAll(/^(\S+).*[^-]linguist-(vendored|generated|documentation)(?!=false)/gm);
|
|
113
|
+
for (const [_line, path] of vendorMatches) {
|
|
114
|
+
gitignores.add(path);
|
|
115
|
+
}
|
|
116
|
+
// Custom file associations
|
|
117
|
+
const customLangMatches = attributesData.matchAll(/^(\S+).*[^-]linguist-language=(\S+)/gm);
|
|
118
|
+
for (let [_line, path, forcedLang] of customLangMatches) {
|
|
119
|
+
// If specified language is an alias, associate it with its full name
|
|
120
|
+
if (!langData[forcedLang]) {
|
|
121
|
+
const overrideLang = Object.entries(langData).find(entry => { var _a; return (_a = entry[1].aliases) === null || _a === void 0 ? void 0 : _a.includes(forcedLang.toLowerCase()); });
|
|
122
|
+
if (overrideLang) {
|
|
123
|
+
forcedLang = overrideLang[0];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const fullPath = path_1.default.join(relPath(folder), path);
|
|
127
|
+
overrides[fullPath] = forcedLang;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Check vendored files
|
|
133
|
+
if (!opts.keepVendored) {
|
|
134
|
+
// Filter out any files that match a vendor file path
|
|
135
|
+
if (useRawContent) {
|
|
136
|
+
files = gitignores.filter(files);
|
|
137
|
+
files = files.filter(file => !regexIgnores.find(match => match.test(file)));
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
files = gitignores.filter(files.map(relPath)).map(unRelPath);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Load all files and parse languages
|
|
144
|
+
const addResult = (file, result) => {
|
|
145
|
+
if (!fileAssociations[file]) {
|
|
146
|
+
fileAssociations[file] = [];
|
|
147
|
+
extensions[file] = '';
|
|
148
|
+
}
|
|
149
|
+
const parent = !opts.childLanguages && result && langData[result].group || false;
|
|
150
|
+
fileAssociations[file].push(parent || result);
|
|
151
|
+
extensions[file] = path_1.default.extname(file).toLowerCase();
|
|
152
|
+
};
|
|
153
|
+
const overridesArray = Object.entries(overrides);
|
|
154
|
+
// List all languages that could be associated with a given file
|
|
155
|
+
const definiteness = {};
|
|
156
|
+
const fromShebang = {};
|
|
157
|
+
for (const file of files) {
|
|
158
|
+
let firstLine;
|
|
159
|
+
if (useRawContent) {
|
|
160
|
+
firstLine = (_e = (_d = (_c = opts.fileContent) === null || _c === void 0 ? void 0 : _c[files.indexOf(file)]) === null || _d === void 0 ? void 0 : _d.split('\n')[0]) !== null && _e !== void 0 ? _e : null;
|
|
161
|
+
}
|
|
162
|
+
else if (fs_1.default.existsSync(file) && !fs_1.default.lstatSync(file).isDirectory()) {
|
|
163
|
+
firstLine = await (0, read_file_1.default)(file, true).catch(() => null);
|
|
164
|
+
}
|
|
165
|
+
else
|
|
166
|
+
continue;
|
|
167
|
+
// Skip if file is unreadable
|
|
168
|
+
if (firstLine === null)
|
|
169
|
+
continue;
|
|
170
|
+
// Check first line for explicit classification
|
|
171
|
+
const hasShebang = opts.checkShebang && /^#!/.test(firstLine);
|
|
172
|
+
const hasModeline = opts.checkModeline && /-\*-|(syntax|filetype|ft)\s*=/.test(firstLine);
|
|
173
|
+
if (!opts.quick && (hasShebang || hasModeline)) {
|
|
174
|
+
const matches = [];
|
|
175
|
+
for (const [lang, data] of Object.entries(langData)) {
|
|
176
|
+
const langMatcher = (lang) => `\\b${lang.toLowerCase().replace(/\W/g, '\\$&')}(?![\\w#+*]|-\*-)`;
|
|
177
|
+
// Check for interpreter match
|
|
178
|
+
if (opts.checkShebang && hasShebang) {
|
|
179
|
+
const matchesInterpretor = (_f = data.interpreters) === null || _f === void 0 ? void 0 : _f.some(interpreter => firstLine.match(`\\b${interpreter}\\b`));
|
|
180
|
+
if (matchesInterpretor)
|
|
181
|
+
matches.push(lang);
|
|
182
|
+
}
|
|
183
|
+
// Check modeline declaration
|
|
184
|
+
if (opts.checkModeline && hasModeline) {
|
|
185
|
+
const modelineText = firstLine.toLowerCase().replace(/^.*-\*-(.+)-\*-.*$/, '$1');
|
|
186
|
+
const matchesLang = modelineText.match(langMatcher(lang));
|
|
187
|
+
const matchesAlias = (_g = data.aliases) === null || _g === void 0 ? void 0 : _g.some(lang => modelineText.match(langMatcher(lang)));
|
|
188
|
+
if (matchesLang || matchesAlias)
|
|
189
|
+
matches.push(lang);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Add identified language(s)
|
|
193
|
+
if (matches.length) {
|
|
194
|
+
for (const match of matches)
|
|
195
|
+
addResult(file, match);
|
|
196
|
+
if (matches.length === 1)
|
|
197
|
+
definiteness[file] = true;
|
|
198
|
+
fromShebang[file] = true;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Check override for manual language classification
|
|
203
|
+
if (!useRawContent && !opts.quick && opts.checkAttributes) {
|
|
204
|
+
const isOverridden = (path) => (0, ignore_1.default)().add(path).ignores(relPath(file));
|
|
205
|
+
const match = overridesArray.find(item => isOverridden(item[0]));
|
|
206
|
+
if (match) {
|
|
207
|
+
const forcedLang = match[1];
|
|
208
|
+
addResult(file, forcedLang);
|
|
209
|
+
definiteness[file] = true;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Search each language
|
|
214
|
+
let skipExts = false;
|
|
215
|
+
// Check if filename is a match
|
|
216
|
+
for (const lang in langData) {
|
|
217
|
+
const matchesName = (_h = langData[lang].filenames) === null || _h === void 0 ? void 0 : _h.some(name => path_1.default.basename(file.toLowerCase()) === name.toLowerCase());
|
|
218
|
+
if (matchesName) {
|
|
219
|
+
addResult(file, lang);
|
|
220
|
+
skipExts = true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Check if extension is a match
|
|
224
|
+
const possibleExts = [];
|
|
225
|
+
if (!skipExts)
|
|
226
|
+
for (const lang in langData) {
|
|
227
|
+
const extMatches = (_j = langData[lang].extensions) === null || _j === void 0 ? void 0 : _j.filter(ext => file.toLowerCase().endsWith(ext.toLowerCase()));
|
|
228
|
+
if (extMatches === null || extMatches === void 0 ? void 0 : extMatches.length) {
|
|
229
|
+
for (const ext of extMatches)
|
|
230
|
+
possibleExts.push({ ext, lang });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Apply more specific extension if available
|
|
234
|
+
const isComplexExt = (ext) => /\..+\./.test(ext);
|
|
235
|
+
const hasComplexExt = possibleExts.some(data => isComplexExt(data.ext));
|
|
236
|
+
for (const { ext, lang } of possibleExts) {
|
|
237
|
+
if (hasComplexExt && !isComplexExt(ext))
|
|
238
|
+
continue;
|
|
239
|
+
if (!hasComplexExt && isComplexExt(ext))
|
|
240
|
+
continue;
|
|
241
|
+
addResult(file, lang);
|
|
242
|
+
}
|
|
243
|
+
// Fallback to null if no language matches
|
|
244
|
+
if (!fileAssociations[file]) {
|
|
245
|
+
addResult(file, null);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Narrow down file associations to the best fit
|
|
249
|
+
for (const file in fileAssociations) {
|
|
250
|
+
// Skip if file has explicit association
|
|
251
|
+
if (definiteness[file]) {
|
|
252
|
+
results.files.results[file] = fileAssociations[file][0];
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
// Skip binary files
|
|
256
|
+
if (!useRawContent && !opts.keepBinary) {
|
|
257
|
+
const isCustomText = customText.ignores(relPath(file));
|
|
258
|
+
const isCustomBinary = customBinary.ignores(relPath(file));
|
|
259
|
+
const isBinaryExt = binary_extensions_1.default.some(ext => file.endsWith('.' + ext));
|
|
260
|
+
if (!isCustomText && (isCustomBinary || isBinaryExt || await (0, isbinaryfile_1.isBinaryFile)(file))) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Parse heuristics if applicable
|
|
265
|
+
if (opts.checkHeuristics)
|
|
266
|
+
for (const heuristics of heuristicsData.disambiguations) {
|
|
267
|
+
// Make sure the extension matches the current file
|
|
268
|
+
if (!fromShebang[file] && !heuristics.extensions.includes(extensions[file]))
|
|
269
|
+
continue;
|
|
270
|
+
// Load heuristic rules
|
|
271
|
+
for (const heuristic of heuristics.rules) {
|
|
272
|
+
// Make sure the language is not an array
|
|
273
|
+
if (Array.isArray(heuristic.language)) {
|
|
274
|
+
heuristic.language = heuristic.language[0];
|
|
275
|
+
}
|
|
276
|
+
// Make sure the results includes this language
|
|
277
|
+
const languageGroup = (_k = langData[heuristic.language]) === null || _k === void 0 ? void 0 : _k.group;
|
|
278
|
+
const matchesLang = fileAssociations[file].includes(heuristic.language);
|
|
279
|
+
const matchesParent = languageGroup && fileAssociations[file].includes(languageGroup);
|
|
280
|
+
if (!matchesLang && !matchesParent)
|
|
281
|
+
continue;
|
|
282
|
+
// Normalise heuristic data
|
|
283
|
+
const patterns = [];
|
|
284
|
+
const normalise = (contents) => patterns.push(...[contents].flat());
|
|
285
|
+
if (heuristic.pattern)
|
|
286
|
+
normalise(heuristic.pattern);
|
|
287
|
+
if (heuristic.named_pattern)
|
|
288
|
+
normalise(heuristicsData.named_patterns[heuristic.named_pattern]);
|
|
289
|
+
if (heuristic.and) {
|
|
290
|
+
for (const data of heuristic.and) {
|
|
291
|
+
if (data.pattern)
|
|
292
|
+
normalise(data.pattern);
|
|
293
|
+
if (data.named_pattern)
|
|
294
|
+
normalise(heuristicsData.named_patterns[data.named_pattern]);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Check file contents and apply heuristic patterns
|
|
298
|
+
const fileContent = ((_l = opts.fileContent) === null || _l === void 0 ? void 0 : _l.length) ? opts.fileContent[files.indexOf(file)] : await (0, read_file_1.default)(file).catch(() => null);
|
|
299
|
+
if (fileContent === null)
|
|
300
|
+
continue;
|
|
301
|
+
if (!patterns.length || patterns.some(pattern => (0, convert_pcre_1.default)(pattern).test(fileContent))) {
|
|
302
|
+
results.files.results[file] = heuristic.language;
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// If no heuristics, assign a language
|
|
308
|
+
(_m = (_u = results.files.results)[file]) !== null && _m !== void 0 ? _m : (_u[file] = fileAssociations[file][0]);
|
|
309
|
+
}
|
|
310
|
+
// Skip specified categories
|
|
311
|
+
if ((_o = opts.categories) === null || _o === void 0 ? void 0 : _o.length) {
|
|
312
|
+
const categories = ['data', 'markup', 'programming', 'prose'];
|
|
313
|
+
const hiddenCategories = categories.filter(cat => !opts.categories.includes(cat));
|
|
314
|
+
for (const [file, lang] of Object.entries(results.files.results)) {
|
|
315
|
+
if (!hiddenCategories.some(cat => { var _a; return lang && ((_a = langData[lang]) === null || _a === void 0 ? void 0 : _a.type) === cat; })) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
delete results.files.results[file];
|
|
319
|
+
if (lang) {
|
|
320
|
+
delete results.languages.results[lang];
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
for (const category of hiddenCategories) {
|
|
324
|
+
for (const [lang, { type }] of Object.entries(results.languages.results)) {
|
|
325
|
+
if (type === category) {
|
|
326
|
+
delete results.languages.results[lang];
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// Convert paths to relative
|
|
332
|
+
if (!useRawContent && opts.relativePaths) {
|
|
333
|
+
const newMap = {};
|
|
334
|
+
for (const [file, lang] of Object.entries(results.files.results)) {
|
|
335
|
+
let relPath = path_1.default.relative(process.cwd(), file).replace(/\\/g, '/');
|
|
336
|
+
if (!relPath.startsWith('../')) {
|
|
337
|
+
relPath = './' + relPath;
|
|
338
|
+
}
|
|
339
|
+
newMap[relPath] = lang;
|
|
340
|
+
}
|
|
341
|
+
results.files.results = newMap;
|
|
342
|
+
}
|
|
343
|
+
// Load language bytes size
|
|
344
|
+
for (const [file, lang] of Object.entries(results.files.results)) {
|
|
345
|
+
if (lang && !langData[lang])
|
|
346
|
+
continue;
|
|
347
|
+
const fileSize = (_r = (_q = (_p = opts.fileContent) === null || _p === void 0 ? void 0 : _p[files.indexOf(file)]) === null || _q === void 0 ? void 0 : _q.length) !== null && _r !== void 0 ? _r : fs_1.default.statSync(file).size;
|
|
348
|
+
results.files.bytes += fileSize;
|
|
349
|
+
// If no language found, add extension in other section
|
|
350
|
+
if (!lang) {
|
|
351
|
+
const ext = path_1.default.extname(file);
|
|
352
|
+
const unknownType = ext === '' ? 'filenames' : 'extensions';
|
|
353
|
+
const name = ext === '' ? path_1.default.basename(file) : ext;
|
|
354
|
+
(_s = (_v = results.unknown[unknownType])[name]) !== null && _s !== void 0 ? _s : (_v[name] = 0);
|
|
355
|
+
results.unknown[unknownType][name] += fileSize;
|
|
356
|
+
results.unknown.bytes += fileSize;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
// Add language and bytes data to corresponding section
|
|
360
|
+
const { type } = langData[lang];
|
|
361
|
+
(_t = (_w = results.languages.results)[lang]) !== null && _t !== void 0 ? _t : (_w[lang] = { type, bytes: 0, color: langData[lang].color });
|
|
362
|
+
if (opts.childLanguages) {
|
|
363
|
+
results.languages.results[lang].parent = langData[lang].group;
|
|
364
|
+
}
|
|
365
|
+
results.languages.results[lang].bytes += fileSize;
|
|
366
|
+
results.languages.bytes += fileSize;
|
|
367
|
+
}
|
|
368
|
+
// Set counts
|
|
369
|
+
results.files.count = Object.keys(results.files.results).length;
|
|
370
|
+
results.languages.count = Object.keys(results.languages.results).length;
|
|
371
|
+
results.unknown.count = Object.keys({ ...results.unknown.extensions, ...results.unknown.filenames }).length;
|
|
372
|
+
// Return
|
|
373
|
+
return results;
|
|
374
|
+
}
|
|
375
|
+
module.exports = analyse;
|