symbolique 0.1.0

Symbol table pipeline for language servers — parse, link, merge, and resolve symbols across files, built on the laburnum LSP framework.
// Copyright Two Neutron Stars Incorporated and contributors
// SPDX-License-Identifier: BlueOak-1.0.0

//! File-level symbol writer extension trait.
//!
//! Generated by [`define_symbol_writer!`] — see `symbol_writer_macro.rs` for
//! the shared implementation.

use {
  crate::{
    core::{Ident, Symbol, SymbolPath, SymbolSortKey, Value},
    partitions::{file::FileSymbols, symbols::Symbols},
  },
  laburnum::database::{
    HasPartition, PartitionKey, PartitionWriteContextRef, RecordHandle,
    storage::Partitions,
  },
};

super::symbol_writer_macro::define_symbol_writer! {
  trait_name: SymboliqueWriteExt,
  index_partition: FileSymbols,
  clear_method: clear_file_symbols,
  method_prefix: write_symbol_,
}

#[cfg(test)]
mod tests {
  use crate::{
    Symbol, SymbolVisibility,
    partitions::{
      FileSymbols, Symbols,
      test_support::{TestPartitions, TestStores},
    },
    test_helpers::{DV, SI, TP, test_span, test_span_cache},
  };
  use laburnum::database::{
    HasPartition, PartitionWriteContextRef,
    chunk::RecordWriter,
  };
  use super::SymboliqueWriteExt;

  fn make_writer() -> RecordWriter<TestPartitions> {
    RecordWriter::<TestPartitions>::new(laburnum::Ident::new("test"))
  }

  #[test]
  fn write_definition_round_trip() {
    let mut writer = make_writer();
    let mut cache = test_span_cache();
    let span = test_span(&mut cache, 0);

    let handle = {
      let mut ctx = PartitionWriteContextRef::new(&mut writer);
      ctx.write_symbol_definition::<DV, SI, TP>(
        "file|fn|my_func".to_string(),
        span,
        SI::new("my_func"),
        Some(DV::Integer(42)),
        SymbolVisibility::Public,
      )
    };

    let chunk = writer.build();
    let stores = chunk.storage();

    let sym_store =
      <TestStores as HasPartition<Symbols<DV, SI, TP>>>::store(stores);
    let record_ref = sym_store.get_by_handle(&handle);
    assert!(record_ref.is_some());

    let record = record_ref.as_ref().and_then(|r| r.record());
    assert!(record.is_some());
    match record {
      Some(Symbol::Definition {
        name, value, visibility,
      }) => {
        assert_eq!(name.as_str(), "my_func");
        assert_eq!(*value, Some(DV::Integer(42)));
        assert_eq!(*visibility, SymbolVisibility::Public);
      }
      _ => panic!("expected Definition"),
    }

    let idx_store =
      <TestStores as HasPartition<FileSymbols<DV, SI, TP>>>::store(stores);
    let entry = idx_store.index_get("file|fn|my_func");
    assert!(entry.is_some());
  }

  #[test]
  fn write_reference_round_trip() {
    let mut writer = make_writer();
    let mut cache = test_span_cache();
    let span = test_span(&mut cache, 1);

    let handle = {
      let mut ctx = PartitionWriteContextRef::new(&mut writer);
      ctx.write_symbol_reference::<DV, SI, TP>(
        "file|ref|other".to_string(),
        span,
        Some(SI::new("other")),
        "file|fn|other".to_string(),
        false,
        true,
      )
    };

    let chunk = writer.build();
    let stores = chunk.storage();

    let sym_store =
      <TestStores as HasPartition<Symbols<DV, SI, TP>>>::store(stores);
    let record = sym_store
      .get_by_handle(&handle)
      .as_ref()
      .and_then(|r| r.record())
      .cloned();
    match record {
      Some(Symbol::Reference {
        path: _,
        name,
        target_path,
        is_absolute,
        nameable,
      }) => {
        assert_eq!(name.as_ref().map(|n| n.as_str()), Some("other"));
        assert_eq!(target_path, "file|fn|other");
        assert!(!is_absolute);
        assert!(nameable);
      }
      _ => panic!("expected Reference"),
    }

    let idx_store =
      <TestStores as HasPartition<FileSymbols<DV, SI, TP>>>::store(stores);
    assert!(idx_store.index_get("file|ref|other").is_some());
  }

  #[test]
  fn write_keyword_round_trip() {
    let mut writer = make_writer();
    let mut cache = test_span_cache();
    let span = test_span(&mut cache, 2);

    let handle = {
      let mut ctx = PartitionWriteContextRef::new(&mut writer);
      ctx.write_symbol_keyword::<DV, SI, TP>(
        "file|kw|if".to_string(),
        span,
        SI::new("if"),
      )
    };

    let chunk = writer.build();
    let stores = chunk.storage();

    let sym_store =
      <TestStores as HasPartition<Symbols<DV, SI, TP>>>::store(stores);
    let record = sym_store
      .get_by_handle(&handle)
      .as_ref()
      .and_then(|r| r.record())
      .cloned();
    match record {
      Some(Symbol::Keyword { name }) => {
        assert_eq!(name.as_str(), "if");
      }
      _ => panic!("expected Keyword"),
    }
  }

  #[test]
  fn write_value_round_trip() {
    let mut writer = make_writer();
    let mut cache = test_span_cache();
    let span = test_span(&mut cache, 3);

    let handle = {
      let mut ctx = PartitionWriteContextRef::new(&mut writer);
      ctx.write_symbol_value::<DV, SI, TP>(
        "file|val|lit".to_string(),
        span,
        DV::Boolean(true),
      )
    };

    let chunk = writer.build();
    let stores = chunk.storage();

    let sym_store =
      <TestStores as HasPartition<Symbols<DV, SI, TP>>>::store(stores);
    let record = sym_store
      .get_by_handle(&handle)
      .as_ref()
      .and_then(|r| r.record())
      .cloned();
    match record {
      Some(Symbol::Value { value }) => {
        assert_eq!(value, DV::Boolean(true));
      }
      _ => panic!("expected Value"),
    }
  }

  #[test]
  fn write_scope_round_trip() {
    let mut writer = make_writer();
    let mut cache = test_span_cache();
    let span = test_span(&mut cache, 4);

    let handle = {
      let mut ctx = PartitionWriteContextRef::new(&mut writer);
      ctx.write_symbol_scope::<DV, SI, TP>(
        "file|scope|block".to_string(),
        span,
        Some(DV::String("module".to_string())),
      )
    };

    let chunk = writer.build();
    let stores = chunk.storage();

    let sym_store =
      <TestStores as HasPartition<Symbols<DV, SI, TP>>>::store(stores);
    let record = sym_store
      .get_by_handle(&handle)
      .as_ref()
      .and_then(|r| r.record())
      .cloned();
    match record {
      Some(Symbol::Scope { value }) => {
        assert_eq!(value, Some(DV::String("module".to_string())));
      }
      _ => panic!("expected Scope"),
    }
  }

  #[test]
  fn write_import_round_trip() {
    let mut writer = make_writer();
    let mut cache = test_span_cache();
    let span = test_span(&mut cache, 5);

    let handle = {
      let mut ctx = PartitionWriteContextRef::new(&mut writer);
      ctx.write_symbol_import::<DV, SI, TP>(
        "file|import|std".to_string(),
        span,
        "std::io".to_string(),
        Some(SI::new("io")),
        None,
      )
    };

    let chunk = writer.build();
    let stores = chunk.storage();

    let sym_store =
      <TestStores as HasPartition<Symbols<DV, SI, TP>>>::store(stores);
    let record = sym_store
      .get_by_handle(&handle)
      .as_ref()
      .and_then(|r| r.record())
      .cloned();
    match record {
      Some(Symbol::Import { path, alias, value }) => {
        assert_eq!(path, "std::io");
        assert_eq!(alias.as_ref().map(|a| a.as_str()), Some("io"));
        assert_eq!(value, None);
      }
      _ => panic!("expected Import"),
    }
  }

  #[test]
  fn write_multiple_symbols() {
    let mut writer = make_writer();
    let mut cache = test_span_cache();

    {
      let mut ctx = PartitionWriteContextRef::new(&mut writer);
      ctx.write_symbol_definition::<DV, SI, TP>(
        "file|fn|a".to_string(),
        test_span(&mut cache, 0),
        SI::new("a"),
        None,
        SymbolVisibility::Public,
      );
      ctx.write_symbol_definition::<DV, SI, TP>(
        "file|fn|b".to_string(),
        test_span(&mut cache, 1),
        SI::new("b"),
        None,
        SymbolVisibility::Private,
      );
      ctx.write_symbol_keyword::<DV, SI, TP>(
        "file|kw|let".to_string(),
        test_span(&mut cache, 2),
        SI::new("let"),
      );
    }

    let chunk = writer.build();
    let stores = chunk.storage();
    let idx_store =
      <TestStores as HasPartition<FileSymbols<DV, SI, TP>>>::store(stores);

    assert!(idx_store.index_get("file|fn|a").is_some());
    assert!(idx_store.index_get("file|fn|b").is_some());
    assert!(idx_store.index_get("file|kw|let").is_some());

    let all = idx_store.index_range("file|");
    assert_eq!(all.len(), 3);
  }

  #[test]
  fn clear_file_symbols_records_prefix() {
    let mut writer = make_writer();

    {
      let mut ctx = PartitionWriteContextRef::new(&mut writer);
      ctx.clear_file_symbols::<DV, SI, TP>(
        &"file|".to_string(),
      );
    }

    let prefixes = writer.clear_prefixes();
    assert_eq!(prefixes.len(), 1);
    assert_eq!(prefixes[0].1, "file|");
  }

  #[test]
  fn identical_shapes_deduplicated() {
    let mut writer = make_writer();
    let mut cache = test_span_cache();

    let (handle1, handle2) = {
      let mut ctx = PartitionWriteContextRef::new(&mut writer);
      let h1 = ctx.write_symbol_definition::<DV, SI, TP>(
        "file|fn|dup1".to_string(),
        test_span(&mut cache, 0),
        SI::new("same_name"),
        Some(DV::Integer(1)),
        SymbolVisibility::Public,
      );
      let h2 = ctx.write_symbol_definition::<DV, SI, TP>(
        "file|fn|dup2".to_string(),
        test_span(&mut cache, 1),
        SI::new("same_name"),
        Some(DV::Integer(1)),
        SymbolVisibility::Public,
      );
      (h1, h2)
    };

    assert_eq!(handle1.content_hash(), handle2.content_hash());
  }
}