`Code.Fragment`
Code.Fragment
텍스트로 된 코드 조각(fragment)을 분석하고 가능할 때마다 유용한 정보를 추출하는 편의 기능들을 제공하는 모듈이에요.
이 모듈은 실험적(experimental)으로 간주해야 합니다.
출처:
Code.Fragment
본문
에디터에서 코드 자동완성이나 툴팁 같은 기능을 만들다 보면, "완성되지 않은 채 입력 중인 코드"를 분석해야 할 때가 있어요. 사용자가 막 절반을 타이핑한 상태라서 문법적으로 완전한 코드가 아니죠. Code.Fragment 모듈은 바로 이런 상황에서 쓰라고 만들어진 도구예요. 입력 중인 코드 조각을 받아서, 그 안에 담길 수 있는 정보를 최대한 뽑아내 줍니다.
핵심 함수는 세 가지예요. container_cursor_to_quoted/2는 커서 위치를 AST로 바꿔주고, cursor_context/2는 커서가 무엇을 의미하는지 문맥을 알려주며, surround_context/3는 커서를 둘러싼 식별자의 시작과 끝 위치를 돌려주죠.
container_cursor_to_quoted/2 — 커서를 포함한 AST로
@spec container_cursor_to_quoted(List.Chars.t(), container_cursor_to_quoted_opts()) ::
{:ok, Macro.t()}
| {:error, {location :: keyword(), binary() | {binary(), binary()}, binary()}}
이 함수는 Elixir 코드 조각(커서 위치를 나타내는 문자열)을 받아, 커서 위치를 나타내는 특별한 __cursor__() 노드를 부모(container) 안에 포함한 AST로 변환해요. 예를 들어 다음 코드가 입력으로 주어지면:
max(some_value,
이 함수는 다음과 같은 AST를 반환합니다:
max(some_value, __cursor__())
즉 열린 괄호를 닫고 커서 위치를 넣을 수 있어요. 커서 위치에 있으면서 부모가 아닌 다른 내용은 버려집니다. 예를 들어 이렇게 입력하면:
max(some_value, another_val
여전히 같은 AST를 반환합니다:
max(some_value, __cursor__())
마찬가지로 이것만 주어져도:
max(some_va
이렇게 반환해요:
max(__cursor__())
괄호가 없는 호출도 지원합니다. 괄호가 암시된다고 가정하기 때문이에요.
튜플, 리스트, 맵, 이진(binary) 모두 커서 위치를 유지합니다:
max(some_value, [1, 2,
다음 AST를 반환하죠:
max(some_value, [1, 2, __cursor__()])
키워드 목록(과 do-end 블록)도 유지됩니다. 다음 입력들이:
if(some_value, do:
if(some_value, do: :token
if(some_value, do: 1 + val
모두 이렇게 반환합니다:
if(some_value, do: __cursor__())
다중 줄 블록에서는 이전 줄들이 모두 보존됩니다.
이 함수가 반환한 AST는 평가하기엔 안전하지 않지만, 분석하고 확장할 수는 있어요.
예시를 몇 개 볼게요. 함수 호출:
iex> Code.Fragment.container_cursor_to_quoted("max(some_value, ")
{:ok, {:max, [line: 1], [{:some_value, [line: 1], nil}, {:__cursor__, [line: 1], []}]}}
컨테이너(리스트):
iex> Code.Fragment.container_cursor_to_quoted("[some, value")
{:ok, [{:some, [line: 1], nil}, {:__cursor__, [line: 1], []}]}
표현식이 완전하면 전체 표현식은 버려지고 부모만 반환됩니다:
iex> Code.Fragment.container_cursor_to_quoted("if(is_atom(var)")
{:ok, {:if, [line: 1], [{:__cursor__, [line: 1], []}]}}
즉 완전한 표현식 자체는 커서만 반환합니다:
iex> Code.Fragment.container_cursor_to_quoted("if(is_atom(var))")
{:ok, {:__cursor__, [line: 1], []}}
연산자도 Elixir v1.15부터 포함됩니다:
iex> Code.Fragment.container_cursor_to_quoted("foo +")
{:ok, {:+, [line: 1], [{:foo, [line: 1], nil}, {:__cursor__, [line: 1], []}]}}
->의 왼쪽을 제대로 파싱하려면(이것은 익명 함수와 do-end 블록 양쪽에 나타남), 나머지 내용을 trailing_fragment 옵션으로 주어야 합니다:
iex> Code.Fragment.container_cursor_to_quoted("fn x", trailing_fragment: " -> :ok end")
{:ok, {:fn, [line: 1], [{:->, [line: 1], [[{:__cursor__, [line: 1], []}], :ok]}]}}
옵션은 다음과 같아요:
:file— 파싱 오류가 있을 때 보고할 파일명. 기본값은"nofile".:line— 파싱할 문자열의 시작 줄. 기본값1.:column— 파싱할 문자열의 시작 열. 기본값1.:columns—true이면 quoted 메타데이터에:column키를 붙임. 기본값false.:token_metadata—true이면 표현식 AST에 토큰 관련 메타데이터를 포함.do·end토큰, 닫는 토큰, 표현식의 끝, sigil의 구분자 등에 대한 메타데이터예요.t:Macro.metadata/0참고. 기본값false.:literal_encoder— AST의 리터럴을 인코딩하는 함수.Code.string_to_quoted/2의 문서를 참고하세요.:trailing_fragment(v1.18.0부터) — 커서 뒤의 나머지 내용. 익명 함수와->의 왼쪽을 올바르게 완성하는 데 필요.:preserve_sigils(v1.20.0부터) — sigil 커서 위치를 보존(아래 "sigil 추적" 참고).
sigil 추적하기
:preserve_sigils 옵션으로 sigil 안의 커서 위치를 추적할 수 있어요.
sigil이 갑자기 끝나면, sigil_* 호출의 두 번째 인자로 커서가 오게 됩니다:
iex> Code.Fragment.container_cursor_to_quoted("~r/foo", preserve_sigils: true)
{:ok,
{:sigil_r, [delimiter: "/", line: 1],
[{:<<>>, [line: 1], ["foo"]}, {:__cursor__, [line: 1, column: 7], []}]}}
sigil이 완성되고 수식자(modifier)가 0개 이상 있으면, 이전 구분자들이 모두 명시된 채 커서가 리스트 안에 중첩됩니다:
iex> Code.Fragment.container_cursor_to_quoted("~r/foo/i", preserve_sigils: true)
{:ok,
{:sigil_r, [delimiter: "/", line: 1],
[{:<<>>, [line: 1], ["foo"]}, [105, {:__cursor__, [line: 1, column: 9], []}]]}}
커서가 sigil 뒤에 있으면 다른 모든 것과 마찬가지로 버려집니다:
iex> Code.Fragment.container_cursor_to_quoted("~r/foo/i ", preserve_sigils: true)
{:ok, {:__cursor__, [line: 1], []}}
cursor_context/2 — 커서 문맥 파악
@spec cursor_context(List.Chars.t(), cursor_opts()) ::
{:alias, charlist()}
| {:alias, inside_alias, charlist()}
| {:block_keyword_or_binary_operator, charlist()}
| {:dot, inside_dot, charlist()}
| {:dot_arity, inside_dot, charlist()}
| {:dot_call, inside_dot, charlist()}
| :expr
| {:local_or_var, charlist()}
| {:local_arity, charlist()}
| {:local_call, charlist()}
| {:anonymous_call, inside_caller}
| {:capture_arg, charlist()}
| {:module_attribute, charlist()}
| {:operator, charlist()}
| {:operator_arity, charlist()}
| {:operator_call, charlist()}
| :none
| {:sigil, charlist()}
| {:struct, inside_struct}
| {:unquoted_atom, charlist()}
when ...
이 함수는 Elixir 코드 조각(커서 위치를 나타내는 문자열)을 받아, 그 문자열을 기반으로 가장 최근 토큰에 대한 문맥 정보를 제공해요. 이 함수의 반환값은 팁(tips), 제안(suggestions), 자동완성 기능을 제공하는 데 사용할 수 있습니다.
이 함수는 토큰을 대상으로 분석을 수행해요. 즉 구성 요소들이 서로 어떻게 중첩되는지는 이해하지 못합니다. 아래 "제한 사항" 섹션을 참고하세요.
새 릴리즈에서 새 커서 정보가 추가될 수 있으므로, 이 함수의 반환 타입을 처리할 때 catch-all 절을 추가하는 것을 고려하세요.
예시:
iex> Code.Fragment.cursor_context("")
:expr
iex> Code.Fragment.cursor_context("hello_wor")
{:local_or_var, ~c"hello_wor"}
반환값의 종류는 다양해요. 주요 몇 가지를 정리하면:
{:alias, charlist}— 문맥이 alias(중첩일 수 있음). 예:Hello.Wor또는HelloWor{:alias, inside_alias, charlist}—inside_alias가{:module_attribute, charlist}또는{:local_or_var, charlist}표현식인 alias. 예:__MODULE__.Submodule또는@hello.Submodule{:block_keyword_or_binary_operator, charlist}— 블록 키워드(do,end,after,catch,else,rescue) 또는 이진(binary) 연산자일 수 있음{:dot, inside_dot, charlist}— 점(dot) 문맥.inside_dot는{:var, charlist},{:alias, charlist},{:module_attribute, charlist},{:unquoted_atom, charlist}또는 점 자신. 변수가 주어지면 원격 호출 또는 맵 필드 접근일 수 있음. 예:Hello.wor,:hello.wor,hello.wor,Hello.nested.wor,hello.nested.wor,@hello.world{:dot_arity, inside_dot, charlist}— 점 arity 문맥. 예:Hello.world/,:hello.world/,hello.world/2,@hello.world/2{:dot_call, inside_dot, charlist}— 점 호출 문맥. 표현식 뒤에 괄호나 공백이 추가됨. 예:Hello.world(,:hello.world(,Hello.world,hello.world(,hello.world,@hello.world(:expr— 어떤 표현식이든 가능. 자동완성이 alias, 로컬 또는 변수를 제안할 수 있음{:local_or_var, charlist}— 변수 또는 로컬(import 또는 local) 호출 문맥. 예:hello_wor{:local_arity, charlist}— 로컬(import 또는 local) arity 문맥. 예:hello_world/{:local_call, charlist}— 로컬(import 또는 local) 호출 문맥. 예:hello_world(와hello_world{:anonymous_call, inside_caller}— 익명 호출 문맥. 예:fun.(와@fun.({:module_attribute, charlist}— 모듈 속성 문맥. 예:@hello_wor{:operator, charlist}— 연산자 문맥. 예:+또는==.when같은 텍스트 연산자는 연산자로 나타나지 않고:local_or_var로 나타나요.@는 결코:operator가 아니고 항상:module_attribute{:operator_arity, charlist}— 연산자 arity 문맥. 연산자 뒤에/가 붙은 것. 예:+/,not/또는when/{:operator_call, charlist}— 연산자 호출 문맥. 연산자 뒤에 공백이 붙은 것. 예:left +,not또는x when:none— 가능한 문맥 없음{:sigil, charlist}— sigil 문맥.~나~s같은 sigil의 시작이거나,~>·~>>처럼~로 시작하는 연산자일 수 있음{:struct, inside_struct}— 구조체 문맥. 예:%,%UR또는%URI{:unquoted_atom, charlist}— 따옴표 없는 atom 문맥. 어떤 atom 또는 모듈을 나타내는 atom일 수 있음
완전한 예시 목록은 이 함수의 테스트 스위트를 살펴보는 것을 권장합니다.
제한 사항
분석은 현재 토큰에 기반하며, 입력의 마지막 줄을 분석해요. 예를 들어 이 코드는:
iex> Code.Fragment.cursor_context("%URI{")
:expr
:expr을 반환하는데, 이는 어떤 변수, 로컬 함수, alias도 사용될 수 있다는 뜻이에요. 하지만 우리가 구조체 안에 있으므로, 최선의 제안은 구조체 필드일 거예요. 그런 경우 container_cursor_to_quoted를 사용할 수 있는데, 이 함수는 커서가 현재 속한 AST의 컨테이너를 반환합니다. 그 AST를 분석해 필드 이름 완성을 제공할 수 있어요.
토큰 기반 구현의 결과로, 이 함수는 입력의 마지막 줄만 고려합니다. 즉 문자열, heredoc 등 안에서도 제안을 보여주는데, 이는 doctest나 참조 등에 도움이 되도록 의도된 것입니다.
surround_context/3 — 커서 주변 식별자 위치
@spec surround_context(List.Chars.t(), position(), cursor_opts()) ::
%{begin: position(), end: position(), context: context} | :none
when context: ...
이 함수는 Elixir 코드 조각과 position을 받아, 식별자의 시작과 끝을 담은 맵을 그 문맥과 함께 반환하고, 알려진 문맥이 없으면 :none을 반환해요. 이는 에디터에서 마우스 오버(호버)와 하이라이트 기능을 제공하는 데 유용합니다.
cursor_context/2와 surround_context/3의 차이는, 전자는 코드 조각의 표현식이 불완전하다고 가정하는 반면, 후자는 표현식이 완전하다고 가정한다는 점이에요. 예를 들어 cursor_context/2의 do는 키워드일 수도, 변수일 수도, 로컬 호출일 수도 있지만, surround_context/3에서는 do가 항상 키워드입니다.
position은 line과 column을 모두 담는데, 둘 다 1부터 시작해요. column은 둘러싼 표현식 앞에 위치해야 합니다. 예를 들어 foo 표현식은 column 1, 2, 3에 대해 뭔가를 반환하지만 4에 대해서는 반환하지 않아요:
foo
^ column 1
foo
^ column 2
foo
^ column 3
foo
^ column 4
반환된 맵은 표현식이 시작하는 column과, 표현식이 끝난 직후의 첫 column을 담습니다.
cursor_context/2와 유사하게 이 함수도 토큰 기반이며 모든 상황에서 정확하지 않을 수 있어요. 자세한 내용은 cursor_context/2의 "Return values"와 "Limitations" 섹션을 참고하세요.
예시:
iex> Code.Fragment.surround_context("foo", {1, 1})
%{begin: {1, 1}, context: {:local_or_var, ~c"foo"}, end: {1, 4}}
cursor_context/2와의 차이
surround_context/3는 복잡한 표현식을 포착하려 하기 때문에 cursor_context/2와 몇 가지 차이가 있어요:
dot_call/dot_arity와operator_call/operator_arity는 그 사이에 의미 있는 구분이 없으므로 각각dot과operator문맥으로 축약됨- 반면 이 함수는
local_call/local_arity와local_or_var사이에는 여전히 구분을 둠. 후자는 로컬이나 변수일 수 있기 때문 - 식별자 뒤에 따르는
@가 없으면{:operator, ~c"@"}로 반환됨(cursor_context/2의{:module_attribute, ~c""}와 대조적) - 이 함수는 빈 sigil
{:sigil, ~c""}이나 빈 구조체{:struct, ~c""}를 문맥으로 결코 반환하지 않음 - 이 함수는 키워드를
{:keyword, ~c"do"}로 반환함 - 이 함수는 결코
:expr을 반환하지 않음
완전한 예시 목록은 이 함수의 테스트 스위트를 참고하세요.
lines/1 — 줄 나누기
iex> Code.Fragment.lines("foo\r\nbar\r\nbaz")
["foo\r\n", "bar\r\n", "baz"]
iex> Code.Fragment.lines("foo\nbar\nbaz")
["foo\n", "bar\n", "baz"]
iex> Code.Fragment.lines("")
[""]
주어진 문자열의 줄 목록을 줄 끝(line ending)을 보존한 채 반환해요. Elixir 컴파일러가 인식하는 줄 끝만 고려합니다. 즉 \r\n과 \n이에요. 줄 끝 없이 줄을 얻고 싶다면 String.split(string, ["\r\n", "\n"])을 사용하세요. (v1.19.0부터)
더 알아보기
- 코드 조각 분석의 기반이 되는 AST 개념은
Code모듈 문서에서 다룹니다. Code.string_to_quoted/2를 참고하면:literal_encoder등 옵션의 의미를 더 자세히 알 수 있어요.- 이 모듈의 완전한 API 목록은 elixir hexdocs의 api-reference에서 확인하세요.