codebase-onboarder 0.1.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/LICENSE +21 -0
- package/README.md +275 -0
- package/bin/onboarder.js +14 -0
- package/bin/postinstall.js +6 -0
- package/cli/commands.js +436 -0
- package/cli/main.js +129 -0
- package/cli/prompt.js +101 -0
- package/cli/ui.js +55 -0
- package/cli/wizard.js +325 -0
- package/package.json +48 -0
- package/public/app.js +1339 -0
- package/public/index.html +455 -0
- package/public/js/about.js +284 -0
- package/public/js/aiDraft.js +163 -0
- package/public/js/analysisPanel.js +270 -0
- package/public/js/analysisReport.js +154 -0
- package/public/js/api.js +264 -0
- package/public/js/atlas.js +98 -0
- package/public/js/blameView.js +41 -0
- package/public/js/codeTab.js +256 -0
- package/public/js/codeViewer.js +184 -0
- package/public/js/components/FileChip.js +102 -0
- package/public/js/components/MetricSparkline.js +89 -0
- package/public/js/components/RiskBadge.js +105 -0
- package/public/js/deepAnalysisView.js +496 -0
- package/public/js/diagramPane.js +154 -0
- package/public/js/diffView.js +352 -0
- package/public/js/docsView.js +566 -0
- package/public/js/fileSourceBrowser.js +47 -0
- package/public/js/flameGraph.js +75 -0
- package/public/js/forceGraph.js +592 -0
- package/public/js/heatmap.js +185 -0
- package/public/js/highlight.js +122 -0
- package/public/js/html.js +50 -0
- package/public/js/insightsView.js +236 -0
- package/public/js/inspector.js +535 -0
- package/public/js/llm.js +345 -0
- package/public/js/mapView.js +159 -0
- package/public/js/markdown.js +119 -0
- package/public/js/mindmap.js +289 -0
- package/public/js/repoFiles.js +44 -0
- package/public/js/sbomView.js +142 -0
- package/public/js/scanCache.js +76 -0
- package/public/js/search.js +874 -0
- package/public/js/serverSettings.js +169 -0
- package/public/js/state.js +192 -0
- package/public/js/tour.js +17 -0
- package/public/js/transitions.js +15 -0
- package/public/js/tree.js +200 -0
- package/public/js/workflowsView.js +95 -0
- package/public/styles.css +2767 -0
- package/public/vendor/mermaid.min.js +3587 -0
- package/public/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- package/public/vendor/monaco/vs/base/worker/workerMain.js +31 -0
- package/public/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
- package/public/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/css/css.js +12 -0
- package/public/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
- package/public/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/go/go.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/html/html.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/java/java.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/less/less.js +11 -0
- package/public/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/php/php.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
- package/public/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/python/python.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/r/r.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
- package/public/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/st/st.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
- package/public/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
- package/public/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
- package/public/vendor/monaco/vs/editor/editor.main.css +8 -0
- package/public/vendor/monaco/vs/editor/editor.main.js +798 -0
- package/public/vendor/monaco/vs/language/css/cssMode.js +13 -0
- package/public/vendor/monaco/vs/language/css/cssWorker.js +77 -0
- package/public/vendor/monaco/vs/language/html/htmlMode.js +13 -0
- package/public/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
- package/public/vendor/monaco/vs/language/json/jsonMode.js +19 -0
- package/public/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
- package/public/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
- package/public/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
- package/public/vendor/monaco/vs/loader.js +11 -0
- package/public/vendor/monaco/worker-boot.js +9 -0
- package/server/.fuse_hidden0000000800000001 +36 -0
- package/server/apiDiff.js +25 -0
- package/server/apiDocs.js +67 -0
- package/server/apiFile.js +36 -0
- package/server/apiGitBlame.js +63 -0
- package/server/apiMcp.js +110 -0
- package/server/apiScan.js +119 -0
- package/server/apiSearch.js +253 -0
- package/server/apiSettings.js +118 -0
- package/server/apiTools.js +90 -0
- package/server/config.js +227 -0
- package/server/fileSourceNode.js +44 -0
- package/server/gitClone.js +95 -0
- package/server/gitDiff.js +184 -0
- package/server/gitHistory.js +110 -0
- package/server/htmlText.js +44 -0
- package/server/http.js +55 -0
- package/server/httpGuards.js +87 -0
- package/server/index.js +156 -0
- package/server/llmProxy.js +162 -0
- package/server/logger.js +29 -0
- package/server/mcp/analysis.js +209 -0
- package/server/mcp/http.js +213 -0
- package/server/mcp/runner.js +278 -0
- package/server/mcp/server.js +241 -0
- package/server/mcp/standalone.js +42 -0
- package/server/mcp/tools.js +683 -0
- package/server/paths.js +39 -0
- package/server/router.js +218 -0
- package/server/searchIndex.js +118 -0
- package/server/sessions.js +163 -0
- package/server/static.js +59 -0
- package/server/tools/install.js +246 -0
- package/server/tools/parse.js +170 -0
- package/server/tools/platform.js +91 -0
- package/server/tools/registry.js +212 -0
- package/server/tools/scan.js +136 -0
- package/server/tools.js +212 -0
- package/server/tunnel.js +92 -0
- package/shared/analyzer/docs.js +135 -0
- package/shared/analyzer/explainLocal.js +163 -0
- package/shared/analyzer/graph.js +461 -0
- package/shared/analyzer/health.js +215 -0
- package/shared/analyzer/history.js +146 -0
- package/shared/analyzer/languages/csharp.js +39 -0
- package/shared/analyzer/languages/generic.js +131 -0
- package/shared/analyzer/languages/go.js +70 -0
- package/shared/analyzer/languages/index.js +43 -0
- package/shared/analyzer/languages/java.js +39 -0
- package/shared/analyzer/languages/javascript.js +240 -0
- package/shared/analyzer/languages/python.js +120 -0
- package/shared/analyzer/languages/rust.js +42 -0
- package/shared/analyzer/languages/typescript.js +138 -0
- package/shared/analyzer/licenses.js +151 -0
- package/shared/analyzer/metrics.js +120 -0
- package/shared/analyzer/pathUtil.js +69 -0
- package/shared/analyzer/patterns.js +272 -0
- package/shared/analyzer/scan.js +473 -0
- package/shared/analyzer/security.js +187 -0
- package/shared/analyzer/services.js +157 -0
- package/shared/analyzer/stack.js +173 -0
- package/shared/analyzer/tour.js +69 -0
- package/shared/analyzer/util.js +123 -0
- package/shared/analyzer/workflows.js +143 -0
- package/shared/diagram/aiFacts.js +145 -0
- package/shared/diagram/aiMermaid.js +59 -0
- package/shared/diagram/atlas.js +93 -0
- package/shared/diagram/mermaid.js +459 -0
- package/shared/search/query.js +518 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// JavaScript / TypeScript / JSX / TSX.
|
|
2
|
+
// Regex-based by design: no parser dependency, runs the same in Node and the
|
|
3
|
+
// browser. It will miss dynamic tricks (require(variable), eval'd imports);
|
|
4
|
+
// that is an accepted, documented trade.
|
|
5
|
+
|
|
6
|
+
import { blankComments, lineCounter, braceBodyEnd, uniqueBy, callsWithin, declarationOrder } from '../util.js';
|
|
7
|
+
import { dirOf, joinPath, baseName } from '../pathUtil.js';
|
|
8
|
+
|
|
9
|
+
export const extensions = ['.js', '.jsx', '.mjs', '.cjs'];
|
|
10
|
+
|
|
11
|
+
const IMPORT_RES = [
|
|
12
|
+
/\bimport\s+(?:type\s+)?[\w$*{}\s,]+?\sfrom\s*['"]([^'"\n]+)['"]/g,
|
|
13
|
+
/(?<!['"\w$])import\s*['"]([^'"\n]+)['"]/g,
|
|
14
|
+
/\bexport\s+(?:type\s+)?[\w$*{}\s,]+?\sfrom\s*['"]([^'"\n]+)['"]/g,
|
|
15
|
+
/\brequire\s*\(\s*['"]([^'"\n]+)['"]\s*\)/g,
|
|
16
|
+
/(?<!['"\w$])import\s*\(\s*['"]([^'"\n]+)['"]\s*\)/g,
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const EXPORT_RES = [
|
|
20
|
+
{ re: /\bexport\s+(?:async\s+)?function\s*\*?\s*([\w$]+)/g, kind: 'function' },
|
|
21
|
+
{ re: /\bexport\s+(?:abstract\s+)?class\s+([\w$]+)/g, kind: 'class' },
|
|
22
|
+
{ re: /\bexport\s+(?:const|let|var)\s+([\w$]+)/g, kind: 'const' },
|
|
23
|
+
{ re: /\bexport\s+(?:interface|type|enum)\s+([\w$]+)/g, kind: 'type' },
|
|
24
|
+
{ re: /\bexport\s+default\b/g, kind: 'default', name: 'default' },
|
|
25
|
+
{ re: /\bexports\.([\w$]+)\s*=/g, kind: 'const' },
|
|
26
|
+
{ re: /\bmodule\.exports\s*=/g, kind: 'default', name: 'default' },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const NAMED_EXPORT_RE = /\bexport\s*\{([^}]*)\}/g;
|
|
30
|
+
|
|
31
|
+
const CONTROL_WORDS = new Set([
|
|
32
|
+
'if', 'for', 'while', 'switch', 'catch', 'return', 'else', 'do', 'new',
|
|
33
|
+
'typeof', 'case', 'throw', 'delete', 'void', 'in', 'of', 'yield', 'await',
|
|
34
|
+
'function', 'constructor', 'class',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const FUNCTION_RES = [
|
|
38
|
+
{ re: /\bfunction\s*\*?\s*([\w$]+)\s*\(/g, kind: 'function' },
|
|
39
|
+
{ re: /\b(?:const|let|var)\s+([\w$]+)\s*=\s*(?:async\s+)?function/g, kind: 'function' },
|
|
40
|
+
{ re: /\b(?:const|let|var)\s+([\w$]+)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[\w$]+)\s*=>/g, kind: 'arrow' },
|
|
41
|
+
{
|
|
42
|
+
re: /^[ \t]*(?:export\s+)?(?:(?:public|private|protected|static|async|override|readonly|abstract|get|set)\s+)*([\w$]+)\s*\([^;{]*\)\s*(?::[\w$<>[\]|&., ]+)?\s*\{/gm,
|
|
43
|
+
kind: 'method',
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const CLASS_RE = /\bclass\s+([\w$]+)/g;
|
|
48
|
+
|
|
49
|
+
function extractClauseSymbols(clause) {
|
|
50
|
+
const symbols = [];
|
|
51
|
+
const c = clause.trim();
|
|
52
|
+
if (!c) return symbols;
|
|
53
|
+
if (c.startsWith('*')) return ['*'];
|
|
54
|
+
|
|
55
|
+
const braceStart = c.indexOf('{');
|
|
56
|
+
if (braceStart === -1) {
|
|
57
|
+
const defMatch = c.match(/^[\w$]+/);
|
|
58
|
+
if (defMatch && defMatch[0] !== 'type') return ['default'];
|
|
59
|
+
return symbols;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const beforeBrace = c.slice(0, braceStart).trim().replace(/,$/, '').trim();
|
|
63
|
+
if (beforeBrace && beforeBrace !== 'type') {
|
|
64
|
+
symbols.push('default');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const braceEnd = c.lastIndexOf('}');
|
|
68
|
+
if (braceEnd > braceStart) {
|
|
69
|
+
const inside = c.slice(braceStart + 1, braceEnd);
|
|
70
|
+
for (const part of inside.split(',')) {
|
|
71
|
+
let t = part.trim();
|
|
72
|
+
if (!t) continue;
|
|
73
|
+
if (t.startsWith('type ')) t = t.slice(5).trim();
|
|
74
|
+
const asMatch = t.match(/^([\w$]+)\s+as\s+[\w$]+/);
|
|
75
|
+
const name = asMatch ? asMatch[1] : (t.match(/^[\w$]+/) || [])[0];
|
|
76
|
+
if (name) symbols.push(name);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return symbols;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function analyze(source, path) {
|
|
83
|
+
const clean = blankComments(source);
|
|
84
|
+
const lineAt = lineCounter(clean);
|
|
85
|
+
|
|
86
|
+
const imports = [];
|
|
87
|
+
for (const m of clean.matchAll(/\bimport\s+(?:type\s+)?([\w$*{}\s,]+?)\s+from\s*['"]([^'"\n]+)['"]/g)) {
|
|
88
|
+
imports.push({ spec: m[2], symbols: extractClauseSymbols(m[1]), line: lineAt(m.index) });
|
|
89
|
+
}
|
|
90
|
+
for (const m of clean.matchAll(/\bexport\s+(?:type\s+)?([\w$*{}\s,]+?)\s+from\s*['"]([^'"\n]+)['"]/g)) {
|
|
91
|
+
imports.push({ spec: m[2], symbols: extractClauseSymbols(m[1]), line: lineAt(m.index) });
|
|
92
|
+
}
|
|
93
|
+
for (const m of clean.matchAll(/(?<!['"\w$])import\s*['"]([^'"\n]+)['"]/g)) {
|
|
94
|
+
imports.push({ spec: m[1], symbols: [], line: lineAt(m.index) });
|
|
95
|
+
}
|
|
96
|
+
for (const m of clean.matchAll(/\brequire\s*\(\s*['"]([^'"\n]+)['"]\s*\)/g)) {
|
|
97
|
+
imports.push({ spec: m[1], symbols: [], line: lineAt(m.index) });
|
|
98
|
+
}
|
|
99
|
+
for (const m of clean.matchAll(/(?<!['"\w$])import\s*\(\s*['"]([^'"\n]+)['"]\s*\)/g)) {
|
|
100
|
+
imports.push({ spec: m[1], symbols: [], line: lineAt(m.index) });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const exports = [];
|
|
104
|
+
for (const { re, kind, name } of EXPORT_RES) {
|
|
105
|
+
for (const m of clean.matchAll(re)) {
|
|
106
|
+
exports.push({ name: name ?? m[1], kind });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const m of clean.matchAll(NAMED_EXPORT_RE)) {
|
|
110
|
+
for (const part of m[1].split(',')) {
|
|
111
|
+
const t = part.trim();
|
|
112
|
+
if (!t || (/^type\b/.test(t) && !/[\w$]/.test(t.replace(/^type\s+/, '')))) continue;
|
|
113
|
+
const asMatch = t.match(/(?:^|\s)as\s+([\w$]+)$/);
|
|
114
|
+
const name = asMatch ? asMatch[1] : (t.match(/^([\w$]+)/) || [])[1];
|
|
115
|
+
if (name) exports.push({ name, kind: 'named' });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const functions = [];
|
|
120
|
+
for (const { re, kind } of FUNCTION_RES) {
|
|
121
|
+
for (const m of clean.matchAll(re)) {
|
|
122
|
+
const name = m[1];
|
|
123
|
+
if (CONTROL_WORDS.has(name)) continue;
|
|
124
|
+
functions.push({ name, kind, line: lineAt(m.index), index: m.index, len: m[0].length });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const classes = [];
|
|
129
|
+
for (const m of clean.matchAll(CLASS_RE)) {
|
|
130
|
+
classes.push({ name: m[1], line: lineAt(m.index) });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Best-effort intra-file call graph: find each function's body, then look
|
|
134
|
+
// for calls to other functions declared in the same file.
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
const fns = [];
|
|
137
|
+
for (const f of functions) {
|
|
138
|
+
const key = f.name + ':' + f.line;
|
|
139
|
+
if (seen.has(key)) continue;
|
|
140
|
+
seen.add(key);
|
|
141
|
+
fns.push(f);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const calls = [];
|
|
145
|
+
const names = new Set(fns.map((f) => f.name));
|
|
146
|
+
const declaredAt = declarationOrder(fns);
|
|
147
|
+
for (const fn of fns) {
|
|
148
|
+
const body = functionBody(clean, fn);
|
|
149
|
+
if (!body) continue;
|
|
150
|
+
const called = callsWithin(body, names)
|
|
151
|
+
.filter((name) => name !== fn.name)
|
|
152
|
+
.sort((a, b) => declaredAt.get(a) - declaredAt.get(b));
|
|
153
|
+
for (const name of called) calls.push({ from: fn.name, to: name });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
imports: uniqueBy(imports, (i) => i.spec + ':' + i.line),
|
|
158
|
+
exports: uniqueBy(exports, (e) => e.name),
|
|
159
|
+
functions: fns.map(({ index, len, ...rest }) => rest),
|
|
160
|
+
classes,
|
|
161
|
+
calls: uniqueBy(calls, (c) => c.from + '->' + c.to),
|
|
162
|
+
hasMain: false,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function functionBody(clean, fn) {
|
|
167
|
+
const open = clean.indexOf('{', fn.index + fn.len - 1);
|
|
168
|
+
if (open === -1 || open > fn.index + fn.len + 400) return null;
|
|
169
|
+
return clean.slice(open, braceBodyEnd(clean, open));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function packageNameOf(spec) {
|
|
173
|
+
if (spec.startsWith('node:')) return spec.slice(5);
|
|
174
|
+
const parts = spec.split('/');
|
|
175
|
+
return spec.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function resolveImport(spec, fromPath, has, context = {}) {
|
|
179
|
+
const tryResolve = (base) => {
|
|
180
|
+
if (has(base)) return { path: base };
|
|
181
|
+
const allExts = [...extensions, '.ts', '.tsx'];
|
|
182
|
+
for (const ext of allExts) {
|
|
183
|
+
if (has(base + ext)) return { path: base + ext };
|
|
184
|
+
}
|
|
185
|
+
for (const ext of allExts) {
|
|
186
|
+
const idx = base + '/index' + ext;
|
|
187
|
+
if (has(idx)) return { path: idx };
|
|
188
|
+
}
|
|
189
|
+
const asTs = base.replace(/\.jsx?$/, '');
|
|
190
|
+
if (asTs !== base) {
|
|
191
|
+
for (const ext of ['.ts', '.tsx']) {
|
|
192
|
+
if (has(asTs + ext)) return { path: asTs + ext };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
if (spec.startsWith('.') || spec.startsWith('/')) {
|
|
199
|
+
const base = spec.startsWith('/')
|
|
200
|
+
? joinPath('', spec)
|
|
201
|
+
: joinPath(dirOf(fromPath), spec);
|
|
202
|
+
const hit = tryResolve(base);
|
|
203
|
+
if (hit) return hit;
|
|
204
|
+
return { unresolved: spec };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// tsconfig.json / jsconfig.json paths aliases
|
|
208
|
+
if (context.tsPaths) {
|
|
209
|
+
const { baseUrl = '', paths = {} } = context.tsPaths;
|
|
210
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
211
|
+
const starIdx = pattern.indexOf('*');
|
|
212
|
+
if (starIdx !== -1) {
|
|
213
|
+
const prefix = pattern.slice(0, starIdx);
|
|
214
|
+
const suffix = pattern.slice(starIdx + 1);
|
|
215
|
+
if (spec.startsWith(prefix) && spec.endsWith(suffix)) {
|
|
216
|
+
const matchWildcard = spec.slice(prefix.length, spec.length - suffix.length);
|
|
217
|
+
for (const target of targets) {
|
|
218
|
+
const resolvedTarget = target.replace('*', matchWildcard);
|
|
219
|
+
const candBase = joinPath(baseUrl, resolvedTarget);
|
|
220
|
+
const hit = tryResolve(candBase);
|
|
221
|
+
if (hit) return hit;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} else if (spec === pattern) {
|
|
225
|
+
for (const target of targets) {
|
|
226
|
+
const candBase = joinPath(baseUrl, target);
|
|
227
|
+
const hit = tryResolve(candBase);
|
|
228
|
+
if (hit) return hit;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (baseUrl) {
|
|
233
|
+
const candBase = joinPath(baseUrl, spec);
|
|
234
|
+
const hit = tryResolve(candBase);
|
|
235
|
+
if (hit) return hit;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return { external: packageNameOf(spec) };
|
|
240
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Python. Line-oriented, indentation-aware enough for our purposes.
|
|
2
|
+
|
|
3
|
+
import { blankComments, lineCounter, uniqueBy, callsWithin, declarationOrder } from '../util.js';
|
|
4
|
+
import { dirOf, joinPath } from '../pathUtil.js';
|
|
5
|
+
|
|
6
|
+
export const extensions = ['.py'];
|
|
7
|
+
|
|
8
|
+
const IMPORT_FROM_RE = /^[ \t]*from\s+([.\w]+)\s+import\s+/gm;
|
|
9
|
+
const IMPORT_PLAIN_RE = /^[ \t]*import\s+([\w.,\s]+?)\s*(?:#.*)?$/gm;
|
|
10
|
+
const DEF_RE = /^([ \t]*)(?:async\s+)?def\s+([\w]+)\s*\(/gm;
|
|
11
|
+
const CLASS_RE = /^([ \t]*)class\s+([\w]+)/gm;
|
|
12
|
+
|
|
13
|
+
export function analyze(source, path) {
|
|
14
|
+
const clean = blankComments(source, { block: false, lineChar: '#' });
|
|
15
|
+
const lineAt = lineCounter(clean);
|
|
16
|
+
|
|
17
|
+
const imports = [];
|
|
18
|
+
for (const m of clean.matchAll(IMPORT_FROM_RE)) {
|
|
19
|
+
imports.push({ spec: m[1], kind: 'from', line: lineAt(m.index) });
|
|
20
|
+
}
|
|
21
|
+
for (const m of clean.matchAll(IMPORT_PLAIN_RE)) {
|
|
22
|
+
for (let part of m[1].split(',')) {
|
|
23
|
+
part = part.trim().split(/\s+as\s+/)[0].trim();
|
|
24
|
+
if (part) imports.push({ spec: part, kind: 'import', line: lineAt(m.index) });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const functions = [];
|
|
29
|
+
for (const m of clean.matchAll(DEF_RE)) {
|
|
30
|
+
const indent = m[1].length;
|
|
31
|
+
functions.push({
|
|
32
|
+
name: m[2],
|
|
33
|
+
kind: indent > 0 ? 'method' : 'function',
|
|
34
|
+
line: lineAt(m.index),
|
|
35
|
+
indent,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const classes = [];
|
|
40
|
+
for (const m of clean.matchAll(CLASS_RE)) {
|
|
41
|
+
classes.push({ name: m[2], line: lineAt(m.index) });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Calls: a function's body is the run of lines more indented than its def.
|
|
45
|
+
const lines = clean.split('\n');
|
|
46
|
+
const calls = [];
|
|
47
|
+
const topLevel = functions.filter((f) => f.kind === 'function');
|
|
48
|
+
const names = new Set(functions.map((f) => f.name));
|
|
49
|
+
const declaredAt = declarationOrder(functions);
|
|
50
|
+
for (const fn of functions) {
|
|
51
|
+
const body = indentedBody(lines, fn.line - 1, fn.indent);
|
|
52
|
+
if (!body) continue;
|
|
53
|
+
const called = callsWithin(body, names)
|
|
54
|
+
.filter((name) => name !== fn.name)
|
|
55
|
+
.sort((a, b) => declaredAt.get(a) - declaredAt.get(b));
|
|
56
|
+
for (const name of called) calls.push({ from: fn.name, to: name });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const exports = topLevel
|
|
60
|
+
.filter((f) => !f.name.startsWith('_'))
|
|
61
|
+
.map((f) => ({ name: f.name, kind: 'function' }))
|
|
62
|
+
.concat(classes.filter((c) => !c.name.startsWith('_')).map((c) => ({ name: c.name, kind: 'class' })));
|
|
63
|
+
|
|
64
|
+
const hasMain = /__name__\s*==\s*['"]__main__['"]/.test(clean);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
imports: uniqueBy(imports, (i) => i.spec + ':' + i.line),
|
|
68
|
+
exports: uniqueBy(exports, (e) => e.name),
|
|
69
|
+
functions: functions.map(({ indent, ...rest }) => rest),
|
|
70
|
+
classes,
|
|
71
|
+
calls: uniqueBy(calls, (c) => c.from + '->' + c.to),
|
|
72
|
+
hasMain,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function indentedBody(lines, defLineIdx, indent) {
|
|
77
|
+
const out = [];
|
|
78
|
+
for (let i = defLineIdx + 1; i < lines.length; i++) {
|
|
79
|
+
const line = lines[i];
|
|
80
|
+
if (line.trim() === '') {
|
|
81
|
+
out.push('');
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const lead = line.length - line.trimStart().length;
|
|
85
|
+
if (lead <= indent) break;
|
|
86
|
+
out.push(line);
|
|
87
|
+
}
|
|
88
|
+
return out.join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function packageNameOf(spec) {
|
|
92
|
+
return spec.replace(/^\.+/, '').split('.')[0] || spec;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function resolveImport(spec, fromPath, has, context = {}) {
|
|
96
|
+
const fromDir = dirOf(fromPath);
|
|
97
|
+
|
|
98
|
+
if (spec.startsWith('.')) {
|
|
99
|
+
// Relative import: one dot = current package, two = parent, and so on.
|
|
100
|
+
const dots = spec.match(/^\.+/)[0].length;
|
|
101
|
+
let base = fromDir;
|
|
102
|
+
for (let i = 1; i < dots; i++) base = dirOf(base);
|
|
103
|
+
const rest = spec.slice(dots).replace(/\./g, '/');
|
|
104
|
+
const target = rest ? joinPath(base, rest) : base;
|
|
105
|
+
for (const cand of [target + '.py', target + '/__init__.py']) {
|
|
106
|
+
if (has(cand)) return { path: cand };
|
|
107
|
+
}
|
|
108
|
+
return { unresolved: spec };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Absolute import: try from repo root, then relative to the file (a common
|
|
112
|
+
// layout in small scripts), then give up and call it external.
|
|
113
|
+
const rel = spec.replace(/\./g, '/');
|
|
114
|
+
for (const root of ['', fromDir]) {
|
|
115
|
+
for (const cand of [joinPath(root, rel) + '.py', joinPath(root, rel) + '/__init__.py']) {
|
|
116
|
+
if (has(cand)) return { path: cand };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { external: spec.split('.')[0] };
|
|
120
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Rust analyzer
|
|
2
|
+
import { blankComments, lineCounter, uniqueBy } from '../util.js';
|
|
3
|
+
|
|
4
|
+
export const extensions = ['.rs'];
|
|
5
|
+
|
|
6
|
+
export function analyze(source, path) {
|
|
7
|
+
const clean = blankComments(source);
|
|
8
|
+
const lineAt = lineCounter(clean);
|
|
9
|
+
|
|
10
|
+
const imports = [];
|
|
11
|
+
for (const m of clean.matchAll(/\b(?:use|mod)\s+([a-zA-Z0-9_:]+)/g)) {
|
|
12
|
+
imports.push({ spec: m[1], line: lineAt(m.index) });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const functions = [];
|
|
16
|
+
for (const m of clean.matchAll(/\b(pub\s+)?fn\s+([a-zA-Z0-9_]+)/g)) {
|
|
17
|
+
functions.push({ name: m[2], pub: !!m[1], line: lineAt(m.index) });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const classes = [];
|
|
21
|
+
for (const m of clean.matchAll(/\b(pub\s+)?(?:struct|enum|trait)\s+([a-zA-Z0-9_]+)/g)) {
|
|
22
|
+
classes.push({ name: m[2], pub: !!m[1], line: lineAt(m.index) });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const exports = [];
|
|
26
|
+
for (const f of functions) if (f.pub) exports.push({ name: f.name, kind: 'function' });
|
|
27
|
+
for (const c of classes) if (c.pub) exports.push({ name: c.name, kind: 'class' });
|
|
28
|
+
|
|
29
|
+
const hasMain = /\bfn\s+main\s*\(/.test(clean);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
imports: uniqueBy(imports, i => i.spec + ':' + i.line),
|
|
33
|
+
exports,
|
|
34
|
+
functions,
|
|
35
|
+
classes,
|
|
36
|
+
hasMain
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveImport(spec, fromPath, has, context = {}) {
|
|
41
|
+
return { unresolved: spec };
|
|
42
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// TypeScript / TSX analyzer.
|
|
2
|
+
// Extracts interfaces, type aliases, enums, decorators, and generic arities,
|
|
3
|
+
// while preserving clause symbols and resolving imports according to tsconfig.json.
|
|
4
|
+
|
|
5
|
+
import { blankComments, lineCounter, braceBodyEnd, uniqueBy } from '../util.js';
|
|
6
|
+
import { dirOf, joinPath, baseName } from '../pathUtil.js';
|
|
7
|
+
import { resolveImport, packageNameOf } from './javascript.js';
|
|
8
|
+
|
|
9
|
+
export const extensions = ['.ts', '.tsx'];
|
|
10
|
+
|
|
11
|
+
const IMPORT_RES = [
|
|
12
|
+
/\bimport\s+(?:type\s+)?([\w$*{}\s,]+?)\s+from\s*['"]([^'"\n]+)['"]/g,
|
|
13
|
+
/\bexport\s+(?:type\s+)?([\w$*{}\s,]+?)\s+from\s*['"]([^'"\n]+)['"]/g,
|
|
14
|
+
/(?<!['"\w$])import\s*['"]([^'"\n]+)['"]/g,
|
|
15
|
+
/\brequire\s*\(\s*['"]([^'"\n]+)['"]\s*\)/g,
|
|
16
|
+
/(?<!['"\w$])import\s*\(\s*['"]([^'"\n]+)['"]\s*\)/g,
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const EXPORT_RES = [
|
|
20
|
+
{ re: /\bexport\s+(?:async\s+)?function\s*\*?\s*([\w$]+)/g, kind: 'function' },
|
|
21
|
+
{ re: /\bexport\s+(?:abstract\s+)?class\s+([\w$]+)/g, kind: 'class' },
|
|
22
|
+
{ re: /\bexport\s+(?:const|let|var)\s+([\w$]+)/g, kind: 'const' },
|
|
23
|
+
{ re: /\bexport\s+(?:interface|type|enum)\s+([\w$]+)/g, kind: 'type' },
|
|
24
|
+
{ re: /\bexport\s+default\b/g, kind: 'default', name: 'default' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const NAMED_EXPORT_RE = /\bexport\s*\{([^}]*)\}/g;
|
|
28
|
+
const DECORATOR_RE = /@([\w$]+)(?:\s*\()?/g;
|
|
29
|
+
const GENERIC_RE = /<([A-Za-z0-9_$,\s]+)>/;
|
|
30
|
+
|
|
31
|
+
function extractClauseSymbols(clause) {
|
|
32
|
+
const symbols = [];
|
|
33
|
+
const c = clause.trim();
|
|
34
|
+
if (!c) return symbols;
|
|
35
|
+
if (c.startsWith('*')) return ['*'];
|
|
36
|
+
|
|
37
|
+
const braceStart = c.indexOf('{');
|
|
38
|
+
if (braceStart === -1) {
|
|
39
|
+
const defMatch = c.match(/^[\w$]+/);
|
|
40
|
+
if (defMatch && defMatch[0] !== 'type') return ['default'];
|
|
41
|
+
return symbols;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const beforeBrace = c.slice(0, braceStart).trim().replace(/,$/, '').trim();
|
|
45
|
+
if (beforeBrace && beforeBrace !== 'type') {
|
|
46
|
+
symbols.push('default');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const braceEnd = c.lastIndexOf('}');
|
|
50
|
+
if (braceEnd > braceStart) {
|
|
51
|
+
const inside = c.slice(braceStart + 1, braceEnd);
|
|
52
|
+
for (const part of inside.split(',')) {
|
|
53
|
+
let t = part.trim();
|
|
54
|
+
if (!t) continue;
|
|
55
|
+
if (t.startsWith('type ')) t = t.slice(5).trim();
|
|
56
|
+
const asMatch = t.match(/^([\w$]+)\s+as\s+[\w$]+/);
|
|
57
|
+
const name = asMatch ? asMatch[1] : (t.match(/^[\w$]+/) || [])[0];
|
|
58
|
+
if (name) symbols.push(name);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return symbols;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function analyze(source, path) {
|
|
65
|
+
const clean = blankComments(source);
|
|
66
|
+
const lineAt = lineCounter(clean);
|
|
67
|
+
|
|
68
|
+
const imports = [];
|
|
69
|
+
for (const m of clean.matchAll(/\bimport\s+(?:type\s+)?([\w$*{}\s,]+?)\s+from\s*['"]([^'"\n]+)['"]/g)) {
|
|
70
|
+
imports.push({ spec: m[2], symbols: extractClauseSymbols(m[1]), line: lineAt(m.index) });
|
|
71
|
+
}
|
|
72
|
+
for (const m of clean.matchAll(/\bexport\s+(?:type\s+)?([\w$*{}\s,]+?)\s+from\s*['"]([^'"\n]+)['"]/g)) {
|
|
73
|
+
imports.push({ spec: m[2], symbols: extractClauseSymbols(m[1]), line: lineAt(m.index) });
|
|
74
|
+
}
|
|
75
|
+
for (const m of clean.matchAll(/(?<!['"\w$])import\s*['"]([^'"\n]+)['"]/g)) {
|
|
76
|
+
imports.push({ spec: m[1], symbols: [], line: lineAt(m.index) });
|
|
77
|
+
}
|
|
78
|
+
for (const m of clean.matchAll(/\brequire\s*\(\s*['"]([^'"\n]+)['"]\s*\)/g)) {
|
|
79
|
+
imports.push({ spec: m[1], symbols: [], line: lineAt(m.index) });
|
|
80
|
+
}
|
|
81
|
+
for (const m of clean.matchAll(/(?<!['"\w$])import\s*\(\s*['"]([^'"\n]+)['"]\s*\)/g)) {
|
|
82
|
+
imports.push({ spec: m[1], symbols: [], line: lineAt(m.index) });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const exports = [];
|
|
86
|
+
for (const { re, kind, name } of EXPORT_RES) {
|
|
87
|
+
for (const m of clean.matchAll(re)) {
|
|
88
|
+
exports.push({ name: name ?? m[1], kind });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const m of clean.matchAll(NAMED_EXPORT_RE)) {
|
|
92
|
+
for (const part of m[1].split(',')) {
|
|
93
|
+
const t = part.trim();
|
|
94
|
+
if (!t || (/^type\b/.test(t) && !/[\w$]/.test(t.replace(/^type\s+/, '')))) continue;
|
|
95
|
+
const asMatch = t.match(/(?:^|\s)as\s+([\w$]+)$/);
|
|
96
|
+
const name = asMatch ? asMatch[1] : (t.match(/^([\w$]+)/) || [])[1];
|
|
97
|
+
if (name) exports.push({ name, kind: 'named' });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const functions = [];
|
|
102
|
+
for (const m of clean.matchAll(/\bfunction\s+([\w$]+)/g)) {
|
|
103
|
+
functions.push({ name: m[1], kind: 'function', line: lineAt(m.index) });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const classes = [];
|
|
107
|
+
for (const m of clean.matchAll(/\bclass\s+([\w$]+)([^{]*)\{/g)) {
|
|
108
|
+
const name = m[1];
|
|
109
|
+
const signature = m[2] || '';
|
|
110
|
+
const decs = [];
|
|
111
|
+
const beforeClass = clean.slice(Math.max(0, m.index - 200), m.index);
|
|
112
|
+
for (const dm of beforeClass.matchAll(DECORATOR_RE)) {
|
|
113
|
+
decs.push(dm[1]);
|
|
114
|
+
}
|
|
115
|
+
const genMatch = signature.match(GENERIC_RE);
|
|
116
|
+
const arity = genMatch ? genMatch[1].split(',').length : 0;
|
|
117
|
+
|
|
118
|
+
classes.push({ name, decorators: decs, genericArity: arity, line: lineAt(m.index) });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const interfaces = [];
|
|
122
|
+
for (const m of clean.matchAll(/\b(?:interface|type|enum)\s+([\w$]+)/g)) {
|
|
123
|
+
interfaces.push({ name: m[1], line: lineAt(m.index) });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const hasMain = /if\s*\(\s*require\.main\s*===\s*module\s*\)|import\.meta\.main/g.test(clean);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
imports: uniqueBy(imports, (i) => i.spec + ':' + i.line),
|
|
130
|
+
exports: uniqueBy(exports, (e) => e.name),
|
|
131
|
+
functions,
|
|
132
|
+
classes,
|
|
133
|
+
interfaces,
|
|
134
|
+
hasMain,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { resolveImport, packageNameOf };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// License detection and Software Bill of Materials (SBOM) compliance analysis.
|
|
2
|
+
// Pure isomorphic analyzer — works in Node and browser.
|
|
3
|
+
|
|
4
|
+
const KNOWN_LICENSES = [
|
|
5
|
+
{ id: 'MIT', name: 'MIT License', type: 'permissive', rx: /\bMIT\b|Permission is hereby granted, free of charge/i },
|
|
6
|
+
{ id: 'Apache-2.0', name: 'Apache License 2.0', type: 'permissive', rx: /Apache License,\s*Version 2\.0|http:\/\/www\.apache\.org\/licenses\/LICENSE-2\.0/i },
|
|
7
|
+
{ id: 'BSD-3-Clause', name: 'BSD 3-Clause "New" or "Revised" License', type: 'permissive', rx: /Redistribution and use in source and binary forms[\s\S]*?Neither the name of[\s\S]*?nor the names of its contributors/i },
|
|
8
|
+
{ id: 'BSD-2-Clause', name: 'BSD 2-Clause "Simplified" License', type: 'permissive', rx: /Redistribution and use in source and binary forms[\s\S]*?Redistributions in binary form must reproduce the above copyright/i },
|
|
9
|
+
{ id: 'ISC', name: 'ISC License', type: 'permissive', rx: /Permission to use, copy, modify, and\/or distribute this software for any purpose/i },
|
|
10
|
+
{ id: 'GPL-3.0', name: 'GNU General Public License v3.0', type: 'copyleft', rx: /GNU GENERAL PUBLIC LICENSE\s+Version 3|GPL-3\.0/i },
|
|
11
|
+
{ id: 'GPL-2.0', name: 'GNU General Public License v2.0', type: 'copyleft', rx: /GNU GENERAL PUBLIC LICENSE\s+Version 2|GPL-2\.0/i },
|
|
12
|
+
{ id: 'AGPL-3.0', name: 'GNU Affero General Public License v3.0', type: 'copyleft', rx: /GNU AFFERO GENERAL PUBLIC LICENSE/i },
|
|
13
|
+
{ id: 'LGPL-3.0', name: 'GNU Lesser General Public License v3.0', type: 'weak-copyleft', rx: /GNU LESSER GENERAL PUBLIC LICENSE\s+Version 3/i },
|
|
14
|
+
{ id: 'MPL-2.0', name: 'Mozilla Public License 2.0', type: 'weak-copyleft', rx: /Mozilla Public License,?\s*v\.?\s*2\.0/i },
|
|
15
|
+
{ id: 'Unlicense', name: 'The Unlicense', type: 'public-domain', rx: /This is free and unencumbered software released into the public domain/i },
|
|
16
|
+
{ id: 'CC0-1.0', name: 'Creative Commons Zero v1.0 Universal', type: 'public-domain', rx: /CC0 1\.0 Universal|Public Domain Dedication/i },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export function detectLicenseFromText(text) {
|
|
20
|
+
if (!text || typeof text !== 'string') return null;
|
|
21
|
+
for (const lic of KNOWN_LICENSES) {
|
|
22
|
+
if (lic.rx.test(text)) {
|
|
23
|
+
return { id: lic.id, name: lic.name, type: lic.type };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function classifyLicenseType(licenseId) {
|
|
30
|
+
if (!licenseId || licenseId === 'UNKNOWN') return 'unknown';
|
|
31
|
+
const id = String(licenseId).toUpperCase();
|
|
32
|
+
if (/MIT|APACHE|BSD|ISC|UNLICENSE|CC0|ZLIB|WTFPL/.test(id)) return 'permissive';
|
|
33
|
+
if (/LGPL|MPL|EPL/.test(id)) return 'weak-copyleft';
|
|
34
|
+
if (/AGPL|GPL-2|GPL-3|GPLV2|GPLV3|GPL/.test(id)) return 'copyleft';
|
|
35
|
+
return 'custom';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function analyzeLicenses(source, manifest = {}) {
|
|
39
|
+
let projectLicense = null;
|
|
40
|
+
|
|
41
|
+
// 1. Check root LICENSE / COPYING / LICENSE.md files
|
|
42
|
+
const rootFiles = await source.list('').catch(() => []);
|
|
43
|
+
const licFile = rootFiles.find((f) => /^LICENSE|^COPYING|^UNLICENSE/i.test(f.name));
|
|
44
|
+
if (licFile) {
|
|
45
|
+
const content = await source.read(licFile.path).catch(() => '');
|
|
46
|
+
projectLicense = detectLicenseFromText(content) || {
|
|
47
|
+
id: 'Custom',
|
|
48
|
+
name: licFile.name,
|
|
49
|
+
type: 'custom',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. Fallback to manifest.license from package.json
|
|
54
|
+
if (!projectLicense && manifest.license) {
|
|
55
|
+
const licId = manifest.license;
|
|
56
|
+
projectLicense = {
|
|
57
|
+
id: licId,
|
|
58
|
+
name: licId,
|
|
59
|
+
type: classifyLicenseType(licId),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 3. Build SBOM entries from manifest dependencies
|
|
64
|
+
const sbom = [];
|
|
65
|
+
const addDep = (name, version, ecosystem, dev = false) => {
|
|
66
|
+
sbom.push({
|
|
67
|
+
name,
|
|
68
|
+
version: version || '*',
|
|
69
|
+
ecosystem,
|
|
70
|
+
isDev: dev,
|
|
71
|
+
license: inferKnownDepLicense(name, ecosystem),
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const npmDeps = manifest.deps?.npm || {};
|
|
76
|
+
for (const [name, ver] of Object.entries(npmDeps)) {
|
|
77
|
+
addDep(name, ver, 'npm', false);
|
|
78
|
+
}
|
|
79
|
+
const devDeps = manifest.deps?.dev || {};
|
|
80
|
+
for (const [name, ver] of Object.entries(devDeps)) {
|
|
81
|
+
addDep(name, ver, 'npm', true);
|
|
82
|
+
}
|
|
83
|
+
for (const name of manifest.deps?.pip || []) {
|
|
84
|
+
addDep(name, '*', 'pip', false);
|
|
85
|
+
}
|
|
86
|
+
for (const name of manifest.deps?.go || []) {
|
|
87
|
+
addDep(name, '*', 'go', false);
|
|
88
|
+
}
|
|
89
|
+
for (const name of manifest.deps?.cargo || []) {
|
|
90
|
+
addDep(name, '*', 'cargo', false);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const counts = { permissive: 0, copyleft: 0, weakCopyleft: 0, unknown: 0, total: sbom.length };
|
|
94
|
+
for (const item of sbom) {
|
|
95
|
+
const type = item.license.type;
|
|
96
|
+
if (type === 'permissive') counts.permissive++;
|
|
97
|
+
else if (type === 'copyleft') counts.copyleft++;
|
|
98
|
+
else if (type === 'weak-copyleft') counts.weakCopyleft++;
|
|
99
|
+
else counts.unknown++;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const hasCopyleft = counts.copyleft > 0;
|
|
103
|
+
const complianceStatus = hasCopyleft && projectLicense?.type === 'permissive'
|
|
104
|
+
? 'warning'
|
|
105
|
+
: 'clean';
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
projectLicense: projectLicense || { id: 'UNLICENSED', name: 'None detected', type: 'unknown' },
|
|
109
|
+
sbom,
|
|
110
|
+
counts,
|
|
111
|
+
complianceStatus,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Inferred licenses for standard popular open-source dependencies
|
|
116
|
+
const POPULAR_LICENSES = {
|
|
117
|
+
react: 'MIT',
|
|
118
|
+
'react-dom': 'MIT',
|
|
119
|
+
vue: 'MIT',
|
|
120
|
+
express: 'MIT',
|
|
121
|
+
lodash: 'MIT',
|
|
122
|
+
axios: 'MIT',
|
|
123
|
+
typescript: 'Apache-2.0',
|
|
124
|
+
next: 'MIT',
|
|
125
|
+
tailwindcss: 'MIT',
|
|
126
|
+
eslint: 'MIT',
|
|
127
|
+
prettier: 'MIT',
|
|
128
|
+
mocha: 'MIT',
|
|
129
|
+
jest: 'MIT',
|
|
130
|
+
vite: 'MIT',
|
|
131
|
+
fastapi: 'MIT',
|
|
132
|
+
django: 'BSD-3-Clause',
|
|
133
|
+
flask: 'BSD-3-Clause',
|
|
134
|
+
requests: 'Apache-2.0',
|
|
135
|
+
pytest: 'MIT',
|
|
136
|
+
numpy: 'BSD-3-Clause',
|
|
137
|
+
pandas: 'BSD-3-Clause',
|
|
138
|
+
tokio: 'MIT',
|
|
139
|
+
serde: 'MIT / Apache-2.0',
|
|
140
|
+
gin: 'MIT',
|
|
141
|
+
fiber: 'MIT',
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
function inferKnownDepLicense(name, ecosystem) {
|
|
145
|
+
const normName = name.toLowerCase().replace(/^@[^/]+\//, '');
|
|
146
|
+
const id = POPULAR_LICENSES[name] || POPULAR_LICENSES[normName] || 'Permissive (Inferred)';
|
|
147
|
+
return {
|
|
148
|
+
id,
|
|
149
|
+
type: classifyLicenseType(id),
|
|
150
|
+
};
|
|
151
|
+
}
|