Skip to content

Commit

Permalink
Clarinet format (#1609)
Browse files Browse the repository at this point in the history
* initial componet

* clarinet fmt boilerplate hooked up

* refactor functions a bit

* add basic formatter blocks

* fix build removing clarity-repl cargo flags

* remove dep on clarity-repl

* fix file path

* basic tests working

* add comment handling and some max line length logic

* settings flags for max line and indentation

* remove max line length check for comments

* switch to use PSE and refactor the matchings

* push settings into display_pse so we can attempt formatting Tuple

* fix map/tuple formatting

* add nested indentation

* fix format_map

* fix match and let formatting

* handle and/or

* golden test prettified

* special casing on comments and previous checking

* update match formatting

* cleanup spacing cruft

* fix boolean comments and a bit of pre/post newline logic

* add a couple golden files

* fix traits spacing

* comments golden and fix simple lists

* use manifest-path properly

* fix key value sugar

* use some peekable for inlining comments

* cleanup previous_expr unused code

* index-of case

* add metadata to golden test files

* simplify tuple handling and add if-tests

* add clarity bitcoin example contract

* module out the helpers and ignored source code

* remove unused previous_expr

* use the ignored and helper mods

* fix the indentation nesting and if statements

* cleanup inner_content functions

* cleanup and change assumptions temporarily

* fix list cons and boolean breaks with simplified line-break

* make in-place OR dry-run required

* fix up trailing parens for inner content

* --tabs flag

* update golden tests and fix match comments

* fix up assumptions in golden

* remove unfinished format-ignore handling

* remove unneeded allocations

* comma after kvs and dedent if body

* fix stack overflow for missing define functions

* pass source through for ignoring formatting pragma

* fix begin tests and use pretty_assertions

* add BND contract test

TODO: add the intended result when closing parens are handled better

* code review changes and special case define-ft

* remove wildcard matching from top level

* fix indentation and regenerate tests

* add warning to cli

---------

Co-authored-by: brady.ouren <[email protected]>
  • Loading branch information
tippenein and brady.ouren authored Feb 18, 2025
1 parent 4d6a2e8 commit 4ef87d4
Show file tree
Hide file tree
Showing 29 changed files with 8,383 additions and 3 deletions.
29 changes: 27 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"components/clarinet-cli",
"components/clarinet-deployments",
"components/clarinet-files",
"components/clarinet-format",
"components/clarinet-utils",
"components/clarinet-sdk-wasm",
"components/clarity-lsp",
Expand Down
1 change: 1 addition & 0 deletions components/clarinet-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ clarity-repl = { package = "clarity-repl", path = "../clarity-repl", features =
] }
clarinet-files = { path = "../clarinet-files", features = ["cli"] }
clarity-lsp = { path = "../clarity-lsp", features = ["cli"] }
clarinet-format = { path = "../clarinet-format" }
clarinet-deployments = { path = "../clarinet-deployments", features = ["cli"] }
hiro-system-kit = { path = "../hiro-system-kit" }
stacks-network = { path = "../stacks-network" }
Expand Down
100 changes: 99 additions & 1 deletion components/clarinet-cli/src/frontend/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use clarinet_files::{
get_manifest_location, FileLocation, NetworkManifest, ProjectManifest, ProjectManifestFile,
RequirementConfig,
};
use clarinet_format::formatter::{self, ClarityFormatter};
use clarity_repl::analysis::call_checker::ContractAnalysis;
use clarity_repl::clarity::vm::analysis::AnalysisDatabase;
use clarity_repl::clarity::vm::costs::LimitedCostTracker;
Expand All @@ -41,7 +42,7 @@ use clarity_repl::{analysis, repl, Terminal};
use stacks_network::{self, DevnetOrchestrator};
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::prelude::*;
use std::io::{self, prelude::*};
use std::{env, process};
use toml;

Expand Down Expand Up @@ -94,11 +95,35 @@ enum Command {
/// Get Clarity autocompletion and inline errors from your code editor (VSCode, vim, emacs, etc)
#[clap(name = "lsp", bin_name = "lsp")]
LSP,
/// Format clarity code files
#[clap(name = "format", aliases = &["fmt"], bin_name = "format")]
Formatter(Formatter),
/// Step by step debugging and breakpoints from your code editor (VSCode, vim, emacs, etc)
#[clap(name = "dap", bin_name = "dap")]
DAP,
}

#[derive(Parser, PartialEq, Clone, Debug)]
struct Formatter {
#[clap(long = "manifest-path", short = 'm')]
pub manifest_path: Option<String>,
/// If specified, format only this file
#[clap(long = "file", short = 'f')]
pub file: Option<String>,
#[clap(long = "max-line-length", short = 'l')]
pub max_line_length: Option<usize>,
#[clap(long = "indent", short = 'i', conflicts_with = "use_tabs")]
/// indentation size, e.g. 2
pub indentation: Option<usize>,
#[clap(long = "tabs", short = 't', conflicts_with = "indentation", action = clap::ArgAction::SetTrue)]
/// use tabs instead of spaces
pub use_tabs: bool,
#[clap(long = "dry-run", conflicts_with = "in_place")]
pub dry_run: bool,
#[clap(long = "in-place", conflicts_with = "dry_run")]
pub in_place: bool,
}

#[derive(Subcommand, PartialEq, Clone, Debug)]
enum Devnet {
/// Generate package of all required devnet artifacts
Expand Down Expand Up @@ -1199,6 +1224,37 @@ pub fn main() {
process::exit(1);
}
},
Command::Formatter(cmd) => {
eprintln!(
"{}",
format_warn!("This command is in beta. Feedback is welcome!"),
);
let sources = get_sources_to_format(cmd.manifest_path, cmd.file);
let mut settings = formatter::Settings::default();

if let Some(max_line_length) = cmd.max_line_length {
settings.max_line_length = max_line_length;
}

if let Some(indentation) = cmd.indentation {
settings.indentation = formatter::Indentation::Space(indentation);
}
if cmd.use_tabs {
settings.indentation = formatter::Indentation::Tab;
}
let formatter = ClarityFormatter::new(settings);

for (file_path, source) in &sources {
let output = formatter.format(source);
if cmd.in_place {
let _ = overwrite_formatted(file_path, output);
} else if cmd.dry_run {
println!("{}", output);
} else {
eprintln!("required flags: in-place or dry-run");
}
}
}
Command::Devnet(subcommand) => match subcommand {
Devnet::Package(cmd) => {
let manifest = load_manifest_or_exit(cmd.manifest_path, false);
Expand All @@ -1212,6 +1268,48 @@ pub fn main() {
};
}

fn overwrite_formatted(file_path: &String, output: String) -> io::Result<()> {
let mut file = fs::File::create(file_path)?;

file.write_all(output.as_bytes())?;
Ok(())
}

fn from_code_source(src: ClarityCodeSource) -> String {
match src {
ClarityCodeSource::ContractOnDisk(path_buf) => {
path_buf.as_path().to_str().unwrap().to_owned()
}
_ => panic!("invalid code source"), // TODO
}
}
// look for files at the default code path (./contracts/) if
// cmd.manifest_path is not specified OR if cmd.file is not specified
fn get_sources_from_manifest(manifest_path: Option<String>) -> Vec<String> {
let manifest = load_manifest_or_exit(manifest_path, true);
let contracts = manifest.contracts.values().cloned();
contracts.map(|c| from_code_source(c.code_source)).collect()
}

fn get_sources_to_format(
manifest_path: Option<String>,
file: Option<String>,
) -> Vec<(String, String)> {
let files: Vec<String> = match file {
Some(file_name) => vec![format!("{}", file_name)],
None => get_sources_from_manifest(manifest_path),
};
// Map each file to its source code
files
.into_iter()
.map(|file_path| {
let source = fs::read_to_string(&file_path)
.unwrap_or_else(|_| "Failed to read file".to_string());
(file_path, source)
})
.collect()
}

fn get_manifest_location_or_exit(path: Option<String>) -> FileLocation {
match get_manifest_location(path) {
Some(manifest_location) => manifest_location,
Expand Down
30 changes: 30 additions & 0 deletions components/clarinet-format/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
name = "clarinet-format"
version = "0.1.0"
edition = "2021"

[dependencies]
clarity = { workspace = true}

[dev-dependencies]
pretty_assertions = "1.3"

[features]
default = ["cli"]
cli = [
"clarity/canonical",
"clarity/developer-mode",
"clarity/devtools",
"clarity/log",
]
wasm = [
"clarity/wasm",
"clarity/developer-mode",
"clarity/devtools",
]


[lib]
name = "clarinet_format"
path = "src/lib.rs"
crate-type = ["lib"]
25 changes: 25 additions & 0 deletions components/clarinet-format/src/formatter/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use clarity::vm::representations::PreSymbolicExpression;

/// trim but leaves newlines preserved
pub fn t(input: &str) -> &str {
let start = input
.find(|c: char| !c.is_whitespace() || c == '\n')
.unwrap_or(0);

let end = input
.rfind(|c: char| !c.is_whitespace() || c == '\n')
.map(|pos| pos + 1)
.unwrap_or(0);

&input[start..end]
}
/// REMOVE: just grabs the 1st and rest from a PSE
pub fn name_and_args(
exprs: &[PreSymbolicExpression],
) -> Option<(&PreSymbolicExpression, &[PreSymbolicExpression])> {
if exprs.len() >= 2 {
Some((&exprs[1], &exprs[2..]))
} else {
None // Return None if there aren't enough items
}
}
46 changes: 46 additions & 0 deletions components/clarinet-format/src/formatter/ignored.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use clarity::vm::representations::PreSymbolicExpression;

pub fn ignored_exprs(exprs: &[PreSymbolicExpression], source: &str) -> String {
let start = exprs.first().unwrap().span();
let end = exprs.last().unwrap().span();

let start_line = start.start_line as usize;
let end_line = end.end_line as usize;

let mut result = String::new();
let mut is_first = true;

// Look at lines including one before our span starts
for (idx, line) in source
.lines()
.skip(start_line - 1) // Start one line earlier
.take(end_line - (start_line - 1) + 1)
.enumerate()
{
if !is_first {
result.push('\n');
}

if idx == 0 {
// First line (the one with the opening parenthesis)
if let Some(paren_pos) = line.find('(') {
result.push_str(&line[paren_pos..]);
}
} else if idx == end_line - (start_line - 1) {
// Last line - up to end column
let end_column = end.end_column as usize;
if end_column <= line.len() {
result.push_str(&line[..end_column]);
} else {
result.push_str(line);
}
} else {
// Middle lines - complete line
result.push_str(line);
}

is_first = false;
}

result
}
Loading

0 comments on commit 4ef87d4

Please sign in to comment.