Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/source/routes/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,9 +275,9 @@ impl<'a> Ctx<'a> {
.map(|(_, id)| id.clone())
}

/// 계약의 위치(루트 기준 경로, 1 기반 줄, UTF-16 열)다.
/// 계약의 위치(루트 기준 경로, 1 기반 줄, UTF-8 바이트 열)다.
pub fn locate(&self, loc: &Loc) -> Option<BridgeLocation> {
self.locate_with(loc, |c| c.len_utf16() as u32)
self.locate_with(loc, |c| c.len_utf8() as u32)
}

/// 계약의 위치 — 열은 GRAPH-EXCHANGE가 정한 UTF-8 바이트 오프셋 + 1이다.
Expand Down
39 changes: 33 additions & 6 deletions src/source/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1625,15 +1625,26 @@ fn sql_relations(text: &str) -> (Vec<String>, bool) {
} else {
match read_qualified_name(&tokens, j) {
Some((name, next)) => {
if buffered_grant {
buf.push(name);
} else if seen.insert(name.clone()) {
out.push(name);
}
for c in consumed.iter_mut().take(next).skip(j) {
*c = true;
}
next
if matches!(word.as_str(), "from" | "join")
&& tokens.get(next).is_some_and(|t| !t.quoted && t.text == "(")
{
// 함수의 내부 관계를 추측하지 않고 미해석 근거를 남긴다.
unresolved = true;
match skip_parens(&tokens, next) {
Some(end) => end,
None => break,
}
} else {
if buffered_grant {
buf.push(name);
} else if seen.insert(name.clone()) {
out.push(name);
}
next
}
}
None => {
// 이름 자리에 절 키워드가 오는 것(`DO UPDATE SET`,
Expand Down Expand Up @@ -2057,6 +2068,22 @@ mod tests {

/// SQL 렉서·관계 추출의 표 테스트 — gartograph의 schema_test.go와
/// 같은 경계를 검증한다.
#[test]
fn sql_table_valued_functions_are_not_relations() {
assert_eq!(
sql_relations("SELECT * FROM pragma_table_info('t')"),
(vec![], true)
);
assert_eq!(
sql_relations("SELECT * FROM users JOIN main.pragma_table_info('t') p ON true"),
(vec!["users".into()], true)
);
assert_eq!(
sql_relations("SELECT * FROM pragma_table_info('t') AS p, users"),
(vec!["users".into()], true)
);
}

#[test]
fn sql_relations_reads_keywords_and_lists() {
let cases: &[(&str, &[&str])] = &[
Expand Down
19 changes: 19 additions & 0 deletions tests/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -799,3 +799,22 @@ pub fn build() {
.iter()
.any(|l| l.starts_with("route-coverage: an App passed to a function")));
}

/// 비ASCII 문자가 앞에 있어도 위치 열은 UTF-8 바이트다.
#[test]
fn server_route_columns_are_utf8_bytes() {
let line =
"pub fn app() -> Router { let _ = \"한😀\"; Router::new().route(\"/items\", get(h)) }";
let source = format!("use axum::{{Router, routing::get}};\nasync fn h() {{}}\n{line}\n");
let dir = temp_crate("utf8columns", &[("axum", "0.8.0")], &[("lib.rs", &source)]);
let doc = doc_of(&dir, None);
let fact = &doc["facts"][0];
assert!(fact.is_object(), "expected route fact: {doc}");
let column = fact["location"]["column"].as_u64().unwrap() as usize;
let suffix = &line.as_bytes()[column - 1..];
assert!(
suffix.starts_with(b"h") || suffix.starts_with(b"get") || suffix.starts_with(b"\"/items"),
"wrong byte column {column}"
);
let _ = std::fs::remove_dir_all(&dir);
}
Loading