feat: lint on bug-like show/set rules (#1634)

* feat: init linter

* feat: init structure

* feat: init tests

* feat: warn on bad set rules

* feat: refactor struct

* feat: run check

* chore: update message

* chore: add hints
This commit is contained in:
Myriad-Dreamin 2025-04-08 07:36:03 +08:00 committed by GitHub
parent ac506dcc31
commit d6bce89e68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 664 additions and 51 deletions

View file

@ -0,0 +1,31 @@
[package]
name = "tinymist-lint"
description = "A linter for Typst."
categories = ["compilers"]
keywords = ["api", "language", "typst"]
authors.workspace = true
version.workspace = true
license.workspace = true
edition.workspace = true
homepage.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
typst-library.workspace = true
typst.workspace = true
tinymist-std.workspace = true
tinymist-analysis.workspace = true
tinymist-world = { workspace = true, features = ["system"] }
parking_lot.workspace = true
comemo.workspace = true
serde.workspace = true
serde_json.workspace = true
base64.workspace = true
rayon.workspace = true
[dev-dependencies]
insta.workspace = true
[lints]
workspace = true

View file

@ -0,0 +1,394 @@
//! A linter for Typst.
use typst::{
diag::{eco_format, EcoString, SourceDiagnostic},
ecow::EcoVec,
syntax::{
ast::{self, AstNode},
Source, SyntaxNode,
},
};
/// A type alias for a vector of diagnostics.
type DiagnosticVec = EcoVec<SourceDiagnostic>;
/// Lints a Typst source and returns a vector of diagnostics.
pub fn lint_source(source: &Source) -> DiagnosticVec {
SourceLinter::new().lint(source.root())
}
struct SourceLinter {
diag: DiagnosticVec,
}
impl SourceLinter {
fn new() -> Self {
Self {
diag: EcoVec::new(),
}
}
fn lint(mut self, node: &SyntaxNode) -> DiagnosticVec {
if let Some(markup) = node.cast::<ast::Markup>() {
self.exprs(markup.exprs());
} else if let Some(expr) = node.cast() {
self.expr(expr);
}
self.diag
}
fn exprs<'a>(&mut self, exprs: impl Iterator<Item = ast::Expr<'a>>) -> Option<()> {
for expr in exprs {
self.expr(expr);
}
Some(())
}
fn exprs_untyped(&mut self, to_untyped: &SyntaxNode) -> Option<()> {
for expr in to_untyped.children() {
if let Some(expr) = expr.cast() {
self.expr(expr);
}
}
Some(())
}
fn expr(&mut self, node: ast::Expr) -> Option<()> {
match node {
ast::Expr::Parenthesized(expr) => self.expr(expr.expr()),
ast::Expr::Code(expr) => self.exprs(expr.body().exprs()),
ast::Expr::Content(expr) => self.exprs(expr.body().exprs()),
ast::Expr::Equation(expr) => self.exprs(expr.body().exprs()),
ast::Expr::Math(expr) => self.exprs(expr.exprs()),
ast::Expr::Text(..) => None,
ast::Expr::Space(..) => None,
ast::Expr::Linebreak(..) => None,
ast::Expr::Parbreak(..) => None,
ast::Expr::Escape(..) => None,
ast::Expr::Shorthand(..) => None,
ast::Expr::SmartQuote(..) => None,
ast::Expr::Raw(..) => None,
ast::Expr::Link(..) => None,
ast::Expr::Label(..) => None,
ast::Expr::Ref(..) => None,
ast::Expr::None(..) => None,
ast::Expr::Auto(..) => None,
ast::Expr::Bool(..) => None,
ast::Expr::Int(..) => None,
ast::Expr::Float(..) => None,
ast::Expr::Numeric(..) => None,
ast::Expr::Str(..) => None,
ast::Expr::MathText(..) => None,
ast::Expr::MathShorthand(..) => None,
ast::Expr::MathAlignPoint(..) => None,
ast::Expr::MathPrimes(..) => None,
ast::Expr::MathRoot(..) => None,
ast::Expr::Strong(content) => self.exprs(content.body().exprs()),
ast::Expr::Emph(content) => self.exprs(content.body().exprs()),
ast::Expr::Heading(content) => self.exprs(content.body().exprs()),
ast::Expr::List(content) => self.exprs(content.body().exprs()),
ast::Expr::Enum(content) => self.exprs(content.body().exprs()),
ast::Expr::Term(content) => {
self.exprs(content.term().exprs());
self.exprs(content.description().exprs())
}
ast::Expr::MathDelimited(content) => self.exprs(content.body().exprs()),
ast::Expr::MathAttach(..) | ast::Expr::MathFrac(..) => {
self.exprs_untyped(node.to_untyped())
}
ast::Expr::Ident(expr) => self.ident(expr),
ast::Expr::MathIdent(expr) => self.math_ident(expr),
ast::Expr::Array(expr) => self.array(expr),
ast::Expr::Dict(expr) => self.dict(expr),
ast::Expr::Unary(expr) => self.unary(expr),
ast::Expr::Binary(expr) => self.binary(expr),
ast::Expr::FieldAccess(expr) => self.field_access(expr),
ast::Expr::FuncCall(expr) => self.func_call(expr),
ast::Expr::Closure(expr) => self.closure(expr),
ast::Expr::Let(expr) => self.let_binding(expr),
ast::Expr::DestructAssign(expr) => self.destruct_assign(expr),
ast::Expr::Set(expr) => self.set(expr),
ast::Expr::Show(expr) => self.show(expr),
ast::Expr::Contextual(expr) => self.contextual(expr),
ast::Expr::Conditional(expr) => self.conditional(expr),
ast::Expr::While(expr) => self.while_loop(expr),
ast::Expr::For(expr) => self.for_loop(expr),
ast::Expr::Import(expr) => self.import(expr),
ast::Expr::Include(expr) => self.include(expr),
ast::Expr::Break(expr) => self.loop_break(expr),
ast::Expr::Continue(expr) => self.loop_continue(expr),
ast::Expr::Return(expr) => self.func_return(expr),
}
}
fn ident(&mut self, _expr: ast::Ident<'_>) -> Option<()> {
Some(())
}
fn math_ident(&mut self, _expr: ast::MathIdent<'_>) -> Option<()> {
Some(())
}
fn import(&mut self, _expr: ast::ModuleImport<'_>) -> Option<()> {
Some(())
}
fn include(&mut self, _expr: ast::ModuleInclude<'_>) -> Option<()> {
Some(())
}
fn array(&mut self, expr: ast::Array<'_>) -> Option<()> {
self.exprs_untyped(expr.to_untyped())
}
fn dict(&mut self, expr: ast::Dict<'_>) -> Option<()> {
self.exprs_untyped(expr.to_untyped())
}
fn unary(&mut self, expr: ast::Unary<'_>) -> Option<()> {
self.expr(expr.expr())
}
fn binary(&mut self, expr: ast::Binary<'_>) -> Option<()> {
self.expr(expr.lhs());
self.expr(expr.rhs())
}
fn field_access(&mut self, expr: ast::FieldAccess<'_>) -> Option<()> {
self.expr(expr.target())
}
fn func_call(&mut self, expr: ast::FuncCall<'_>) -> Option<()> {
self.exprs_untyped(expr.args().to_untyped());
self.expr(expr.callee())
}
fn closure(&mut self, expr: ast::Closure<'_>) -> Option<()> {
self.exprs_untyped(expr.params().to_untyped());
self.expr(expr.body())
}
fn let_binding(&mut self, expr: ast::LetBinding<'_>) -> Option<()> {
self.expr(expr.init()?)
}
fn destruct_assign(&mut self, expr: ast::DestructAssignment<'_>) -> Option<()> {
self.expr(expr.value())
}
fn set(&mut self, expr: ast::SetRule<'_>) -> Option<()> {
if let Some(target) = expr.condition() {
self.expr(target);
}
self.exprs_untyped(expr.args().to_untyped());
self.expr(expr.target())
}
fn show(&mut self, expr: ast::ShowRule<'_>) -> Option<()> {
if let Some(target) = expr.selector() {
self.expr(target);
}
let transform = expr.transform();
match transform {
ast::Expr::Code(..) | ast::Expr::Content(..) => {
self.buggy_show(transform, BuggyShowLoc::Show(expr))
}
_ => None,
};
self.expr(transform)
}
fn contextual(&mut self, expr: ast::Contextual<'_>) -> Option<()> {
self.expr(expr.body())
}
fn conditional(&mut self, expr: ast::Conditional<'_>) -> Option<()> {
self.expr(expr.condition());
let if_body = expr.if_body();
self.buggy_show(if_body, BuggyShowLoc::IfTrue(expr));
self.expr(if_body);
if let Some(else_body) = expr.else_body() {
self.buggy_show(else_body, BuggyShowLoc::IfFalse(expr));
self.expr(else_body);
}
Some(())
}
fn while_loop(&mut self, expr: ast::WhileLoop<'_>) -> Option<()> {
self.expr(expr.condition());
let body = expr.body();
self.buggy_show(body, BuggyShowLoc::While(expr));
self.expr(body)
}
fn for_loop(&mut self, expr: ast::ForLoop<'_>) -> Option<()> {
self.expr(expr.iterable());
let body = expr.body();
self.buggy_show(body, BuggyShowLoc::For(expr));
self.expr(body)
}
fn loop_break(&mut self, _expr: ast::LoopBreak<'_>) -> Option<()> {
Some(())
}
fn loop_continue(&mut self, _expr: ast::LoopContinue<'_>) -> Option<()> {
Some(())
}
fn func_return(&mut self, _expr: ast::FuncReturn<'_>) -> Option<()> {
Some(())
}
fn buggy_show(&mut self, expr: ast::Expr, loc: BuggyShowLoc) -> Option<()> {
if self.only_set(expr) {
let sets = match expr {
ast::Expr::Code(block) => block
.body()
.exprs()
.filter(|it| is_show_set(*it))
.collect::<Vec<_>>(),
ast::Expr::Content(block) => block
.body()
.exprs()
.filter(|it| is_show_set(*it))
.collect::<Vec<_>>(),
_ => return None,
};
for (idx, set) in sets.iter().enumerate() {
let msg = match set {
ast::Expr::Set(..) => "This set statement doesn't take effect.",
ast::Expr::Show(..) => "This show statement doesn't take effect.",
_ => continue,
};
let mut warning = SourceDiagnostic::warning(set.span(), msg);
if idx == 0 {
warning.hint(loc.hint(*set));
}
self.diag.push(warning);
}
return None;
}
Some(())
}
fn only_set(&mut self, expr: ast::Expr) -> bool {
let mut has_set = false;
match expr {
ast::Expr::Code(block) => {
for it in block.body().exprs() {
if is_show_set(it) {
has_set = true;
} else {
return false;
}
}
}
ast::Expr::Content(block) => {
for it in block.body().exprs() {
if is_show_set(it) {
has_set = true;
} else if !it.to_untyped().kind().is_trivia() {
return false;
}
}
}
_ => {
return false;
}
}
has_set
}
}
enum BuggyShowLoc<'a> {
Show(ast::ShowRule<'a>),
IfTrue(ast::Conditional<'a>),
IfFalse(ast::Conditional<'a>),
While(ast::WhileLoop<'a>),
For(ast::ForLoop<'a>),
}
impl BuggyShowLoc<'_> {
fn hint(&self, show_set: ast::Expr<'_>) -> EcoString {
match self {
BuggyShowLoc::Show(show_parent) => {
if let ast::Expr::Show(show) = show_set {
eco_format!(
"consider changing parent to `show {}: it => {{ {}; it }}`",
match show_parent.selector() {
Some(selector) => selector.to_untyped().clone().into_text(),
None => "".into(),
},
show.to_untyped().clone().into_text()
)
} else {
eco_format!(
"consider changing parent to `show {}: {}`",
match show_parent.selector() {
Some(selector) => selector.to_untyped().clone().into_text(),
None => "".into(),
},
show_set.to_untyped().clone().into_text()
)
}
}
BuggyShowLoc::IfTrue(conditional) | BuggyShowLoc::IfFalse(conditional) => {
let neg = if matches!(self, BuggyShowLoc::IfTrue(..)) {
""
} else {
"not "
};
if let ast::Expr::Show(show) = show_set {
eco_format!(
"consider changing parent to `show {}: if {neg}({}) {{ .. }}`",
match show.selector() {
Some(selector) => selector.to_untyped().clone().into_text(),
None => "".into(),
},
conditional.condition().to_untyped().clone().into_text()
)
} else {
eco_format!(
"consider changing parent to `{} if {neg}({})`",
show_set.to_untyped().clone().into_text(),
conditional.condition().to_untyped().clone().into_text()
)
}
}
BuggyShowLoc::While(w) => {
eco_format!(
"consider changing parent to `show: it => if {} {{ {}; it }}`",
w.condition().to_untyped().clone().into_text(),
show_set.to_untyped().clone().into_text()
)
}
BuggyShowLoc::For(f) => {
eco_format!(
"consider changing parent to `show: {}.fold(it => it, (style-it, {}) => it => {{ {}; style-it(it) }})`",
f.iterable().to_untyped().clone().into_text(),
f.pattern().to_untyped().clone().into_text(),
show_set.to_untyped().clone().into_text()
)
}
}
}
}
fn is_show_set(it: ast::Expr) -> bool {
matches!(it, ast::Expr::Set(..) | ast::Expr::Show(..))
}

View file

@ -48,6 +48,7 @@ tinymist-analysis.workspace = true
tinymist-derive.workspace = true
tinymist-std.workspace = true
tinymist-l10n.workspace = true
tinymist-lint.workspace = true
typst.workspace = true
typst-shim.workspace = true
unscanny.workspace = true

View file

@ -598,3 +598,27 @@ mod call_info_tests {
}
}
}
#[cfg(test)]
mod lint_tests {
use std::collections::BTreeMap;
use crate::tests::*;
#[test]
fn test() {
snapshot_testing("lint", &|ctx, path| {
let source = ctx.source_by_path(&path).unwrap();
let result = tinymist_lint::lint_source(&source);
let result =
crate::diagnostics::CheckDocWorker::new(&ctx.world, ctx.position_encoding())
.convert_all(result.iter());
let result = result
.into_iter()
.map(|(k, v)| (file_path_(&k), v))
.collect::<BTreeMap<_, _>>();
assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC));
});
}
}

View file

@ -1,7 +1,8 @@
use std::borrow::Cow;
use tinymist_project::LspWorld;
use typst::syntax::Span;
use tinymist_world::vfs::WorkspaceResolver;
use typst::{diag::SourceDiagnostic, syntax::Span};
use crate::{prelude::*, LspWorldExt};
@ -13,15 +14,29 @@ pub type DiagnosticsMap = HashMap<Url, EcoVec<Diagnostic>>;
type TypstDiagnostic = typst::diag::SourceDiagnostic;
type TypstSeverity = typst::diag::Severity;
/// Converts a list of Typst diagnostics to LSP diagnostics,
/// with potential refinements on the error messages.
pub fn check_doc<'a>(
world: &LspWorld,
errors: impl IntoIterator<Item = &'a TypstDiagnostic>,
position_encoding: PositionEncoding,
) -> DiagnosticsMap {
CheckDocWorker::new(world, position_encoding)
.check()
.convert_all(errors)
}
/// Context for converting Typst diagnostics to LSP diagnostics.
struct LocalDiagContext<'a> {
pub(crate) struct CheckDocWorker<'a> {
/// The world surface for Typst compiler.
pub world: &'a LspWorld,
/// The position encoding for the source.
pub position_encoding: PositionEncoding,
/// Results
pub results: DiagnosticsMap,
}
impl std::ops::Deref for LocalDiagContext<'_> {
impl std::ops::Deref for CheckDocWorker<'_> {
type Target = LspWorld;
fn deref(&self) -> &Self::Target {
@ -29,37 +44,64 @@ impl std::ops::Deref for LocalDiagContext<'_> {
}
}
/// Converts a list of Typst diagnostics to LSP diagnostics,
/// with potential refinements on the error messages.
pub fn convert_diagnostics<'a>(
world: &LspWorld,
errors: impl IntoIterator<Item = &'a TypstDiagnostic>,
position_encoding: PositionEncoding,
) -> DiagnosticsMap {
let ctx = LocalDiagContext {
world,
position_encoding,
};
let kvs = errors
.into_iter()
.flat_map(|error| {
convert_diagnostic(&ctx, error)
.map_err(move |conversion_err| {
log::error!("could not convert Typst error to diagnostic: {conversion_err:?} error to convert: {error:?}");
})
});
let mut lookup = HashMap::new();
for (key, val) in kvs {
lookup.entry(key).or_insert_with(EcoVec::new).push(val);
impl<'w> CheckDocWorker<'w> {
/// Creates a new `CheckDocWorker` instance.
pub fn new(world: &'w LspWorld, position_encoding: PositionEncoding) -> Self {
Self {
world,
position_encoding,
results: DiagnosticsMap::default(),
}
}
lookup
/// Runs code check on the document.
pub fn check(mut self) -> Self {
for dep in self.world.depended_files() {
if WorkspaceResolver::is_package_file(dep) {
continue;
}
let Ok(source) = self.world.source(dep) else {
continue;
};
let res = lint_source(&source);
if !res.is_empty() {
for diag in res {
self.handle(&diag);
}
}
}
self
}
/// Converts a list of Typst diagnostics to LSP diagnostics.
pub fn convert_all<'a>(
mut self,
errors: impl IntoIterator<Item = &'a TypstDiagnostic>,
) -> DiagnosticsMap {
for diag in errors {
self.handle(diag);
}
self.results
}
/// Converts a list of Typst diagnostics to LSP diagnostics.
pub fn handle(&mut self, diag: &TypstDiagnostic) {
match convert_diagnostic(self, diag) {
Ok((uri, diagnostic)) => {
self.results.entry(uri).or_default().push(diagnostic);
}
Err(error) => {
log::error!("Failed to convert Typst diagnostic: {error:?}");
}
}
}
}
fn convert_diagnostic(
ctx: &LocalDiagContext,
ctx: &CheckDocWorker,
typst_diagnostic: &TypstDiagnostic,
) -> anyhow::Result<(Url, Diagnostic)> {
let typst_diagnostic = {
@ -102,7 +144,7 @@ fn convert_diagnostic(
}
fn tracepoint_to_relatedinformation(
ctx: &LocalDiagContext,
ctx: &CheckDocWorker,
tracepoint: &Spanned<Tracepoint>,
position_encoding: PositionEncoding,
) -> anyhow::Result<Option<DiagnosticRelatedInformation>> {
@ -127,7 +169,7 @@ fn tracepoint_to_relatedinformation(
}
fn diagnostic_related_information(
project: &LocalDiagContext,
project: &CheckDocWorker,
typst_diagnostic: &TypstDiagnostic,
position_encoding: PositionEncoding,
) -> anyhow::Result<Vec<DiagnosticRelatedInformation>> {
@ -145,7 +187,7 @@ fn diagnostic_related_information(
}
fn diagnostic_span_id(
ctx: &LocalDiagContext,
ctx: &CheckDocWorker,
typst_diagnostic: &TypstDiagnostic,
) -> (TypstFileId, Span) {
iter::once(typst_diagnostic.span)
@ -238,3 +280,8 @@ impl DiagnosticRefiner for OutOfRootHintRefiner {
raw.with_hint("Cannot read file outside of project root.")
}
}
#[comemo::memoize]
fn lint_source(source: &Source) -> EcoVec<SourceDiagnostic> {
tinymist_lint::lint_source(source)
}

View file

@ -0,0 +1,6 @@
#if false {
set text(red)
}
123

View file

@ -0,0 +1,6 @@
#if false {
show: text(red)
}
123

View file

@ -0,0 +1,6 @@
#show: {
set text(red)
}
123

View file

@ -0,0 +1,6 @@
#show raw: {
set text(red)
}
123

View file

@ -0,0 +1,16 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/lint/if_set.typ
---
{
"s0.typ": [
{
"message": "This set statement doesn't take effect.\nHint: consider changing parent to `set text(red) if (false)`",
"range": "1:2:1:15",
"relatedInformation": [],
"severity": 2,
"source": "typst"
}
]
}

View file

@ -0,0 +1,16 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/lint/if_show.typ
---
{
"s0.typ": [
{
"message": "This show statement doesn't take effect.\nHint: consider changing parent to `show : if (false) { .. }`",
"range": "1:2:1:17",
"relatedInformation": [],
"severity": 2,
"source": "typst"
}
]
}

View file

@ -0,0 +1,16 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/lint/show_set.typ
---
{
"s0.typ": [
{
"message": "This set statement doesn't take effect.\nHint: consider changing parent to `show : set text(red)`",
"range": "1:2:1:15",
"relatedInformation": [],
"severity": 2,
"source": "typst"
}
]
}

View file

@ -0,0 +1,16 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/lint/show_set2.typ
---
{
"s0.typ": [
{
"message": "This set statement doesn't take effect.\nHint: consider changing parent to `show raw: set text(red)`",
"range": "1:2:1:15",
"relatedInformation": [],
"severity": 2,
"source": "typst"
}
]
}

View file

@ -477,12 +477,16 @@ impl Redact for RedactFields {
}
pub(crate) fn file_path(uri: &str) -> String {
file_path_(&lsp_types::Url::parse(uri).unwrap())
}
pub(crate) fn file_path_(uri: &lsp_types::Url) -> String {
let root = if cfg!(windows) {
PathBuf::from("C:\\root")
} else {
PathBuf::from("/root")
};
let uri = lsp_types::Url::parse(uri).unwrap().to_file_path().unwrap();
let uri = uri.to_file_path().unwrap();
let abs_path = Path::new(&uri).strip_prefix(root).map(|p| p.to_owned());
let rel_path =
abs_path.unwrap_or_else(|_| Path::new("-").join(Path::new(&uri).iter().last().unwrap()));

View file

@ -438,27 +438,32 @@ impl CompileHandlerImpl {
}
fn notify_diagnostics(&self, snap: &LspCompiledArtifact) {
let world = snap.world();
let dv = ProjVersion {
id: snap.id().clone(),
revision: world.revision().get(),
revision: snap.world().revision().get(),
};
// todo: better way to remove diagnostics
// todo: check all errors in this file
let valid = !world.entry_state().is_inactive();
let diagnostics = valid.then(|| {
let diagnostics = tinymist_query::convert_diagnostics(
world,
snap.diagnostics(),
self.analysis.position_encoding,
);
let valid = !snap.world().entry_state().is_inactive();
if !valid {
self.push_diagnostics(dv, None);
return;
}
let snap = snap.clone();
let editor_tx = self.editor_tx.clone();
let enc = self.analysis.position_encoding;
rayon::spawn(move || {
let world = snap.world();
// todo: check all errors in this file
let diagnostics = tinymist_query::check_doc(world, snap.diagnostics(), enc);
log::trace!("notify diagnostics({dv:?}): {diagnostics:#?}");
diagnostics
});
self.push_diagnostics(dv, diagnostics);
editor_tx
.send(EditorRequest::Diag(dv, Some(diagnostics)))
.log_error("failed to send diagnostics");
});
}
}
@ -619,8 +624,6 @@ impl CompileHandler<LspCompilerFeat, ProjectInsStateExt> for CompileHandlerImpl
.log_error("failed to print diagnostics");
}
self.notify_diagnostics(art);
self.client.interrupt(LspInterrupt::Compiled(art.clone()));
self.export.signal(art);
@ -631,6 +634,8 @@ impl CompileHandler<LspCompilerFeat, ProjectInsStateExt> for CompileHandlerImpl
} else {
log::info!("Project: no preview for {:?}", art.id());
}
self.notify_diagnostics(art);
}
}

View file

@ -177,8 +177,7 @@ async fn trace_main(
let timings = writer.into_inner().unwrap();
let handle = &state.project;
let diagnostics =
tinymist_query::convert_diagnostics(w, diags.iter(), handle.analysis.position_encoding);
let diagnostics = tinymist_query::check_doc(w, diags.iter(), handle.analysis.position_encoding);
let rpc_kind = rpc_kind.as_str();