Cu puțin mai puțin de doi ani în urmă, pe baza lucrărilor lui Jim Hester și Kevin Ushey, Davis Vaughan a completat un fișier JavaScript foarte impactant pentru comunitatea R: o gramatică R pentru generatorul de analiză Tree-sitter. A primit chiar și o rundă de aplauze pentru asta în timpul unei discuții la user! Conferinta 2024! Deci, a fost încurajat pentru… reguli gramaticale într-un fișier JavaScript? 😅
Nu, publicul a fost încântat de experiență îmbunătățită a dezvoltatorului pentru R că acest fișier a fost deblocat. R instrumenting în jurul Tree-sitter este modul în care obții
- reformatarea prin aer și scame prin Jarl;
- completare automată sau ajutor la hover în Positron IDE;
- căutare mai bună pentru R pe GitHub;
- si mai mult!
În această postare, vom explica ce este Tree-sitter și cum instrumentele construite pe Tree-sitter vă pot aduce beneficii fluxului de lucru de dezvoltare R.
Analiza codului: ce este Tree-sitter?
Tree-sitter este un generator de analiză a codului scris în C, cu legături existente în mai multe limbi, inclusiv Rust (și R!).
Să derulăm puțin înapoi. Ce înseamnă să analizezi codul?
Practic, dat un șir de cod ca
a <- mean(x, na.rm = TRUE)
De unde știi asta mean este un nume de funcție, na.rm un nume de argument, TRUE un logic? Va trebui să analiza acel cod în ceea ce se numește un arbore de analiză. Faceți asta în cap când citiți codul R. 😸
R însuși poate analiza codul R, datorită gramaticii sale. Vezi, de exemplu, commit-ul care a introdus conducta nativă a lui R, care a necesitat extinderea sintaxei lui R modificându-i astfel gramatica.
Puteți folosi parse() şi getParseData() pentru a analiza codul R.
parse(
text = "a <- mean(x, na.rm = TRUE)",
keep.source = TRUE
) |>
getParseData()
#> line1 col1 line2 col2 id parent token terminal text
#> 23 1 1 1 26 23 0 expr FALSE
#> 1 1 1 1 1 1 3 SYMBOL TRUE a
#> 3 1 1 1 1 3 23 expr FALSE
#> 2 1 3 1 4 2 23 LEFT_ASSIGN TRUE <-
#> 21 1 6 1 26 21 23 expr FALSE
#> 4 1 6 1 9 4 6 SYMBOL_FUNCTION_CALL TRUE mean
#> 6 1 6 1 9 6 21 expr FALSE
#> 5 1 10 1 10 5 21 '(' TRUE (
#> 7 1 11 1 11 7 9 SYMBOL TRUE x
#> 9 1 11 1 11 9 21 expr FALSE
#> 8 1 12 1 12 8 21 ',' TRUE ,
#> 13 1 14 1 18 13 21 SYMBOL_SUB TRUE na.rm
#> 14 1 20 1 20 14 21 EQ_SUB TRUE =
#> 15 1 22 1 25 15 16 NUM_CONST TRUE TRUE
#> 16 1 22 1 25 16 21 expr FALSE
#> 17 1 26 1 26 17 21 ')' TRUE )
Sau puteți transforma aceleași date în XML folosind {xmlparsedata} lui Gábor Csárdi:
parse(
text = "a <- mean(x, na.rm = TRUE)",
keep.source = TRUE
) |>
xmlparsedata::xml_parse_data(pretty = TRUE) |>
xml2::read_xml() |>
as.character() |>
cat()
#>
#>
#>
#>
#> a
#>
#> <-
#>
#>
#> mean
#>
#> (
#>
#> x
#>
#> ,
#> na.rm
#> =
#>
#> TRUE
#>
#> )
#>
#>
#>
În ambele cazuri, recunoașteți cuvinte precum LEFT_ASSIGN sau SYMBOL_FUNCTION_CALL. Analizarea este un pas esențial înainte ca codul să fie executat efectiv, dar codul analizat poate fi folosit și în alte scopuri, cum ar fi analiza codului fără expresii regulate fragile (apelează o anumită funcție?), navigarea codului (trecând de la un apel de funcție la definiția acelei funcții) sau modificarea codului (înlocuirea tuturor aparițiilor unei funcții cu alta).
Acum, Tree-sitter efectuează aceeași analiză de cod, dar Mai repede în special datorită suportului pentru analiza incrementală – care este cheia pentru actualizarea arborelui de sintaxă pe măsură ce tastați în editorul dvs., de exemplu! Tree-sitter este agnostic prin faptul că poate analiza orice cod atâta timp cât există o gramatică pentru acesta (gândiți-vă la pluginurile Rosetta Stone). A fost folosit pentru multe limbi, ceea ce înseamnă că au fost construite multe instrumente în jurul lui.
Pentru ca Tree-sitter să „învețe” o nouă limbă, trebuie să îi oferiți un fișier care conține definiția sintaxei acelei limbi, ceea ce se numește un gramatică. Aici intră în joc fișierul JavaScript menționat mai sus de Davis Vaughan și colaboratori! Repo-ul treesitter-r, care oferă o traducere a gramaticii R în formatul așteptat de Tree-sitter, este baza tuturor instrumentelor prezentate în această postare care folosesc codul R ca intrare.
Iată cum să utilizați pachetul {treesitter} R pentru același cod ca mai devreme. Pachetul {treesitter} R ne permite să folosim Tree-sitter din R. Pentru a analiza codul R cu acesta, avem nevoie de language() funcție din {treesitter.r}.
library(treesitter)
#>
#> Attaching package: 'treesitter'
#> The following object is masked from 'package:base':
#>
#> range
language <- treesitter.r::language()
parser <- parser(language)
text <- "a <- mean(x, na.rm = TRUE)"
parser_parse(parser, text)
#>
#>
#> ── Text ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
#> a <- mean(x, na.rm = TRUE)
#>
#> ── S-Expression ────────────────────────────────────────────────────────────────────────────────────────────────────────────────
#> (program ((0, 0), (0, 26))
#> (binary_operator ((0, 0), (0, 26))
#> lhs: (identifier ((0, 0), (0, 1)))
#> operator: "<-" ((0, 2), (0, 4))
#> rhs: (call ((0, 5), (0, 26))
#> function: (identifier ((0, 5), (0, 9)))
#> arguments: (arguments ((0, 9), (0, 26))
#> open: "(" ((0, 9), (0, 10))
#> argument: (argument ((0, 10), (0, 11))
#> value: (identifier ((0, 10), (0, 11)))
#> )
#> (comma ((0, 11), (0, 12)))
#> argument: (argument ((0, 13), (0, 25))
#> name: (identifier ((0, 13), (0, 18)))
#> "=" ((0, 19), (0, 20))
#> value: (true ((0, 21), (0, 25)))
#> )
#> close: ")" ((0, 25), (0, 26))
#> )
#> )
#> )
#> )
Tree-sitter este calul de lucru al multor unelte, care sunt menționate în diagrama de mai jos. Toate depind de tree-sitter și de gramatica R oferită acestuia. Unele dintre ele sunt interfețe de linie de comandă (CLI), în timp ce altele sunt pachete R.

Codul de navigare interactiv: Positron IDE, GitHub
De asemenea, foarte utilă este utilizarea Tree-sitter de către Ark, nucleul R folosit în IDE-ul Positron. Ark este modul în care obțineți completarea automată și ajutor la hover în Positron. Videoclipul de mai jos arată cum puteți extinde selecția la pașii suplimentari ai unei conducte în Positron.
Acest caz de utilizare al lui Tree-sitter este, de asemenea, prezentat în slide-urile lui Davis. Vezi, de asemenea, discuția lui Lionel Henry și Davis Vaughan despre Ark la postul conf 2024, în special partea despre asistența pentru cod.
Alte medii de dezvoltare, cum ar fi Emacs, au și suport pentru Tree-sitter.
Cod de căutare/navigare
Puteți analiza și căuta codul R folosind pachetul R {treesitter} și sintaxa de interogare treesitter. Pachetul {treesitter} R este o dependență a pachetului {gander} de la Simon Couch, care este menit să fie folosit pentru o experiență mai bună cu LLM-urile atunci când scrieți codul R. Un alt caz de utilizare al pachetului {treesitter} R este extensia {igraph.r2cdocs} la {roxygen2} pentru pachetul {igraph}, care analizează tot codul igraph R pentru a putea apoi identifica, pentru fiecare funcție exportată, dacă apelează (in)direct o funcție al cărei nume se termină cu _implindicând un wrapper la o funcție C igraph ale cărei documente pot fi apoi legate din manualul funcției R.
Pachetul {pkgdepends} apelează Tree-sitter (C) pentru a detecta dependențele în fișiere. Mai jos îl rulăm pe sursa pachetului saperlipopette R.
pkgdepends::scan_deps(
"../../../../../CHAMPIONS/saperlipopette",
"../../../../../CHAMPIONS"
)
#>
#> Dependencies:
#> + brio @ R/blame.R, R/check-editor.R, R/clean-dir.R, R/committed-to-main.R, R/committed-to-wrong-branch.R, R/conflict…
#> + cli @ inst/exo_bisect-Rprofile.en.R, inst/exo_bisect-Rprofile.es.R, inst/exo_bisect-Rprofile.fr.R, inst/exo_blame-…
#> + devtools @ saperlipopette.Rproj
#> + fs @ R/blame.R, R/check-editor.R, R/clean-dir.R, R/committed-to-main.R, R/committed-to-wrong-branch.R, R/conflict…
#> + gert @ inst/exo_check_editor-Rprofile.en.R, inst/exo_check_editor-Rprofile.es.R, inst/exo_check_editor-Rprofile.fr.…
#> + knitr @ README.Rmd
#> + parsedate @ R/utils-git.R
#> + purrr @ R/create-all.R, R/debug.R, R/log-deleted-file.R, R/log-deleted-line.R, R/revparse.R, R/roxygen2.R, R/worktre…
#> + rlang @ R/create-all.R, R/roxygen2.R, R/utils-fs.R, R/utils-usethis.R, R/zzz.R
#> + rmarkdown @ README.Rmd, vignettes/saperlipopette.qmd
#> + roxygen2 @ R/roxygen2.R, saperlipopette.Rproj
#> + saperlipopette @ README.Rmd, vignettes/saperlipopette.qmd
#> + tibble @ R/roxygen2.R
#> + usethis @ R/blame.R, R/check-editor.R, R/clean-dir.R, R/committed-to-main.R, R/committed-to-wrong-branch.R, R/conflict…
#> + vctrs @ R/roxygen2.R
#> + withr @ R/blame.R, R/check-editor.R, R/clean-dir.R, R/committed-to-main.R, R/committed-to-wrong-branch.R, R/conflict…
#>
#> Test dependencies:
#> + fs @ tests/testthat/test-blame.R, tests/testthat/test-check-editor.R, tests/testthat/test-clean-dir.R, tests/test…
#> + gert @ tests/testthat/test-blame.R, tests/testthat/test-clean-dir.R, tests/testthat/test-committed-to-main.R, tests…
#> + rlang @ tests/testthat/test-blame.R, tests/testthat/test-check-editor.R, tests/testthat/test-clean-dir.R, tests/test…
#> + saperlipopette @ tests/testthat.R
#> + testthat @ tests/testthat.R
#> + withr @ tests/testthat/test-blame.R, tests/testthat/test-check-editor.R, tests/testthat/test-clean-dir.R, tests/test…
ast-grep este un instrument util construit pe Tree-sitter pentru căutarea și rescrierea codului, cu o sintaxă de interogare mai clară decât cea a lui Tree-sitter. Numele său amintește de grep, dar cu ast-grep nu trebuie să scriem expresii regulate fragile 😸. {astgrepr} de Etienne Bacher este un înveliș R pentru legăturile Rust ale ast-grep și este folosit în pachetul {flir} al lui Etienne pentru refactorizarea codului.
Interfața de linie de comandă (CLI) ast-grep în sine este prezentată într-o postare utilă pe blog a lui Emil Hvitfeldt, unde explică cum să documentezi utilizarea ast-grep pentru Claude.
Formatare și scame: Air, Jarl
Apropo de CLI…


Air, de Davis Vaughan și Lionel Henry, este un CLI construit pe Tree-sitter, în Rust. Ea reformatează cod uluitor de rapid.
Jarl, de Etienne Bacher, este un CLI construit pe Air, deci și pe Tree-sitter, în Rust. Ea scame şi remedieri cod, de asemenea uluitor de rapid. Poate chiar detecta cod inaccesibil, funcții neutilizate și definiții de funcții duplicate.
În ambele exemple, crearea de CLI împachetarea legăturilor Rust a fost mai eficientă decât crearea de pachete R care împachetează pachetul R {treesitter}, din mai multe motive:
- Rust CLI poate edita codul foarte rapid;
- CLI-urile sunt integrate în extensii pentru IDE-uri populare (de exemplu Positron);
- un CLI este mai ușor de instalat pe CI decât un pachet R care necesită, ei bine, o instalare R.
Mai multe instrumente
O scurtă mențiune a altor instrumente interesante pe care le-am explorat puțin mai puțin.
Configurare: {ts} pentru analizarea JSON și TOML (nu R!)
Pachetul {ts} de Gábor Csárdi este coloana vertebrală a două pachete R utilizate pentru editare și manipulare:
În comparație cu analizatorii existenți în R pentru aceste formate, aceste două pachete păstrează comentariile.
Cod de testare: {muttest}
Testarea mutațiilor este un fel de testare în care, să zicem, schimbi aleatoriu + cu - în codul tău (tu muta it) și îți faci testele pentru a vedea dacă prind mutantul. Pachetul {muttest} de Jakub Sobolewski este un pachet R pentru testarea mutațiilor, care depinde de pachetul R {treesitter}.
Cod diferit: difftastic
Difftastic CLI de Wilfred Hughes este „un instrument structural de diff care înțelege sintaxa”. ✨ Aceasta înseamnă că difftastic nu compară doar linia sau „cuvinte”, ci și sintaxa reală, uitându-se la liniile din jurul liniilor care s-au schimbat (în mod implicit, 3). Și mai bine, înțelege R din cutie. Vedeți această postare de blog cu exemple de diferențe ale codului R.
Concluzie: urmează mai multe?
În această postare, am prezentat o prezentare generală a instrumentelor bazate pe Tree-sitter pentru R sau în R.
Rețineți că acest ecosistem de instrumente este dezvoltat foarte activ, așa că unele instrumente ar putea veni și dispare. Cu toate acestea, ideea că conectarea gramaticii R într-un generator de analiză generală aduce caracteristici interesante pentru noi, dezvoltatorii R, va rămâne adevărată. Pot fi tu va contribui la acest ecosistem, fie printr-un instrument existent, fie prin crearea unuia nou?
