axmap-cli 0.0.1
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/.claude/commands/ax-done.md +10 -0
- package/.claude/commands/ax-setup.md +31 -0
- package/.claude/commands/ax-start.md +15 -0
- package/.claude/commands/ax-tell.md +14 -0
- package/.claude/commands/ax-update.md +19 -0
- package/.claude/commands/ax.md +13 -0
- package/CLAUDE.md +309 -0
- package/LICENSE +20 -0
- package/README.md +207 -0
- package/app/README.md +366 -0
- package/app/eval/edges.mjs +242 -0
- package/app/lib/adjacent.mjs +125 -0
- package/app/lib/agentcli.mjs +153 -0
- package/app/lib/analyze.mjs +1159 -0
- package/app/lib/cochange.mjs +421 -0
- package/app/lib/datanodes.mjs +127 -0
- package/app/lib/entry.mjs +192 -0
- package/app/lib/featuregraph.mjs +389 -0
- package/app/lib/features.mjs +645 -0
- package/app/lib/fetchrepo-run.mjs +37 -0
- package/app/lib/fetchrepo.mjs +164 -0
- package/app/lib/flow.mjs +1089 -0
- package/app/lib/ladder.mjs +387 -0
- package/app/lib/langs.mjs +630 -0
- package/app/lib/live.mjs +346 -0
- package/app/lib/llm.mjs +594 -0
- package/app/lib/newfile.mjs +126 -0
- package/app/lib/prdiff.mjs +651 -0
- package/app/lib/reveal.mjs +316 -0
- package/app/lib/roots.mjs +186 -0
- package/app/lib/scope.mjs +342 -0
- package/app/lib/session.mjs +389 -0
- package/app/lib/slots.mjs +233 -0
- package/app/lib/ssot.mjs +277 -0
- package/app/lib/teamview.mjs +962 -0
- package/app/lib/terms.ko.mjs +169 -0
- package/app/server.mjs +1959 -0
- package/app/web/shell.css +538 -0
- package/app/web/shell.html +197 -0
- package/app/web/shell.js +638 -0
- package/app/web/stage.js +347 -0
- package/app/web/words.js +85 -0
- package/bin/axmap.mjs +1918 -0
- package/governance/GOVERNANCE.md +433 -0
- package/governance/gate.mjs +526 -0
- package/governance/vote.mjs +501 -0
- package/mcp/README.md +254 -0
- package/mcp/SETUP-FOR-AI.md +186 -0
- package/mcp/install.ps1 +341 -0
- package/mcp/install.sh +339 -0
- package/mcp/server.mjs +969 -0
- package/package.json +48 -0
- package/src/closure.mjs +343 -0
- package/src/governance.mjs +839 -0
- package/src/invariants.mjs +226 -0
- package/src/mrtarget.mjs +284 -0
- package/src/promote.mjs +177 -0
- package/src/protocol.mjs +423 -0
- package/src/repotarget.mjs +81 -0
- package/src/update.mjs +177 -0
- package/src/version.mjs +186 -0
- package/tools/bus.mjs +520 -0
- package/tools/cluster-experiment.mjs +256 -0
- package/tools/cluster-sweep.mjs +226 -0
- package/tools/make-icon.mjs +108 -0
- package/tools/mcp-register.mjs +269 -0
- package/tools/mr-target.mjs +49 -0
- package/tools/persona-bench.mjs +362 -0
- package/tools/pick-repo.mjs +229 -0
- package/tools/promote.mjs +550 -0
- package/tools/reveal-demo.mjs +158 -0
- package/tools/run-tests.mjs +42 -0
- package/tools/setup.mjs +490 -0
- package/tools/shortcut.mjs +121 -0
- package/tools/smoke.mjs +166 -0
- package/tools/topicgraph.py +154 -0
- package/tools/vendor.mjs +382 -0
- package/tools/version.mjs +115 -0
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 언어별 import 규칙.
|
|
3
|
+
*
|
|
4
|
+
* 🔴 왜 떼어냈나.
|
|
5
|
+
*
|
|
6
|
+
* 예전에는 `analyze.mjs` 의 `importsOf()` 안에 언어별 if-체인으로 있었다.
|
|
7
|
+
* 5개일 때는 읽을 만했다. 그런데 SSOT 코퍼스가 12,672개 저장소로 커지면서
|
|
8
|
+
* 드러난 사실이 이랬다 — **결합 판정이 실제로 도는 언어는 5개뿐**이고
|
|
9
|
+
* 코퍼스가 잰 커버리지(스냅샷 16,331) — 13개 언어가 90~100%, swift 만 4.9% 였다.
|
|
10
|
+
* 그 숫자가 이 파일의 작업 순서를 정했다. 이제 swift 도 모듈 수준까지 푼다.
|
|
11
|
+
*
|
|
12
|
+
* 저장소는 500개씩 모았는데 import 를 못 읽으니 결합이 **0 으로 보인다.**
|
|
13
|
+
* 그 0 을 근거로 "잘 분리돼 있군요" 라고 말하면 새빨간 거짓말이라
|
|
14
|
+
* `MIN_COVERAGE_FOR_COUPLING` 게이트로 막아뒀다. 즉 **저장소를 더 모아도
|
|
15
|
+
* 소용이 없고, 파서를 늘려야 한다.**
|
|
16
|
+
*
|
|
17
|
+
* 그래서 언어를 여럿 동시에 붙이게 됐고, if-체인은 그 순간 병목이 된다 —
|
|
18
|
+
* 두 사람이 같은 함수의 같은 자리를 고치게 되므로 선점 프로토콜이 한 명을
|
|
19
|
+
* 30분 세운다. **파일 경계가 곧 협업의 경계다.** 표로 바꾸면 언어마다
|
|
20
|
+
* 자기 항목만 건드린다.
|
|
21
|
+
*
|
|
22
|
+
* 순수 함수만 둔다 — fs 도 git 도 시계도 없다 (CLAUDE.md).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {object} LangRule
|
|
27
|
+
* @property {string[]} ext 이 언어로 볼 확장자
|
|
28
|
+
* @property {'path'|'dir'} resolve
|
|
29
|
+
* `path` — 명세가 파일을 가리킨다 (python `a.b.c`, js `./x`, java `a.b.C`)
|
|
30
|
+
* `dir` — 명세가 디렉터리를 가리킨다 (go `github.com/x/y`)
|
|
31
|
+
* @property {(text: string) => Set<string>} extract
|
|
32
|
+
* 본문에서 import **명세**를 그대로 뽑는다. 여기서 파일로 해석하지 않는다 —
|
|
33
|
+
* 해석은 저장소 전체를 아는 `resolveImport()` 의 몫이다.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** python — `import a.b` · `from a.b import c` */
|
|
37
|
+
const PY_RE = /^\s*(?:from\s+([\w.]+)\s+import|import\s+([\w.]+))/gm
|
|
38
|
+
|
|
39
|
+
/** js/ts — 상대 경로만. 외부 패키지는 이 그래프에 의미가 없다. */
|
|
40
|
+
const JS_RE = /(?:from|import)\s+["'](\.[^"']+)["']/g
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* java — `import a.b.C;` · `import static a.b.C.member;` · `import a.b.*;`
|
|
44
|
+
* 세 형태를 한 정규식으로 받고 아래에서 가른다.
|
|
45
|
+
*/
|
|
46
|
+
const JAVA_RE = /^\s*import\s+(static\s+)?([\w.]+(?:\.\*)?)\s*;/gm
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* go — 한 줄짜리와 괄호 묶음 둘 다.
|
|
50
|
+
*
|
|
51
|
+
* import "fmt"
|
|
52
|
+
* import (
|
|
53
|
+
* "fmt"
|
|
54
|
+
* alias "github.com/x/y/z"
|
|
55
|
+
* _ "github.com/blank/import"
|
|
56
|
+
* )
|
|
57
|
+
*/
|
|
58
|
+
const GO_BLOCK_RE = /^\s*import\s*\(([\s\S]*?)^\s*\)/gm
|
|
59
|
+
const GO_ONE_RE = /^\s*import\s+(?:[\w.]+\s+)?"([^"]+)"/gm
|
|
60
|
+
const GO_IN_BLOCK_RE = /^\s*(?:[\w.]+\s+)?"([^"]+)"/gm
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* rust — `mod x;` 와 `use a::b::C;`
|
|
64
|
+
*
|
|
65
|
+
* 🔴 두 문법은 신뢰도가 다르다. 그래서 다르게 다룬다.
|
|
66
|
+
*
|
|
67
|
+
* `mod x;` 는 **파일 선언**이다. 러스트에서는 이 줄이 있어야 `x.rs` 또는
|
|
68
|
+
* `x/mod.rs` 가 빌드에 들어간다. 추측이 아니라 사실이고, 이것만으로도
|
|
69
|
+
* 모듈 트리가 통째로 나온다.
|
|
70
|
+
*
|
|
71
|
+
* `use a::b::C;` 는 **이름 가져오기**다. 마지막 마디가 파일이 아니라 타입일
|
|
72
|
+
* 때가 대부분이라(`Config` · `HashMap`) 그대로 경로로 쓰면 아무 데도 안 붙는다.
|
|
73
|
+
* 러스트 관례상 타입은 UpperCamelCase, 모듈은 snake_case 라 그것으로 가른다.
|
|
74
|
+
*
|
|
75
|
+
* ⚠️ 일부러 안 하는 것 —
|
|
76
|
+
* · `mod x { ... }` (인라인 모듈) 은 파일이 없다. `;` 를 요구해 걸러진다.
|
|
77
|
+
* · `#[path = "..."] mod x;` 는 파일 위치를 바꾼다. 드물어서 안 다룬다.
|
|
78
|
+
* · 마디가 하나뿐인 `use serde;` 는 버린다 — 외부 크레이트 전체를 뜻하는데,
|
|
79
|
+
* 해석기의 단일 마디 분기는 저장소에 이름이 유일하기만 하면 이어버린다.
|
|
80
|
+
* 저장소에 우연히 `serde.rs` 가 있으면 거기 붙는다.
|
|
81
|
+
*/
|
|
82
|
+
const RS_MOD_RE = /^\s*(?:pub(?:\s*\([^)]*\))?\s+)?mod\s+([A-Za-z_]\w*)\s*;/gm
|
|
83
|
+
const RS_USE_RE = /^\s*(?:pub(?:\s*\([^)]*\))?\s+)?use\s+([^;]+);/gm
|
|
84
|
+
|
|
85
|
+
/** 마지막 마디가 파일이 아니라 타입·매크로·와일드카드인가. */
|
|
86
|
+
const rsIsItem = (x) => x === '*' || x === '' || /^[A-Z]/.test(x)
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 한 `use` 에서 나올 수 있는 명세들.
|
|
90
|
+
*
|
|
91
|
+
* 🔴 **내부 뿌리(`crate`·`self`·`super`)일 때만** 접두사를 함께 낸다.
|
|
92
|
+
*
|
|
93
|
+
* `use self::helper::run;` 에서 `run` 은 함수일 수도 모듈일 수도 있다.
|
|
94
|
+
* 러스트는 둘 다 snake_case 라 대소문자로 못 가른다. 그래서 `./helper/run` 과
|
|
95
|
+
* `./helper` 를 둘 다 넘긴다 — **존재하는 파일만 엣지가 되므로** 오탐이 안 는다.
|
|
96
|
+
* (`helper/run.rs` 가 있다면 `helper` 는 디렉터리 모듈이고 `run` 은 모듈이 맞다.)
|
|
97
|
+
*
|
|
98
|
+
* 외부 뿌리에는 안 한다. `use std::collections::HashMap` 의 접두사 `std` 를
|
|
99
|
+
* 넘기면 저장소에 `std.rs` 가 있을 때 붙는다.
|
|
100
|
+
*/
|
|
101
|
+
function rsUseSpecs(raw, dir) {
|
|
102
|
+
const out = []
|
|
103
|
+
const spec = rsUseSpec(raw, dir)
|
|
104
|
+
if (spec) out.push(spec)
|
|
105
|
+
const head = raw.replace(/\s+/g, '').split('{')[0].split('::').filter(Boolean)[0]
|
|
106
|
+
if (spec && ['crate', 'self', 'super'].includes(head)) {
|
|
107
|
+
const cut = spec.lastIndexOf(spec.includes('/') ? '/' : '.')
|
|
108
|
+
// 접두사가 `./` 나 `../` 만 남으면 가리키는 것이 없다.
|
|
109
|
+
if (cut > 0) {
|
|
110
|
+
const pre = spec.slice(0, cut)
|
|
111
|
+
if (!/^\.+\/?$/.test(pre)) out.push(pre)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return out
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* `self`·`super` 가 가리키는 디렉터리를 **깊이**로 계산한다.
|
|
119
|
+
*
|
|
120
|
+
* `dir` 은 이 파일의 자식 모듈이 사는 곳(`rsModuleDir`) 이다.
|
|
121
|
+
* `./` 이면 파일이 있는 디렉터리와 같고(깊이 0), `./stem/` 이면 한 칸 아래다(1).
|
|
122
|
+
*
|
|
123
|
+
* self → 그 자리 그대로
|
|
124
|
+
* super × N → 거기서 N 칸 위
|
|
125
|
+
*
|
|
126
|
+
* 🔴 `src/a/mod.rs` 에서 `use super::x` 는 `src/x` 다 — `src/a/x` 가 아니다.
|
|
127
|
+
* 예전에는 super 를 늘 파일 디렉터리로 봤고, mod.rs 에서 그건 자기 자식을
|
|
128
|
+
* 가리킨다(`self::x` 와 같아진다). 같은 원인의 두 번째 버그였다.
|
|
129
|
+
*/
|
|
130
|
+
function rsPrefix(dir, up) {
|
|
131
|
+
const depth = (dir === './' ? 0 : 1) - up
|
|
132
|
+
if (depth === 1) return dir
|
|
133
|
+
if (depth === 0) return './'
|
|
134
|
+
return '../'.repeat(-depth)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function rsUseSpec(raw, dir) {
|
|
138
|
+
// `use a::b::{c, d}` — 중괄호 앞의 공통 경로만 쓴다. 안쪽까지 펼치면
|
|
139
|
+
// 각 마디가 모듈인지 타입인지 가릴 수 없어 오탐이 는다.
|
|
140
|
+
let p = raw.split('{')[0]
|
|
141
|
+
// `use a::b as c` — 별칭은 경로가 아니다.
|
|
142
|
+
p = p.split(/\s+as\s+/)[0].replace(/\s+/g, '')
|
|
143
|
+
const segs = p.split('::').filter((x) => x !== '')
|
|
144
|
+
if (!segs.length) return null
|
|
145
|
+
|
|
146
|
+
// 끝이 타입이면 떼어낸다. `crate::config::Config` → `crate::config`
|
|
147
|
+
while (segs.length && rsIsItem(segs[segs.length - 1])) segs.pop()
|
|
148
|
+
if (!segs.length) return null
|
|
149
|
+
|
|
150
|
+
const head = segs[0]
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 🔴 `super::x` 는 **같은 디렉터리**다. 한 단계 위가 아니다.
|
|
154
|
+
*
|
|
155
|
+
* 파이썬 상대 import 와 헷갈리기 쉬운 자리다. `src/a/b.rs` 는 모듈 `a::b` 이고
|
|
156
|
+
* 그 `super` 는 모듈 `a` 인데, `a` 의 자식 파일들은 `src/a/` 에 산다 —
|
|
157
|
+
* 즉 `b.rs` 자신이 있는 디렉터리다. 그래서 super 하나는 `./`,
|
|
158
|
+
* 둘이어야 `../` 다. (해석기의 파이썬 분기가 `up <= 1` 로 같은 판단을 한다.)
|
|
159
|
+
*
|
|
160
|
+
* 처음에 `'../'.repeat(up)` 으로 썼다가 실측에서 잡았다 —
|
|
161
|
+
* `use super::parent_thing` 이 형제가 아니라 삼촌을 가리키고 있었다.
|
|
162
|
+
*/
|
|
163
|
+
if (head === 'self' || head === 'super') {
|
|
164
|
+
let up = 0
|
|
165
|
+
while (segs[up] === 'super') up++
|
|
166
|
+
const rest = segs.slice(head === 'self' ? 1 : up)
|
|
167
|
+
if (!rest.length) return null
|
|
168
|
+
return `${rsPrefix(dir, up)}${rest.join('/')}`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// `crate::` 는 크레이트 뿌리다. 그 뿌리가 어느 디렉터리인지는 저장소마다
|
|
172
|
+
// 다르므로(워크스페이스면 여럿) 마디만 넘기고 해석은 색인에 맡긴다.
|
|
173
|
+
const rest = head === 'crate' ? segs.slice(1) : segs
|
|
174
|
+
if (!rest.length) return null
|
|
175
|
+
/**
|
|
176
|
+
* 🔴 마디가 하나일 때의 판정이 `crate::` 여부로 갈린다.
|
|
177
|
+
*
|
|
178
|
+
* `use serde;` 는 외부 크레이트 전체다. 그대로 넘기면 해석기의 단일 마디
|
|
179
|
+
* 분기가 "저장소에 이름이 유일하면" 이어버려, 우연히 있는 `serde.rs` 에 붙는다.
|
|
180
|
+
*
|
|
181
|
+
* 반면 `use crate::config::Config` 는 타입을 떼고 나면 `config` 하나만 남는데
|
|
182
|
+
* 이건 **내부 모듈이 확실하다.** 처음에 둘을 같이 막았다가 러스트에서 가장
|
|
183
|
+
* 흔한 형태를 통째로 잃었다.
|
|
184
|
+
*/
|
|
185
|
+
if (rest.length < 2 && head !== 'crate') return null
|
|
186
|
+
return rest.join('.')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* ruby — `require_relative` 와 `require`
|
|
191
|
+
*
|
|
192
|
+
* 🔴 둘의 신뢰도가 다르다.
|
|
193
|
+
*
|
|
194
|
+
* `require_relative 'x'` 는 **이 파일 기준 경로**라 확실하다.
|
|
195
|
+
*
|
|
196
|
+
* `require 'x'` 는 로드 경로 기준이라 대부분 젬·표준 라이브러리다
|
|
197
|
+
* (`json` · `set` · `logger`). 마디가 하나뿐인 것을 그대로 넘기면 해석기의
|
|
198
|
+
* 단일 마디 분기가 "저장소에 이름이 유일하면" 이어버린다 —
|
|
199
|
+
* `require 'logger'` 가 우리 `app/logger.rb` 에 붙는 식이다.
|
|
200
|
+
*
|
|
201
|
+
* 그래서 **경로 모양(`/` 포함)만** 넘긴다. 그건 꼬리가 통째로 맞아야 붙으므로
|
|
202
|
+
* (bySuffix) 외부 젬은 자연히 아무 데도 안 붙는다.
|
|
203
|
+
* 잃는 것: `require 'my_helper'` 같은 내부 단일 이름. fail-closed 쪽이다.
|
|
204
|
+
*/
|
|
205
|
+
const RB_REL_RE = /^\s*require_relative\s+['"]([^'"]+)['"]/gm
|
|
206
|
+
const RB_REQ_RE = /^\s*require\s+['"]([^'"]+)['"]/gm
|
|
207
|
+
|
|
208
|
+
/** 색인은 확장자를 떼고 만든다. `require_relative 'x.rb'` 도 있어서 떼어준다. */
|
|
209
|
+
const RB_EXT = /\.rb$/
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 러스트에서 **이 파일의 자식 모듈이 사는 디렉터리**.
|
|
213
|
+
*
|
|
214
|
+
* 🔴 여기를 틀리면 없는 관계를 그럴듯하게 그린다.
|
|
215
|
+
*
|
|
216
|
+
* 실측(tokio): `src/runtime/context.rs` 가 `mod blocking;` 으로 자기 자식
|
|
217
|
+
* 모듈(`context/blocking.rs`)을 선언하는데, 같은 디렉터리로 풀면
|
|
218
|
+
* `src/runtime/blocking/mod.rs` 라는 **무관한 파일이 실제로 있어서** 거기
|
|
219
|
+
* 선이 붙었다. 못 이은 것이 아니라 **틀리게 이은 것**이라 더 나쁘다 —
|
|
220
|
+
* 신입은 화면에 있는 선을 사실로 읽는다. 같은 저장소에서 `mod tests;` 는
|
|
221
|
+
* 아예 놓쳤다(21개 파일이 이 모양이다).
|
|
222
|
+
*
|
|
223
|
+
* 규칙(러스트 2018):
|
|
224
|
+
* `foo/mod.rs` 자식은 **같은 디렉터리** — `foo/x.rs`
|
|
225
|
+
* 크레이트 뿌리 자식은 **같은 디렉터리** — `src/lib.rs` → `src/x.rs`
|
|
226
|
+
* 그 밖의 `foo.rs` 자식은 **`foo/` 안** — `src/a.rs` → `src/a/x.rs`
|
|
227
|
+
*
|
|
228
|
+
* 크레이트 뿌리는 `lib.rs`·`main.rs` 와 `tests/`·`benches/`·`examples/`·
|
|
229
|
+
* `src/bin/` 바로 아래의 파일이다. 마지막 넷은 카고가 각각을 독립 크레이트로
|
|
230
|
+
* 빌드하기 때문이다 — ripgrep 의 `tests/tests.rs` 가 `mod util;` 로
|
|
231
|
+
* `tests/util.rs` 를 가리키는 것이 그 경우다.
|
|
232
|
+
*
|
|
233
|
+
* 🔴 두 후보를 다 내지 않는다. tokio 처럼 **둘 다 존재하는** 저장소가 있고,
|
|
234
|
+
* 그러면 틀린 선이 같이 그려진다. 갈래를 여기서 결정한다.
|
|
235
|
+
*/
|
|
236
|
+
const RS_ROOT = /(^|\/)(lib|main)\.rs$/
|
|
237
|
+
const RS_TARGET_DIR = /(^|\/)(tests|benches|examples|src\/bin)\/[^/]+\.rs$/
|
|
238
|
+
|
|
239
|
+
export function rsModuleDir(filePath) {
|
|
240
|
+
const p = String(filePath ?? '').split(String.fromCharCode(92)).join('/')
|
|
241
|
+
const slash = p.lastIndexOf('/')
|
|
242
|
+
const base = slash < 0 ? p : p.slice(slash + 1)
|
|
243
|
+
// 🔴 경로를 모르면 같은 디렉터리로 둔다. 그게 크레이트 뿌리·`mod.rs` 의
|
|
244
|
+
// 답이고, 2015 판의 기본이기도 하다. 여기서 `./stem/` 을 지어내면
|
|
245
|
+
// 있지도 않은 하위 디렉터리를 가리킨다.
|
|
246
|
+
if (!base || base === 'mod.rs' || RS_ROOT.test(p) || RS_TARGET_DIR.test(p)) return './'
|
|
247
|
+
return `./${base.replace(/\.rs$/, '')}/`
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* c# — `using System;` · `using static A.B;` · `using X = A.B;`
|
|
252
|
+
*
|
|
253
|
+
* 🔴 자바와 닮았지만 **한 가지가 다르다.** 자바는 패키지가 곧 디렉터리이고
|
|
254
|
+
* 파일 이름이 클래스 이름이라 `a.b.C` 가 파일 하나로 풀린다. c# 의
|
|
255
|
+
* `using` 은 **네임스페이스**를 부르고, 한 네임스페이스에 파일이 여럿일
|
|
256
|
+
* 수 있으며 디렉터리와 일치할 의무도 없다.
|
|
257
|
+
*
|
|
258
|
+
* 그래도 관례상 대부분 일치하므로 마디를 넘기고 **해석기의 꼬리 검사에
|
|
259
|
+
* 맡긴다** — 디렉터리가 안 맞으면 아무 데도 안 붙는다(fail-closed).
|
|
260
|
+
*/
|
|
261
|
+
const CS_USE_RE = /^\s*using\s+(?:static\s+)?(?:[\w]+\s*=\s*)?([\w.]+)\s*;/gm
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* php — `use A\\B\\C;` 와 `require '경로'`
|
|
265
|
+
*
|
|
266
|
+
* 두 축이 다 있다. `use` 는 PSR-4 네임스페이스(구분자가 역슬래시)이고,
|
|
267
|
+
* `require`/`include` 는 파일 경로다.
|
|
268
|
+
*
|
|
269
|
+
* 🔴 `use A\\{B, C}` 같은 묶음은 앞쪽 공통 경로만 쓴다. 안쪽까지 펼치면
|
|
270
|
+
* 각 마디가 네임스페이스인지 클래스인지 가릴 수 없어 오탐이 는다
|
|
271
|
+
* (러스트 `use a::{b, c}` 에서 내린 것과 같은 판단).
|
|
272
|
+
*/
|
|
273
|
+
const PHP_USE_RE = /^\s*use\s+(?:function\s+|const\s+)?([\w\\\\]+)/gm
|
|
274
|
+
const PHP_REQ_RE = /(?:require|include)(?:_once)?\s*\(?\s*(?:__DIR__\s*\.\s*)?["']([^"']+)["']/g
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* kotlin — `import a.b.C` (세미콜론 없음) · `import a.b.*` 는 버린다
|
|
278
|
+
*
|
|
279
|
+
* ⚠️ 자바보다 근거가 약하다. 코틀린은 **파일 이름이 클래스 이름과 같을
|
|
280
|
+
* 의무가 없고** 최상위 함수도 import 할 수 있다. 그래서 `a.b.foo` 가
|
|
281
|
+
* 파일을 못 가리키는 경우가 자바보다 흔하다. 못 찾으면 안 잇는다.
|
|
282
|
+
*/
|
|
283
|
+
const KT_IMPORT_RE = /^\s*import\s+([\w.]+(?:\.\*)?)(?:\s+as\s+\w+)?\s*$/gm
|
|
284
|
+
|
|
285
|
+
/** scala — `import a.b.C` · `import a.b.{C, D}` · `import a.b._` */
|
|
286
|
+
const SCALA_IMPORT_RE = /^\s*import\s+([\w.]+)/gm
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 마디가 둘 이상인 점 경로만 남긴다.
|
|
290
|
+
*
|
|
291
|
+
* 마디가 하나면 해석기의 단일 마디 분기를 타서 "저장소에 이름이 유일하면"
|
|
292
|
+
* 이어버린다. `using System;` 이 우연히 있는 `System.cs` 에 붙는 식이다.
|
|
293
|
+
*/
|
|
294
|
+
function dotted(spec) {
|
|
295
|
+
return spec.includes('.') && !spec.endsWith('.*') && !spec.endsWith('._') ? spec : null
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** @type {Record<string, LangRule>} */
|
|
299
|
+
export const LANGS = {
|
|
300
|
+
python: {
|
|
301
|
+
ext: ['.py'],
|
|
302
|
+
resolve: 'path',
|
|
303
|
+
extract(text) {
|
|
304
|
+
const set = new Set()
|
|
305
|
+
for (const m of text.matchAll(PY_RE)) set.add(m[1] ?? m[2])
|
|
306
|
+
return set
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
js: {
|
|
311
|
+
ext: ['.mjs', '.cjs', '.js', '.jsx'],
|
|
312
|
+
resolve: 'path',
|
|
313
|
+
extract(text) {
|
|
314
|
+
const set = new Set()
|
|
315
|
+
for (const m of text.matchAll(JS_RE)) set.add(m[1])
|
|
316
|
+
return set
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
ts: {
|
|
321
|
+
ext: ['.ts', '.tsx'],
|
|
322
|
+
resolve: 'path',
|
|
323
|
+
extract(text) {
|
|
324
|
+
const set = new Set()
|
|
325
|
+
for (const m of text.matchAll(JS_RE)) set.add(m[1])
|
|
326
|
+
return set
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
go: {
|
|
331
|
+
ext: ['.go'],
|
|
332
|
+
// go 의 명세는 **디렉터리**다. `github.com/x/y` 아래 파일이 여럿이다.
|
|
333
|
+
resolve: 'dir',
|
|
334
|
+
extract(text) {
|
|
335
|
+
const set = new Set()
|
|
336
|
+
// 괄호 묶음이 흔하다. 블록을 먼저 훑고 한 줄짜리를 더한다.
|
|
337
|
+
for (const b of text.matchAll(GO_BLOCK_RE)) {
|
|
338
|
+
for (const m of b[1].matchAll(GO_IN_BLOCK_RE)) set.add(m[1])
|
|
339
|
+
}
|
|
340
|
+
for (const m of text.matchAll(GO_ONE_RE)) set.add(m[1])
|
|
341
|
+
return set
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
java: {
|
|
346
|
+
ext: ['.java'],
|
|
347
|
+
resolve: 'path',
|
|
348
|
+
extract(text) {
|
|
349
|
+
const set = new Set()
|
|
350
|
+
for (const m of text.matchAll(JAVA_RE)) {
|
|
351
|
+
const isStatic = Boolean(m[1])
|
|
352
|
+
let spec = m[2]
|
|
353
|
+
// 와일드카드는 패키지 전체를 뜻하므로 파일 하나로 못 푼다 (fail-closed).
|
|
354
|
+
if (spec.endsWith('.*')) continue
|
|
355
|
+
// `import static a.b.C.member;` 의 마지막 마디는 클래스가 아니라 멤버다.
|
|
356
|
+
if (isStatic) spec = spec.slice(0, spec.lastIndexOf('.'))
|
|
357
|
+
if (spec.includes('.')) set.add(spec)
|
|
358
|
+
}
|
|
359
|
+
return set
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
rust: {
|
|
364
|
+
ext: ['.rs'],
|
|
365
|
+
resolve: 'path',
|
|
366
|
+
extract(text, filePath) {
|
|
367
|
+
const set = new Set()
|
|
368
|
+
// 자식 모듈이 어디 사는지는 **파일 이름에 달렸다** (rsModuleDir 주석).
|
|
369
|
+
const dir = rsModuleDir(filePath)
|
|
370
|
+
for (const m of text.matchAll(RS_MOD_RE)) {
|
|
371
|
+
// `x.rs` 와 `x/mod.rs` 둘 다 가능하다. 동시에 존재할 수는 없으므로
|
|
372
|
+
// 둘을 다 넘겨도 엣지가 겹치지 않는다 — 해석기가 맞는 쪽만 찾는다.
|
|
373
|
+
set.add(`${dir}${m[1]}`)
|
|
374
|
+
set.add(`${dir}${m[1]}/mod`)
|
|
375
|
+
}
|
|
376
|
+
for (const m of text.matchAll(RS_USE_RE)) {
|
|
377
|
+
for (const spec of rsUseSpecs(m[1], dir)) set.add(spec)
|
|
378
|
+
}
|
|
379
|
+
return set
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* `.h` 를 c 에 둔다. c++ 프로젝트의 `.h` 도 여기로 오지만, 확장자 하나는
|
|
385
|
+
* 한 언어에만 속해야 하고(EXT_TO_LANG 이 조용히 덮어쓴다) 규칙이 같으므로
|
|
386
|
+
* 엣지 결과는 달라지지 않는다. 코퍼스(ssot.mjs LANG_BY_EXT)도 같은 선택이다.
|
|
387
|
+
*/
|
|
388
|
+
c: {
|
|
389
|
+
ext: ['.c', '.h'],
|
|
390
|
+
resolve: 'include',
|
|
391
|
+
extract: cIncludes,
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
cpp: {
|
|
395
|
+
ext: ['.cc', '.cpp', '.cxx', '.hpp', '.hh', '.hxx', '.ipp', '.tcc'],
|
|
396
|
+
resolve: 'include',
|
|
397
|
+
extract: cIncludes,
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
ruby: {
|
|
401
|
+
ext: ['.rb'],
|
|
402
|
+
resolve: 'path',
|
|
403
|
+
extract(text) {
|
|
404
|
+
const set = new Set()
|
|
405
|
+
for (const m of text.matchAll(RB_REL_RE)) {
|
|
406
|
+
const p = m[1].replace(RB_EXT, '')
|
|
407
|
+
// 해석기의 상대 경로 분기는 `.` 으로 시작하고 `/` 를 포함해야 탄다.
|
|
408
|
+
set.add(p.startsWith('.') ? p : `./${p}`)
|
|
409
|
+
}
|
|
410
|
+
for (const m of text.matchAll(RB_REQ_RE)) {
|
|
411
|
+
const p = m[1].replace(RB_EXT, '')
|
|
412
|
+
// 경로 모양만 받는다 (위 주석 — 젬·표준 라이브러리 오탐 방지)
|
|
413
|
+
if (!p.includes('/')) continue
|
|
414
|
+
// 해석기는 점으로 마디를 가른다. `foo/bar` → `foo.bar` → bySuffix('foo/bar')
|
|
415
|
+
set.add(p.split('/').filter(Boolean).join('.'))
|
|
416
|
+
}
|
|
417
|
+
return set
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
csharp: {
|
|
422
|
+
ext: ['.cs'],
|
|
423
|
+
/**
|
|
424
|
+
* 🔴 `path` 가 아니라 `ns` 다. `using MyApp.Models;` 는 **네임스페이스**를
|
|
425
|
+
* 부르고 그 안에 파일이 여럿이다. 파일 하나로 풀려고 하면 거의 안 붙는다
|
|
426
|
+
* (Dapper 실측: 156개 파일에 엣지 1개).
|
|
427
|
+
*
|
|
428
|
+
* 그래서 마디가 하나여도 버리지 않는다 — `using Dapper;` 는 실제로
|
|
429
|
+
* `Dapper/` 디렉터리다. 없는 저장소에서는 아무 데도 안 붙으므로
|
|
430
|
+
* (fail-closed) 남겨도 오탐이 안 는다.
|
|
431
|
+
*/
|
|
432
|
+
resolve: 'ns',
|
|
433
|
+
extract(text) {
|
|
434
|
+
const set = new Set()
|
|
435
|
+
for (const m of text.matchAll(CS_USE_RE)) set.add(m[1])
|
|
436
|
+
return set
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
|
|
440
|
+
php: {
|
|
441
|
+
ext: ['.php'],
|
|
442
|
+
resolve: 'path',
|
|
443
|
+
extract(text) {
|
|
444
|
+
const set = new Set()
|
|
445
|
+
for (const m of text.matchAll(PHP_USE_RE)) {
|
|
446
|
+
// 역슬래시를 점으로. 해석기는 점으로 마디를 가른다.
|
|
447
|
+
const d = dotted(m[1].split(String.fromCharCode(92)).filter(Boolean).join('.'))
|
|
448
|
+
if (d) set.add(d)
|
|
449
|
+
}
|
|
450
|
+
for (const m of text.matchAll(PHP_REQ_RE)) {
|
|
451
|
+
const raw = m[1].split(String.fromCharCode(92)).join('/')
|
|
452
|
+
// `__DIR__ . '/x.php'` 는 여는 파일 기준이다. 앞의 `/` 를 떼고 상대로 만든다.
|
|
453
|
+
const rel = raw.startsWith('.') ? raw : `./${raw.replace(/^\//, '')}`
|
|
454
|
+
if (rel.includes('/')) set.add(rel)
|
|
455
|
+
}
|
|
456
|
+
return set
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
kotlin: {
|
|
461
|
+
ext: ['.kt', '.kts'],
|
|
462
|
+
resolve: 'path',
|
|
463
|
+
extract(text) {
|
|
464
|
+
const set = new Set()
|
|
465
|
+
for (const m of text.matchAll(KT_IMPORT_RE)) {
|
|
466
|
+
const d = dotted(m[1])
|
|
467
|
+
if (d) set.add(d)
|
|
468
|
+
}
|
|
469
|
+
return set
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
scala: {
|
|
474
|
+
ext: ['.scala'],
|
|
475
|
+
// c# 과 같은 이유로 `ns` 다. 실측(scalaz)에서 import 의 압도적 다수가
|
|
476
|
+
// `import scalaz._` 같은 와일드카드였고, 그건 파일이 아니라 패키지다.
|
|
477
|
+
resolve: 'ns',
|
|
478
|
+
extract(text) {
|
|
479
|
+
const set = new Set()
|
|
480
|
+
for (const m of text.matchAll(SCALA_IMPORT_RE)) {
|
|
481
|
+
// `import a.b._` 는 패키지 전체 — 뒤의 `._` 를 떼면 그 패키지 경로다.
|
|
482
|
+
// 마디가 하나(`import scalaz._`)여도 버리지 않는다. c# 과 같은 이유로
|
|
483
|
+
// 그것도 디렉터리이고, 없는 저장소에서는 아무 데도 안 붙는다.
|
|
484
|
+
const spec = m[1].replace(/\._?$/, '')
|
|
485
|
+
if (spec) set.add(spec)
|
|
486
|
+
}
|
|
487
|
+
return set
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
swift: {
|
|
492
|
+
ext: ['.swift'],
|
|
493
|
+
resolve: 'swift',
|
|
494
|
+
extract(text) {
|
|
495
|
+
const set = new Set()
|
|
496
|
+
for (const m of text.matchAll(SWIFT_IMPORT_RE)) set.add(m[1])
|
|
497
|
+
return set
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* c / c++ — `#include`
|
|
505
|
+
*
|
|
506
|
+
* 🔴 두 가지가 다른 언어들과 다르다.
|
|
507
|
+
*
|
|
508
|
+
* ① 명세가 **확장자를 달고 다닌다** (`"foo.h"`). 점 경로를 마디로 가르는
|
|
509
|
+
* 다른 언어들과 달라서 해석 방식이 따로 필요하다 (`resolve: 'include'`).
|
|
510
|
+
* ② 같은 따옴표 include 가 저장소마다 **다른 기준**을 쓴다. 작은
|
|
511
|
+
* 프로젝트는 여는 파일 기준(`#include "util.h"`)이고, 큰 프로젝트는
|
|
512
|
+
* 루트 기준이다 — protobuf 의 `#include "google/protobuf/message.h"` 는
|
|
513
|
+
* `src/google/protobuf/foo.cc` 에서 써도 자기 디렉터리를 안 가리킨다.
|
|
514
|
+
* 둘 다 시도하고, 애매하면 잇지 않는다(해석기 쪽 판단).
|
|
515
|
+
*
|
|
516
|
+
* 🔴 꺾쇠(`<...>`)는 **버린다.**
|
|
517
|
+
*
|
|
518
|
+
* `<vector>` · `<cstdio>` 는 표준 라이브러리다. 그것을 저장소 파일에
|
|
519
|
+
* 이으려 하면 우연히 이름이 같은 파일에 붙는다. 프로젝트가 자기 헤더를
|
|
520
|
+
* 꺾쇠로 부르는 경우(`-I` 로 잡은 include 디렉터리)를 놓치지만,
|
|
521
|
+
* **못 잇는 것보다 잘못 잇는 것이 나쁘다** — 신입은 화면에 있는 선을
|
|
522
|
+
* 사실로 읽는다.
|
|
523
|
+
*/
|
|
524
|
+
/**
|
|
525
|
+
* 스위프트의 `import` 는 **모듈**을 부른다 — 파일이 아니다.
|
|
526
|
+
*
|
|
527
|
+
* import Foundation 시스템 모듈
|
|
528
|
+
* import MyFeature 같은 저장소의 다른 모듈
|
|
529
|
+
* @testable import MyFeature 테스트에서
|
|
530
|
+
* import struct Foundation.Data 한 타입만
|
|
531
|
+
*
|
|
532
|
+
* 마지막 형태 때문에 `struct`·`class` 같은 낱말이 모듈 이름 앞에 온다.
|
|
533
|
+
* 우리가 쓰는 것은 **첫 마디(모듈)뿐**이고 `.Data` 는 버린다 — 파일로 풀리는
|
|
534
|
+
* 것은 모듈까지이기 때문이다.
|
|
535
|
+
*/
|
|
536
|
+
const SWIFT_IMPORT_RE =
|
|
537
|
+
/^[ \t]*(?:@testable[ \t]+)?import[ \t]+(?:(?:struct|class|enum|protocol|typealias|func|var|let)[ \t]+)?([A-Za-z_]\w*)/gm
|
|
538
|
+
|
|
539
|
+
const C_INCLUDE_RE = /^[ \t]*#[ \t]*include[ \t]*"([^"]+)"/gm
|
|
540
|
+
|
|
541
|
+
function cIncludes(text) {
|
|
542
|
+
const set = new Set()
|
|
543
|
+
for (const m of text.matchAll(C_INCLUDE_RE)) {
|
|
544
|
+
// 윈도우 스타일 구분자를 쓰는 코드가 있다. 색인은 `/` 로만 만든다.
|
|
545
|
+
const spec = m[1].split('\\').join('/').trim()
|
|
546
|
+
if (spec) set.add(spec)
|
|
547
|
+
}
|
|
548
|
+
return set
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* 🔴 스위프트는 **모듈까지만** 푼다. 파일 사이의 import 는 이 언어에 없다.
|
|
553
|
+
*
|
|
554
|
+
* 같은 모듈 안의 파일들은 서로를 import 하지 않고 그냥 보인다(접근 제어가
|
|
555
|
+
* 모듈 단위다). 그래서 `import MyModule` 을 아무 파일에나 이으면 그것은
|
|
556
|
+
* 못 잇는 것이 아니라 **없는 관계를 지어내는 것**이다.
|
|
557
|
+
*
|
|
558
|
+
* 그런데 모듈은 실재한다 — SwiftPM 에서 모듈은 `Sources/<이름>/` 이라는
|
|
559
|
+
* **진짜 디렉터리**다. 거기까지는 지어내는 것이 아니라 관측이다. Go 의 패키지·
|
|
560
|
+
* c# 의 네임스페이스에 쓰는 해석과 같고, 같은 상한(GO_PKG_CAP)을 받는다.
|
|
561
|
+
*
|
|
562
|
+
* 세 가지로 fail-closed 를 지킨다 (analyze.mjs 의 `resolveSwift`) —
|
|
563
|
+
* ① 결과가 `.swift` 파일이 아니면 버린다. 스위프트 모듈은 스위프트다.
|
|
564
|
+
* ② `Sources/<이름>/`·`src/<이름>/` 처럼 **모듈 뿌리**로 보이는 것을 먼저 쓴다.
|
|
565
|
+
* ③ 뿌리가 없는데 이름이 애플 프레임워크면 버린다. 저장소 어딘가에 우연히
|
|
566
|
+
* `Network/` 하위 폴더가 있다고 `import Network` 가 그것일 리 없다.
|
|
567
|
+
* 반대로 `Sources/Network/` 가 있으면 그건 저장소의 진짜 모듈이라 받는다.
|
|
568
|
+
*
|
|
569
|
+
* **아직 못 보는 것** — 같은 모듈 안 파일끼리의 결합. 그건 import 문이 아니라
|
|
570
|
+
* 타입 참조에 있고, 보려면 정규식이 아니라 파서가 필요하다. 그때까지 그 관계는
|
|
571
|
+
* 공변경 축으로만 보인다. 화면에서 "정적 엣지 없음" 이 "관계 없음" 으로 읽히지
|
|
572
|
+
* 않게 하는 것은 confidence 표시의 몫이다.
|
|
573
|
+
*/
|
|
574
|
+
|
|
575
|
+
/** 확장자 → 언어 이름. `LANGS` 에서 파생한다 — 두 곳에 적으면 갈라진다. */
|
|
576
|
+
export const EXT_TO_LANG = Object.fromEntries(
|
|
577
|
+
Object.entries(LANGS).flatMap(([name, r]) => r.ext.map((e) => [e, name])),
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
/** import 를 뽑을 수 있는 언어. 표의 열쇠 집합이 곧 이 답이다. */
|
|
581
|
+
export const PARSED_LANG = new Set(Object.keys(LANGS))
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* 지금 우리가 읽을 수 있는 언어들 — **측정 기록에 찍는 도장.**
|
|
585
|
+
*
|
|
586
|
+
* 🔴 코퍼스가 이걸 안 찍어서 새 파서가 헛일이 될 뻔했다.
|
|
587
|
+
*
|
|
588
|
+
* `parseCoverage` 는 "이 칸의 저장소에서 내부 import 중 몇 개를 실제로
|
|
589
|
+
* 이었나" 다. 파서가 없는 언어에서는 0 이 나오고, 그 0 은 **결합 판정을
|
|
590
|
+
* 보류시키는 근거**로 쓰인다 — 못 본 것을 "결합이 없다" 로 오해하지 않기
|
|
591
|
+
* 위해서다. 옳은 설계다.
|
|
592
|
+
*
|
|
593
|
+
* 그런데 그 값이 **레코드 1만 2천 개의 평균**이다. 오늘 c·rust·ruby 파서를
|
|
594
|
+
* 붙여도 새 레코드 몇백 개로는 평균이 안 움직이고, 게이트는 영원히 안 열린다.
|
|
595
|
+
* 옛 기록이 "그때 우리가 파서가 없었다" 를 말하는데 화면은 그것을 "이 언어는
|
|
596
|
+
* 결합을 볼 수 없다" 로 읽는다.
|
|
597
|
+
*
|
|
598
|
+
* 그래서 잰 순간의 파서 집합을 레코드에 남긴다. 집계는 **같은 도장끼리만**
|
|
599
|
+
* 평균을 낸다. 표에 언어를 더하면 이 문자열이 저절로 바뀐다 — 손으로 올리는
|
|
600
|
+
* 버전 번호는 반드시 올리는 것을 잊는다.
|
|
601
|
+
*/
|
|
602
|
+
export const PARSER_VERSION = [...PARSED_LANG].sort().join(',')
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* 본문에서 import 명세를 뽑는다.
|
|
606
|
+
*
|
|
607
|
+
* 🔴 파서가 없는 언어에서는 **아무것도 뽑지 않는다.** JS 정규식을 Java 에
|
|
608
|
+
* 들이대면 조용히 헛것을 잡는다 — 모르는 것은 모른다고 두는 편이 낫다 (D5).
|
|
609
|
+
*
|
|
610
|
+
* 판정은 `lang` 하나로만 한다. 처음엔 `parsed` 플래그를 봤는데, 손으로
|
|
611
|
+
* 파일 객체를 만드는 호출부(테스트 등)가 그 필드를 모르면 조용히 엣지가
|
|
612
|
+
* 통째로 사라졌다. 진실의 출처는 하나여야 한다.
|
|
613
|
+
*/
|
|
614
|
+
export function extractImports(lang, text, filePath = '') {
|
|
615
|
+
const rule = LANGS[lang]
|
|
616
|
+
if (!rule || typeof text !== 'string') return new Set()
|
|
617
|
+
// 🔴 경로를 함께 넘긴다. 러스트는 **파일 이름에 따라** 자식 모듈이 사는
|
|
618
|
+
// 디렉터리가 달라지므로 본문만으로는 명세를 만들 수 없다 (rsModuleDir).
|
|
619
|
+
return rule.extract(text, filePath)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** 이 언어의 명세가 디렉터리를 가리키나. */
|
|
623
|
+
export function resolvesToDir(lang) {
|
|
624
|
+
return LANGS[lang]?.resolve === 'dir'
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** 이 언어의 명세를 어떤 방식으로 파일로 바꾸나. 모르는 언어는 null. */
|
|
628
|
+
export function resolveKind(lang) {
|
|
629
|
+
return LANGS[lang]?.resolve ?? null
|
|
630
|
+
}
|