linguist-js 2.9.2 → 3.0.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/bin/index.js +1 -1
- package/dist/analyser/classifiers/byAttributes.d.ts +3 -0
- package/dist/analyser/classifiers/byAttributes.js +13 -0
- package/dist/analyser/classifiers/byExtension.d.ts +3 -0
- package/dist/analyser/classifiers/byExtension.js +31 -0
- package/dist/analyser/classifiers/byFilename.d.ts +3 -0
- package/dist/analyser/classifiers/byFilename.js +10 -0
- package/dist/analyser/classifiers/byHeuristics.d.ts +3 -0
- package/dist/analyser/classifiers/byHeuristics.js +67 -0
- package/dist/analyser/classifiers/byModeline.d.ts +3 -0
- package/dist/analyser/classifiers/byModeline.js +22 -0
- package/dist/analyser/classifiers/byShebang.d.ts +3 -0
- package/dist/analyser/classifiers/byShebang.js +27 -0
- package/dist/analyser/index.d.ts +3 -0
- package/dist/analyser/index.js +12 -0
- package/dist/analyser/pipeline/aggregate.d.ts +4 -0
- package/dist/analyser/pipeline/aggregate.js +98 -0
- package/dist/analyser/pipeline/classify.d.ts +3 -0
- package/dist/analyser/pipeline/classify.js +32 -0
- package/dist/analyser/pipeline/filter.d.ts +2 -0
- package/dist/analyser/pipeline/filter.js +14 -0
- package/dist/analyser/pipeline/heuristics.d.ts +3 -0
- package/dist/analyser/pipeline/heuristics.js +12 -0
- package/dist/analyser/pipeline/normalise.d.ts +2 -0
- package/dist/analyser/pipeline/normalise.js +9 -0
- package/dist/cli/output/default.d.ts +3 -0
- package/dist/cli/output/default.js +106 -0
- package/dist/cli/output/tree.d.ts +3 -0
- package/dist/cli/output/tree.js +11 -0
- package/dist/cli/runCliAnalysis.d.ts +2 -0
- package/dist/cli/runCliAnalysis.js +26 -0
- package/dist/cli/utils.d.ts +2 -0
- package/dist/cli/utils.js +9 -0
- package/dist/cli.js +14 -143
- package/dist/entry/analyseFs.d.ts +2 -0
- package/dist/entry/analyseFs.js +11 -0
- package/dist/entry/analyseRaw.d.ts +4 -0
- package/dist/entry/analyseRaw.js +11 -0
- package/dist/index.d.ts +19 -4
- package/dist/index.js +18 -483
- package/dist/input/fromFilesystem.d.ts +4 -0
- package/dist/input/fromFilesystem.js +115 -0
- package/dist/input/fromRawContent.d.ts +2 -0
- package/dist/input/fromRawContent.js +19 -0
- package/dist/input/normaliseOpts.d.ts +2 -0
- package/dist/input/normaliseOpts.js +13 -0
- package/dist/program/classes/attributes.d.ts +11 -0
- package/dist/program/classes/attributes.js +37 -0
- package/dist/program/data/loadDataFiles.d.ts +4 -0
- package/dist/program/data/loadDataFiles.js +32 -0
- package/dist/program/data/retrieveData.d.ts +12 -0
- package/dist/program/data/retrieveData.js +27 -0
- package/dist/program/fs/normalisedPath.d.ts +3 -0
- package/dist/program/fs/normalisedPath.js +17 -0
- package/dist/program/fs/readFile.js +17 -0
- package/dist/program/fs/walkTree.d.ts +20 -0
- package/dist/program/fs/walkTree.js +82 -0
- package/dist/program/parsing/parseGitattributes.d.ts +17 -0
- package/dist/program/parsing/parseGitattributes.js +33 -0
- package/dist/program/parsing/parseGitignore.js +9 -0
- package/dist/program/processFiles.d.ts +9 -0
- package/dist/program/processFiles.js +111 -0
- package/dist/program/utils/pcre.js +35 -0
- package/dist/src/cli/output/default.d.ts +3 -0
- package/dist/src/cli/output/default.js +106 -0
- package/dist/src/cli/output/tree.d.ts +3 -0
- package/dist/src/cli/output/tree.js +11 -0
- package/dist/src/cli/runCliAnalysis.d.ts +2 -0
- package/dist/src/cli/runCliAnalysis.js +25 -0
- package/dist/src/cli/utils.d.ts +2 -0
- package/dist/src/cli/utils.js +9 -0
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.js +56 -0
- package/dist/src/helpers/convert-pcre.d.ts +2 -0
- package/dist/src/helpers/parse-gitignore.d.ts +1 -0
- package/dist/src/helpers/read-file.d.ts +5 -0
- package/dist/{helpers → src/helpers}/read-file.js +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +470 -0
- package/dist/src/program/data/loadData.d.ts +4 -0
- package/dist/src/program/data/loadData.js +32 -0
- package/dist/src/program/fs/normalisedPath.d.ts +2 -0
- package/dist/src/program/fs/normalisedPath.js +7 -0
- package/dist/src/program/fs/readFile.d.ts +5 -0
- package/dist/src/program/fs/readFile.js +17 -0
- package/dist/src/program/fs/walkTree.d.ts +20 -0
- package/dist/src/program/fs/walkTree.js +82 -0
- package/dist/src/program/parsing/parseGitattributes.d.ts +17 -0
- package/dist/src/program/parsing/parseGitattributes.js +33 -0
- package/dist/src/program/parsing/parseGitignore.d.ts +1 -0
- package/dist/src/program/parsing/parseGitignore.js +9 -0
- package/dist/src/program/utils/pcre.d.ts +2 -0
- package/dist/src/program/utils/pcre.js +35 -0
- package/dist/src/types/schema.d.ts +37 -0
- package/dist/src/types/schema.js +1 -0
- package/dist/src/types/types.d.ts +65 -0
- package/dist/src/types/types.js +1 -0
- package/dist/types/schema.d.ts +37 -0
- package/dist/types/schema.js +1 -0
- package/dist/types/types.d.ts +84 -0
- package/dist/types/types.js +1 -0
- package/ext/generated.rb +6 -0
- package/ext/heuristics.yml +102 -11
- package/ext/languages.yml +526 -88
- package/ext/vendor.yml +1 -0
- package/package.json +14 -13
- package/readme.md +99 -111
- /package/dist/{helpers/read-file.d.ts → program/fs/readFile.d.ts} +0 -0
- /package/dist/{helpers/parse-gitignore.d.ts → program/parsing/parseGitignore.d.ts} +0 -0
- /package/dist/{helpers/convert-pcre.d.ts → program/utils/pcre.d.ts} +0 -0
- /package/dist/{helpers → src/helpers}/convert-pcre.js +0 -0
- /package/dist/{helpers → src/helpers}/load-data.d.ts +0 -0
- /package/dist/{helpers → src/helpers}/load-data.js +0 -0
- /package/dist/{helpers → src/helpers}/norm-path.d.ts +0 -0
- /package/dist/{helpers → src/helpers}/norm-path.js +0 -0
- /package/dist/{helpers → src/helpers}/parse-gitattributes.d.ts +0 -0
- /package/dist/{helpers → src/helpers}/parse-gitattributes.js +0 -0
- /package/dist/{helpers → src/helpers}/parse-gitignore.js +0 -0
- /package/dist/{helpers → src/helpers}/walk-tree.d.ts +0 -0
- /package/dist/{helpers → src/helpers}/walk-tree.js +0 -0
- /package/dist/{schema.d.ts → src/schema.d.ts} +0 -0
- /package/dist/{schema.js → src/schema.js} +0 -0
- /package/dist/{types.d.ts → src/types.d.ts} +0 -0
- /package/dist/{types.js → src/types.js} +0 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function colouredMsg([r, g, b], msg) {
|
|
2
|
+
return `\u001B[${38};2;${r};${g};${b}m${msg}\u001b[0m`;
|
|
3
|
+
}
|
|
4
|
+
export function hexToRgb(hex) {
|
|
5
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
6
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
7
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
8
|
+
return [r, g, b];
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { program } from 'commander';
|
|
2
|
+
import FS from 'node:fs';
|
|
3
|
+
import runCliAnalysis from './cli/runCliAnalysis.js';
|
|
4
|
+
const packageJson = JSON.parse(FS.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
5
|
+
const VERSION = packageJson.version;
|
|
6
|
+
program
|
|
7
|
+
.name('linguist')
|
|
8
|
+
.usage('--analyze [<folders...>] [<options...>]')
|
|
9
|
+
.option('-a|--analyze [folders...]', 'Analyse the languages of all files in a folder')
|
|
10
|
+
.option('-i|--ignoredFiles <files...>', `A list of file path globs to ignore`)
|
|
11
|
+
.option('-l|--ignoredLanguages <languages...>', `A list of languages to ignore`)
|
|
12
|
+
.option('-c|--categories <categories...>', 'Language categories to include in output')
|
|
13
|
+
.option('-C|--childLanguages [bool]', 'Display child languages instead of their parents', false)
|
|
14
|
+
.option('-j|--json [bool]', 'Display the output as JSON', false)
|
|
15
|
+
.option('-t|--tree <traversal>', 'Which part of the output JSON to display (dot-delimited)')
|
|
16
|
+
.option('-F|--listFiles [bool]', 'Whether to list every matching file under the language results', false)
|
|
17
|
+
.option('-m|--minSize <size>', 'Minimum size of file to show language results for (must have a unit: b, kb, mb, %, or loc)')
|
|
18
|
+
.option('-q|--quick [bool]', 'Skip complex language analysis (alias for -{A|I|H|S}=false)', false)
|
|
19
|
+
.option('-o|--offline [bool]', 'Use packaged data files instead of fetching latest from GitHub', false)
|
|
20
|
+
.option('-L|--calculateLines [bool]', 'Calculate lines of code totals', true)
|
|
21
|
+
.option('-V|--keepVendored [bool]', 'Prevent skipping over vendored/generated files', false)
|
|
22
|
+
.option('-B|--keepBinary [bool]', 'Prevent skipping over binary files', false)
|
|
23
|
+
.option('-r|--relativePaths [bool]', 'Convert absolute file paths to relative', false)
|
|
24
|
+
.option('-A|--checkAttributes [bool]', 'Force the checking of gitattributes files', true)
|
|
25
|
+
.option('-I|--checkIgnored [bool]', 'Force the checking of gitignore files', true)
|
|
26
|
+
.option('-D|--checkDetected [bool]', 'Force files marked with linguist-detectable to always appear in output', true)
|
|
27
|
+
.option('-H|--checkHeuristics [bool]', 'Apply heuristics to ambiguous languages', true)
|
|
28
|
+
.option('-S|--checkShebang [bool]', 'Check shebang lines for explicit classification', true)
|
|
29
|
+
.option('-M|--checkModeline [bool]', 'Check modelines for explicit classification', true)
|
|
30
|
+
.helpOption(`-h|--help`, 'Display this help message')
|
|
31
|
+
.version(VERSION, '-v|--version', 'Display the installed version of linguist-js');
|
|
32
|
+
program.parse(process.argv);
|
|
33
|
+
const args = program.opts();
|
|
34
|
+
// Normalise arguments
|
|
35
|
+
for (const arg in args) {
|
|
36
|
+
const normalise = (val) => {
|
|
37
|
+
if (typeof val !== 'string')
|
|
38
|
+
return val;
|
|
39
|
+
val = val.replace(/^=/, '');
|
|
40
|
+
if (val.match(/true$|false$/))
|
|
41
|
+
val = val === 'true';
|
|
42
|
+
return val;
|
|
43
|
+
};
|
|
44
|
+
if (Array.isArray(args[arg]))
|
|
45
|
+
args[arg] = args[arg].map(normalise);
|
|
46
|
+
else
|
|
47
|
+
args[arg] = normalise(args[arg]);
|
|
48
|
+
}
|
|
49
|
+
// Run Linguist
|
|
50
|
+
if (args.analyze) {
|
|
51
|
+
void runCliAnalysis(args);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
console.log(`Welcome to linguist-js, a JavaScript port of GitHub's language analyzer.`);
|
|
55
|
+
console.log(`Type 'linguist --help' for a list of commands.`);
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function parseGitignore(content: string): string[];
|
|
@@ -15,7 +15,7 @@ async function readFileChunk(filename, onlyFirstLine = false) {
|
|
|
15
15
|
let content = '';
|
|
16
16
|
for await (const data of stream) { // may throw
|
|
17
17
|
content += data.toString();
|
|
18
|
-
if (onlyFirstLine) {
|
|
18
|
+
if (onlyFirstLine && content.includes('\n')) {
|
|
19
19
|
return content.split(/\r?\n/)[0];
|
|
20
20
|
}
|
|
21
21
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import * as T from './types/types.js';
|
|
2
|
+
declare function analyse(path?: string, opts?: T.Options): Promise<T.Results>;
|
|
3
|
+
declare function analyse(paths?: string[], opts?: T.Options): Promise<T.Results>;
|
|
4
|
+
declare function analyse(content?: Record<string, string>, opts?: T.Options): Promise<T.Results>;
|
|
5
|
+
export default analyse;
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import commonPrefix from 'common-path-prefix';
|
|
2
|
+
import ignore from 'ignore';
|
|
3
|
+
import { isBinaryFile } from 'isbinaryfile';
|
|
4
|
+
import YAML from 'js-yaml';
|
|
5
|
+
import FS from 'node:fs';
|
|
6
|
+
import Path from 'node:path';
|
|
7
|
+
import loadFile, { parseGeneratedDataFile } from './program/data/loadData.js';
|
|
8
|
+
import { normPath } from './program/fs/normalisedPath.js';
|
|
9
|
+
import readFileChunk from './program/fs/readFile.js';
|
|
10
|
+
import walkTree from './program/fs/walkTree.js';
|
|
11
|
+
import parseGitattributes from './program/parsing/parseGitattributes.js';
|
|
12
|
+
import pcre from './program/utils/pcre.js';
|
|
13
|
+
const binaryData = JSON.parse(FS.readFileSync(new URL('../node_modules/binary-extensions/binary-extensions.json', import.meta.url), 'utf-8'));
|
|
14
|
+
async function analyse(rawInput, opts = {}) {
|
|
15
|
+
const inputs = {
|
|
16
|
+
path: typeof rawInput === 'string' ? rawInput : null,
|
|
17
|
+
paths: Array.isArray(rawInput) ? rawInput : null,
|
|
18
|
+
content: typeof rawInput === 'object' && !Array.isArray(rawInput) ? rawInput : null,
|
|
19
|
+
};
|
|
20
|
+
const inputPaths = inputs.paths ?? (inputs.path ? [inputs.path] : null);
|
|
21
|
+
const inputContent = inputs.content;
|
|
22
|
+
const useRawContent = inputContent !== null;
|
|
23
|
+
const input = useRawContent ? Object.keys(inputContent) : (inputPaths ?? []);
|
|
24
|
+
// Normalise input option arguments
|
|
25
|
+
opts = {
|
|
26
|
+
calculateLines: opts.calculateLines ?? true, // default to true if unset
|
|
27
|
+
checkIgnored: !opts.quick,
|
|
28
|
+
checkDetected: !opts.quick,
|
|
29
|
+
checkAttributes: !opts.quick,
|
|
30
|
+
checkHeuristics: !opts.quick,
|
|
31
|
+
checkShebang: !opts.quick,
|
|
32
|
+
checkModeline: !opts.quick,
|
|
33
|
+
...opts,
|
|
34
|
+
};
|
|
35
|
+
// Load data from github-linguist web repo
|
|
36
|
+
const langData = await loadFile('languages.yml', opts.offline).then(YAML.load);
|
|
37
|
+
const vendorData = await loadFile('vendor.yml', opts.offline).then(YAML.load);
|
|
38
|
+
const docData = await loadFile('documentation.yml', opts.offline).then(YAML.load);
|
|
39
|
+
const heuristicsData = await loadFile('heuristics.yml', opts.offline).then(YAML.load);
|
|
40
|
+
const generatedData = await loadFile('generated.rb', opts.offline).then(parseGeneratedDataFile);
|
|
41
|
+
const vendorPaths = [...vendorData, ...docData, ...generatedData];
|
|
42
|
+
// Setup main variables
|
|
43
|
+
const fileAssociations = {};
|
|
44
|
+
const extensions = {};
|
|
45
|
+
const globOverrides = {};
|
|
46
|
+
const results = {
|
|
47
|
+
files: { count: 0, bytes: 0, lines: { total: 0, content: 0 }, results: {}, alternatives: {} },
|
|
48
|
+
languages: { count: 0, bytes: 0, lines: { total: 0, content: 0 }, results: {} },
|
|
49
|
+
unknown: { count: 0, bytes: 0, lines: { total: 0, content: 0 }, extensions: {}, filenames: {} },
|
|
50
|
+
repository: {},
|
|
51
|
+
};
|
|
52
|
+
// Set a common root path so that vendor paths do not incorrectly match parent folders
|
|
53
|
+
const resolvedInput = input.map((path) => normPath(Path.resolve(path)));
|
|
54
|
+
const commonRoot = (input.length > 1 ? commonPrefix(resolvedInput) : resolvedInput[0]).replace(/\/?$/, '');
|
|
55
|
+
const relPath = (file) => (useRawContent ? file : normPath(Path.relative(commonRoot, file)));
|
|
56
|
+
const unRelPath = (file) => (useRawContent ? file : normPath(Path.resolve(commonRoot, file)));
|
|
57
|
+
// Other helper functions
|
|
58
|
+
const fileMatchesGlobs = (file, ...globs) => ignore().add(globs).ignores(relPath(file));
|
|
59
|
+
const filterOutIgnored = (files, ignored) => ignored.filter(files.map(relPath)).map(unRelPath);
|
|
60
|
+
//*PREPARE FILES AND DATA*//
|
|
61
|
+
// Prepare list of ignored files
|
|
62
|
+
const ignored = ignore();
|
|
63
|
+
ignored.add('.git/');
|
|
64
|
+
ignored.add(opts.ignoredFiles ?? []);
|
|
65
|
+
const regexIgnores = opts.keepVendored ? [] : vendorPaths.map((path) => RegExp(path, 'i'));
|
|
66
|
+
// Load file paths and folders
|
|
67
|
+
let files;
|
|
68
|
+
if (useRawContent) {
|
|
69
|
+
// Uses raw file content
|
|
70
|
+
files = input;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// Uses directory on disc
|
|
74
|
+
const data = walkTree({ init: true, commonRoot, folderRoots: resolvedInput, folders: resolvedInput, ignored });
|
|
75
|
+
files = data.files;
|
|
76
|
+
}
|
|
77
|
+
// Fetch and normalise gitattributes data of all subfolders and save to metadata
|
|
78
|
+
const manualAttributes = {}; // Maps file globs to gitattribute boolean flags
|
|
79
|
+
const getFlaggedGlobs = (attr, val) => {
|
|
80
|
+
return Object.entries(manualAttributes)
|
|
81
|
+
.filter(([, attrs]) => attrs[attr] === val)
|
|
82
|
+
.map(([glob]) => glob);
|
|
83
|
+
};
|
|
84
|
+
const findAttrsForPath = (filePath) => {
|
|
85
|
+
const resultAttrs = {};
|
|
86
|
+
for (const glob in manualAttributes) {
|
|
87
|
+
if (ignore().add(glob).ignores(relPath(filePath))) {
|
|
88
|
+
const matchingAttrs = manualAttributes[glob];
|
|
89
|
+
for (const [attr, val] of Object.entries(matchingAttrs)) {
|
|
90
|
+
if (val !== null)
|
|
91
|
+
resultAttrs[attr] = val;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!JSON.stringify(resultAttrs)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return resultAttrs;
|
|
99
|
+
};
|
|
100
|
+
if (!useRawContent && opts.checkAttributes) {
|
|
101
|
+
const nestedAttrFiles = files.filter((file) => file.endsWith('.gitattributes'));
|
|
102
|
+
for (const attrFile of nestedAttrFiles) {
|
|
103
|
+
const relAttrFile = relPath(attrFile);
|
|
104
|
+
const relAttrFolder = Path.dirname(relAttrFile);
|
|
105
|
+
const contents = await readFileChunk(attrFile);
|
|
106
|
+
const parsed = parseGitattributes(contents, relAttrFolder);
|
|
107
|
+
for (const { glob, attrs } of parsed) {
|
|
108
|
+
manualAttributes[glob] = attrs;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Remove files that are linguist-ignored via regex by default unless explicitly unignored in gitattributes
|
|
113
|
+
const filesToIgnore = [];
|
|
114
|
+
for (const file of files) {
|
|
115
|
+
const relFile = relPath(file);
|
|
116
|
+
const isRegexIgnored = regexIgnores.some((pattern) => pattern.test(relFile));
|
|
117
|
+
if (!isRegexIgnored) {
|
|
118
|
+
// Checking overrides is moot if file is not even marked as ignored by default
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const fileAttrs = findAttrsForPath(file);
|
|
122
|
+
if (fileAttrs?.generated === false || fileAttrs?.vendored === false) {
|
|
123
|
+
// File is explicitly marked as *not* to be ignored
|
|
124
|
+
// do nothing
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
filesToIgnore.push(file);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
files = files.filter((file) => !filesToIgnore.includes(file));
|
|
131
|
+
// Apply vendor file path matches and filter out vendored files
|
|
132
|
+
if (!opts.keepVendored) {
|
|
133
|
+
// Get data of files that have been manually marked with metadata
|
|
134
|
+
const vendorTrueGlobs = [
|
|
135
|
+
...getFlaggedGlobs('vendored', true),
|
|
136
|
+
...getFlaggedGlobs('generated', true),
|
|
137
|
+
...getFlaggedGlobs('documentation', true),
|
|
138
|
+
];
|
|
139
|
+
const vendorFalseGlobs = [
|
|
140
|
+
...getFlaggedGlobs('vendored', false),
|
|
141
|
+
...getFlaggedGlobs('generated', false),
|
|
142
|
+
...getFlaggedGlobs('documentation', false),
|
|
143
|
+
];
|
|
144
|
+
// Set up glob ignore object to use for expanding globs to match files
|
|
145
|
+
const vendorTrueIgnore = ignore().add(vendorTrueGlobs);
|
|
146
|
+
const vendorFalseIgnore = ignore().add(vendorFalseGlobs);
|
|
147
|
+
// Remove all files marked as vendored by default
|
|
148
|
+
const excludedFiles = files.filter((file) => vendorPaths.some((pathPtn) => RegExp(pathPtn, 'i').test(relPath(file))));
|
|
149
|
+
files = files.filter((file) => !excludedFiles.includes(file));
|
|
150
|
+
// Re-add removed files that are overridden manually in gitattributes
|
|
151
|
+
const overriddenExcludedFiles = excludedFiles.filter((file) => vendorFalseIgnore.ignores(relPath(file)));
|
|
152
|
+
files.push(...overriddenExcludedFiles);
|
|
153
|
+
// Remove files explicitly marked as vendored in gitattributes
|
|
154
|
+
files = files.filter((file) => !vendorTrueIgnore.ignores(relPath(file)));
|
|
155
|
+
}
|
|
156
|
+
// Filter out binary files
|
|
157
|
+
if (!opts.keepBinary) {
|
|
158
|
+
// Filter out files that are binary by default
|
|
159
|
+
files = files.filter((file) => !binaryData.some((ext) => file.endsWith('.' + ext)));
|
|
160
|
+
// Filter out manually specified binary files
|
|
161
|
+
const binaryIgnored = ignore().add(getFlaggedGlobs('binary', true));
|
|
162
|
+
files = filterOutIgnored(files, binaryIgnored);
|
|
163
|
+
// Re-add files manually marked not as binary
|
|
164
|
+
const binaryUnignored = ignore().add(getFlaggedGlobs('binary', false));
|
|
165
|
+
const unignoredList = filterOutIgnored(files, binaryUnignored);
|
|
166
|
+
files.push(...unignoredList);
|
|
167
|
+
}
|
|
168
|
+
// Ignore specific languages
|
|
169
|
+
for (const lang of opts.ignoredLanguages ?? []) {
|
|
170
|
+
for (const key in langData) {
|
|
171
|
+
if (lang.toLowerCase() === key.toLowerCase()) {
|
|
172
|
+
delete langData[key];
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Establish language overrides taken from gitattributes
|
|
178
|
+
const forcedLangs = Object.entries(manualAttributes).filter(([, attrs]) => attrs.language);
|
|
179
|
+
for (const [globPath, attrs] of forcedLangs) {
|
|
180
|
+
let forcedLang = attrs.language;
|
|
181
|
+
if (!forcedLang)
|
|
182
|
+
continue;
|
|
183
|
+
// If specified language is an alias, associate it with its full name
|
|
184
|
+
if (!langData[forcedLang]) {
|
|
185
|
+
const overrideLang = Object.entries(langData).find((entry) => entry[1].aliases?.includes(forcedLang.toLowerCase()));
|
|
186
|
+
if (overrideLang) {
|
|
187
|
+
forcedLang = overrideLang[0];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
globOverrides[globPath] = forcedLang;
|
|
191
|
+
}
|
|
192
|
+
//*PARSE LANGUAGES*//
|
|
193
|
+
const addResult = (file, result) => {
|
|
194
|
+
if (!fileAssociations[file]) {
|
|
195
|
+
fileAssociations[file] = [];
|
|
196
|
+
extensions[file] = '';
|
|
197
|
+
}
|
|
198
|
+
// Set parent to result group if it is present
|
|
199
|
+
// Is nullish if either `opts.childLanguages` is set or if there is no group
|
|
200
|
+
const finalResult = (!opts.childLanguages && result && langData[result] && langData[result].group) || result;
|
|
201
|
+
if (!fileAssociations[file].includes(finalResult)) {
|
|
202
|
+
fileAssociations[file].push(finalResult);
|
|
203
|
+
}
|
|
204
|
+
extensions[file] = Path.extname(file).toLowerCase();
|
|
205
|
+
};
|
|
206
|
+
const definiteness = {};
|
|
207
|
+
const fromShebang = {};
|
|
208
|
+
fileLoop: for (const file of files) {
|
|
209
|
+
// Check manual override
|
|
210
|
+
for (const globMatch in globOverrides) {
|
|
211
|
+
if (!fileMatchesGlobs(file, globMatch))
|
|
212
|
+
continue;
|
|
213
|
+
// If the given file matches the glob, apply the override to the file
|
|
214
|
+
const forcedLang = globOverrides[globMatch];
|
|
215
|
+
addResult(file, forcedLang);
|
|
216
|
+
definiteness[file] = true;
|
|
217
|
+
continue fileLoop; // no need to check other heuristics, the classified language has been found
|
|
218
|
+
}
|
|
219
|
+
// Check first line for readability
|
|
220
|
+
let firstLine;
|
|
221
|
+
if (useRawContent) {
|
|
222
|
+
firstLine = inputContent[file]?.split('\n')[0] ?? null;
|
|
223
|
+
}
|
|
224
|
+
else if (FS.existsSync(file) && !FS.lstatSync(file).isDirectory()) {
|
|
225
|
+
firstLine = await readFileChunk(file, true).catch(() => null);
|
|
226
|
+
}
|
|
227
|
+
else
|
|
228
|
+
continue;
|
|
229
|
+
// Skip if file is unreadable or blank
|
|
230
|
+
if (firstLine === null)
|
|
231
|
+
continue;
|
|
232
|
+
// Check first line for explicit classification
|
|
233
|
+
const modelineRegex = /-\*-|(?:syntax|filetype|ft)\s*=/;
|
|
234
|
+
const hasShebang = opts.checkShebang && /^#!/.test(firstLine);
|
|
235
|
+
const hasModeline = opts.checkModeline && modelineRegex.test(firstLine);
|
|
236
|
+
if (!opts.quick && (hasShebang || hasModeline)) {
|
|
237
|
+
const matches = [];
|
|
238
|
+
for (const [lang, data] of Object.entries(langData)) {
|
|
239
|
+
const langMatcher = (lang) => `\\b${lang.toLowerCase().replace(/\W/g, '\\$&')}(?![\\w#+*]|-\*-)`;
|
|
240
|
+
// Check for interpreter match
|
|
241
|
+
if (opts.checkShebang && hasShebang) {
|
|
242
|
+
const matchesInterpretor = data.interpreters?.some((interpreter) => firstLine.match(`\\b${interpreter}\\b`));
|
|
243
|
+
if (matchesInterpretor)
|
|
244
|
+
matches.push(lang);
|
|
245
|
+
}
|
|
246
|
+
// Check modeline declaration
|
|
247
|
+
if (opts.checkModeline && hasModeline) {
|
|
248
|
+
const modelineText = firstLine.toLowerCase().split(modelineRegex)[1];
|
|
249
|
+
const matchesLang = modelineText.match(langMatcher(lang));
|
|
250
|
+
const matchesAlias = data.aliases?.some((lang) => modelineText.match(langMatcher(lang)));
|
|
251
|
+
if (matchesLang || matchesAlias)
|
|
252
|
+
matches.push(lang);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Add identified language(s)
|
|
256
|
+
if (matches.length) {
|
|
257
|
+
for (const match of matches)
|
|
258
|
+
addResult(file, match);
|
|
259
|
+
if (matches.length === 1)
|
|
260
|
+
definiteness[file] = true;
|
|
261
|
+
fromShebang[file] = true;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Search each language
|
|
266
|
+
let skipExts = false;
|
|
267
|
+
// Check if filename is a match
|
|
268
|
+
for (const lang in langData) {
|
|
269
|
+
const matchesName = langData[lang].filenames?.some((name) => Path.basename(file.toLowerCase()) === name.toLowerCase());
|
|
270
|
+
if (matchesName) {
|
|
271
|
+
addResult(file, lang);
|
|
272
|
+
skipExts = true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Check if extension is a match
|
|
276
|
+
const possibleExts = [];
|
|
277
|
+
if (!skipExts)
|
|
278
|
+
for (const lang in langData) {
|
|
279
|
+
const extMatches = langData[lang].extensions?.filter((ext) => file.toLowerCase().endsWith(ext.toLowerCase()));
|
|
280
|
+
if (extMatches?.length) {
|
|
281
|
+
for (const ext of extMatches)
|
|
282
|
+
possibleExts.push({ ext, lang });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Apply more specific extension if available
|
|
286
|
+
const isComplexExt = (ext) => /\..+\./.test(ext);
|
|
287
|
+
const hasComplexExt = possibleExts.some((data) => isComplexExt(data.ext));
|
|
288
|
+
for (const { ext, lang } of possibleExts) {
|
|
289
|
+
if (hasComplexExt && !isComplexExt(ext))
|
|
290
|
+
continue;
|
|
291
|
+
if (!hasComplexExt && isComplexExt(ext))
|
|
292
|
+
continue;
|
|
293
|
+
addResult(file, lang);
|
|
294
|
+
}
|
|
295
|
+
// Fallback to null if no language matches
|
|
296
|
+
if (!fileAssociations[file]) {
|
|
297
|
+
addResult(file, null);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// Narrow down file associations to the best fit
|
|
301
|
+
for (const file in fileAssociations) {
|
|
302
|
+
// Skip if file has explicit association
|
|
303
|
+
if (definiteness[file]) {
|
|
304
|
+
results.files.results[file] = fileAssociations[file][0];
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
// Skip binary files
|
|
308
|
+
if (!useRawContent && !opts.keepBinary) {
|
|
309
|
+
if (await isBinaryFile(file))
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
// Parse heuristics if applicable
|
|
313
|
+
if (opts.checkHeuristics)
|
|
314
|
+
for (const heuristics of heuristicsData.disambiguations) {
|
|
315
|
+
// Make sure the extension matches the current file
|
|
316
|
+
if (!fromShebang[file] && !heuristics.extensions.includes(extensions[file]))
|
|
317
|
+
continue;
|
|
318
|
+
// Load heuristic rules
|
|
319
|
+
for (const heuristic of heuristics.rules) {
|
|
320
|
+
// Make sure the language is not an array
|
|
321
|
+
if (Array.isArray(heuristic.language)) {
|
|
322
|
+
heuristic.language = heuristic.language[0];
|
|
323
|
+
}
|
|
324
|
+
// Make sure the results includes this language
|
|
325
|
+
const languageGroup = langData[heuristic.language]?.group;
|
|
326
|
+
const matchesLang = fileAssociations[file].includes(heuristic.language);
|
|
327
|
+
const matchesParent = languageGroup && fileAssociations[file].includes(languageGroup);
|
|
328
|
+
if (!matchesLang && !matchesParent)
|
|
329
|
+
continue;
|
|
330
|
+
// Normalise heuristic data
|
|
331
|
+
const patterns = [];
|
|
332
|
+
const normalise = (contents) => patterns.push(...[contents].flat());
|
|
333
|
+
if (heuristic.pattern)
|
|
334
|
+
normalise(heuristic.pattern);
|
|
335
|
+
if (heuristic.named_pattern)
|
|
336
|
+
normalise(heuristicsData.named_patterns[heuristic.named_pattern]);
|
|
337
|
+
if (heuristic.and) {
|
|
338
|
+
for (const data of heuristic.and) {
|
|
339
|
+
if (data.pattern)
|
|
340
|
+
normalise(data.pattern);
|
|
341
|
+
if (data.named_pattern)
|
|
342
|
+
normalise(heuristicsData.named_patterns[data.named_pattern]);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Check file contents and apply heuristic patterns
|
|
346
|
+
const fileContent = useRawContent ? inputContent[file] : await readFileChunk(file).catch(() => null);
|
|
347
|
+
// Skip if file read errors
|
|
348
|
+
if (fileContent === null)
|
|
349
|
+
continue;
|
|
350
|
+
// Apply heuristics
|
|
351
|
+
if (!patterns.length || patterns.some((pattern) => pcre(pattern).test(fileContent))) {
|
|
352
|
+
results.files.results[file] = heuristic.language;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// If no heuristics, assign a language
|
|
358
|
+
if (!results.files.results[file]) {
|
|
359
|
+
const possibleLangs = fileAssociations[file];
|
|
360
|
+
// Assign first language as a default option
|
|
361
|
+
const defaultLang = possibleLangs[0];
|
|
362
|
+
const alternativeLangs = possibleLangs.slice(1);
|
|
363
|
+
results.files.results[file] = defaultLang;
|
|
364
|
+
// List alternative languages if there are any
|
|
365
|
+
if (alternativeLangs.length > 0)
|
|
366
|
+
results.files.alternatives[file] = alternativeLangs;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Skip specified categories
|
|
370
|
+
if (opts.categories?.length) {
|
|
371
|
+
const categories = ['data', 'markup', 'programming', 'prose'];
|
|
372
|
+
const hiddenCategories = categories.filter((cat) => !opts.categories.includes(cat));
|
|
373
|
+
for (const [file, lang] of Object.entries(results.files.results)) {
|
|
374
|
+
// Skip if language is not hidden
|
|
375
|
+
if (!hiddenCategories.some((cat) => lang && langData[lang]?.type === cat))
|
|
376
|
+
continue;
|
|
377
|
+
// Skip if language is forced as detectable
|
|
378
|
+
if (opts.checkDetected) {
|
|
379
|
+
const detectable = ignore().add(getFlaggedGlobs('detectable', true));
|
|
380
|
+
if (detectable.ignores(relPath(file)))
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
// Delete result otherwise
|
|
384
|
+
delete results.files.results[file];
|
|
385
|
+
if (lang)
|
|
386
|
+
delete results.languages.results[lang];
|
|
387
|
+
}
|
|
388
|
+
for (const category of hiddenCategories) {
|
|
389
|
+
for (const [lang, { type }] of Object.entries(results.repository)) {
|
|
390
|
+
if (type === category) {
|
|
391
|
+
delete results.languages.results[lang];
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// Convert paths to relative
|
|
397
|
+
if (!useRawContent && opts.relativePaths) {
|
|
398
|
+
const newMap = {};
|
|
399
|
+
for (const [file, lang] of Object.entries(results.files.results)) {
|
|
400
|
+
let relPath = normPath(Path.relative(process.cwd(), file));
|
|
401
|
+
if (!relPath.startsWith('../')) {
|
|
402
|
+
relPath = './' + relPath;
|
|
403
|
+
}
|
|
404
|
+
newMap[relPath] = lang;
|
|
405
|
+
}
|
|
406
|
+
results.files.results = newMap;
|
|
407
|
+
}
|
|
408
|
+
// Load language bytes size
|
|
409
|
+
for (const [file, lang] of Object.entries(results.files.results)) {
|
|
410
|
+
if (lang && !langData[lang])
|
|
411
|
+
continue;
|
|
412
|
+
// Calculate file size
|
|
413
|
+
const fileSize = useRawContent ? inputContent[file]?.length : FS.statSync(file).size;
|
|
414
|
+
// Calculate lines of code
|
|
415
|
+
const loc = { total: 0, content: 0 };
|
|
416
|
+
if (opts.calculateLines) {
|
|
417
|
+
const fileContent = useRawContent ? inputContent[file] : FS.readFileSync(file).toString();
|
|
418
|
+
const allLines = fileContent.split(/\r?\n/gm);
|
|
419
|
+
loc.total = allLines.length;
|
|
420
|
+
loc.content = allLines.filter((line) => line.trim().length > 0).length;
|
|
421
|
+
}
|
|
422
|
+
// Apply to files totals
|
|
423
|
+
results.files.bytes += fileSize;
|
|
424
|
+
results.files.lines.total += loc.total;
|
|
425
|
+
results.files.lines.content += loc.content;
|
|
426
|
+
// Add results to 'languages' section if language match found, or 'unknown' section otherwise
|
|
427
|
+
if (lang) {
|
|
428
|
+
// update language in repository if not yet present
|
|
429
|
+
if (!results.repository[lang]) {
|
|
430
|
+
const { type, color } = langData[lang];
|
|
431
|
+
results.repository[lang] = { type, color };
|
|
432
|
+
if (opts.childLanguages) {
|
|
433
|
+
results.repository[lang].parent = langData[lang].group;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// set default if unset
|
|
437
|
+
results.languages.results[lang] ??= { count: 0, bytes: 0, lines: { total: 0, content: 0 } };
|
|
438
|
+
// apply results to 'languages' section
|
|
439
|
+
results.languages.results[lang].count++;
|
|
440
|
+
results.languages.results[lang].bytes += fileSize;
|
|
441
|
+
results.languages.bytes += fileSize;
|
|
442
|
+
results.languages.results[lang].lines.total += loc.total;
|
|
443
|
+
results.languages.results[lang].lines.content += loc.content;
|
|
444
|
+
results.languages.lines.total += loc.total;
|
|
445
|
+
results.languages.lines.content += loc.content;
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
const ext = Path.extname(file);
|
|
449
|
+
const unknownType = ext ? 'extensions' : 'filenames';
|
|
450
|
+
const name = ext || Path.basename(file);
|
|
451
|
+
// apply results to 'unknown' section
|
|
452
|
+
results.unknown[unknownType][name] ??= 0;
|
|
453
|
+
results.unknown[unknownType][name] += fileSize;
|
|
454
|
+
results.unknown.bytes += fileSize;
|
|
455
|
+
results.unknown.lines.total += loc.total;
|
|
456
|
+
results.unknown.lines.content += loc.content;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// Set lines output to NaN when line calculation is disabled
|
|
460
|
+
if (opts.calculateLines === false) {
|
|
461
|
+
results.files.lines = { total: NaN, content: NaN };
|
|
462
|
+
}
|
|
463
|
+
// Set counts
|
|
464
|
+
results.files.count = Object.keys(results.files.results).length;
|
|
465
|
+
results.languages.count = Object.keys(results.languages.results).length;
|
|
466
|
+
results.unknown.count = Object.keys({ ...results.unknown.extensions, ...results.unknown.filenames }).length;
|
|
467
|
+
// Return
|
|
468
|
+
return results;
|
|
469
|
+
}
|
|
470
|
+
export default analyse;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import Cache from 'node-cache';
|
|
2
|
+
import FS from 'node:fs';
|
|
3
|
+
import Path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const cache = new Cache({});
|
|
6
|
+
const dirname = Path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
async function loadWebFile(file) {
|
|
8
|
+
// Return cache if it exists
|
|
9
|
+
const cachedContent = cache.get(file);
|
|
10
|
+
if (cachedContent)
|
|
11
|
+
return cachedContent;
|
|
12
|
+
// Otherwise cache the request
|
|
13
|
+
const dataUrl = (file) => `https://raw.githubusercontent.com/github/linguist/HEAD/lib/linguist/${file}`;
|
|
14
|
+
// Load file content, falling back to the local file if the request fails
|
|
15
|
+
const fileContent = await fetch(dataUrl(file))
|
|
16
|
+
.then((data) => data.text())
|
|
17
|
+
.catch(async () => await loadLocalFile(file));
|
|
18
|
+
cache.set(file, fileContent);
|
|
19
|
+
return fileContent;
|
|
20
|
+
}
|
|
21
|
+
async function loadLocalFile(file) {
|
|
22
|
+
const filePath = Path.resolve(dirname, '../../ext', file);
|
|
23
|
+
return FS.promises.readFile(filePath).then((buffer) => buffer.toString());
|
|
24
|
+
}
|
|
25
|
+
/** Nukes unused `generated.rb` file content. */
|
|
26
|
+
export function parseGeneratedDataFile(fileContent) {
|
|
27
|
+
return [...(fileContent.match(/(?<=name\.match\(\/).+?(?=(?<!\\)\/)/gm) ?? [])];
|
|
28
|
+
}
|
|
29
|
+
/** Load a data file from github-linguist. */
|
|
30
|
+
export default function loadFile(file, offline = false) {
|
|
31
|
+
return offline ? loadLocalFile(file) : loadWebFile(file);
|
|
32
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import Path from 'node:path';
|
|
2
|
+
export const normPath = function normalisedPath(...inputPaths) {
|
|
3
|
+
return Path.join(...inputPaths).replace(/\\/g, '/');
|
|
4
|
+
};
|
|
5
|
+
export const normAbsPath = function normalisedAbsolutePath(...inputPaths) {
|
|
6
|
+
return Path.resolve(...inputPaths).replace(/\\/g, '/');
|
|
7
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import FS from 'node:fs';
|
|
2
|
+
/**
|
|
3
|
+
* Read part of a file on disc.
|
|
4
|
+
* @throws 'EPERM' if the file is not readable.
|
|
5
|
+
*/
|
|
6
|
+
export default async function readFileChunk(filename, onlyFirstLine = false) {
|
|
7
|
+
const chunkSize = 100;
|
|
8
|
+
const stream = FS.createReadStream(filename, { highWaterMark: chunkSize });
|
|
9
|
+
let content = '';
|
|
10
|
+
for await (const data of stream) { // may throw
|
|
11
|
+
content += data.toString();
|
|
12
|
+
if (onlyFirstLine && content.includes('\n')) {
|
|
13
|
+
return content.split(/\r?\n/)[0];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return content;
|
|
17
|
+
}
|