diff --git a/src/source/routes/common.rs b/src/source/routes/common.rs index d97a016..9fae7bb 100644 --- a/src/source/routes/common.rs +++ b/src/source/routes/common.rs @@ -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 { - 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이다. diff --git a/src/source/schema.rs b/src/source/schema.rs index 820eaa3..e51a9a7 100644 --- a/src/source/schema.rs +++ b/src/source/schema.rs @@ -1625,15 +1625,26 @@ fn sql_relations(text: &str) -> (Vec, 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`, @@ -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])] = &[ diff --git a/tests/routes.rs b/tests/routes.rs index 73a8159..d830238 100644 --- a/tests/routes.rs +++ b/tests/routes.rs @@ -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); +}