Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP - Clarinet format #1609

Draft
wants to merge 39 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
1578266
initial componet
Nov 4, 2024
c114c34
clarinet fmt boilerplate hooked up
Nov 5, 2024
02a7db6
refactor functions a bit
Nov 11, 2024
0cef41e
add basic formatter blocks
Nov 12, 2024
07bd0ec
fix build removing clarity-repl cargo flags
Nov 14, 2024
6d33c36
remove dep on clarity-repl
Nov 15, 2024
cb252fe
fix file path
Nov 18, 2024
29c4618
basic tests working
Nov 20, 2024
ec6d9e7
add comment handling and some max line length logic
Nov 21, 2024
a7d0adf
settings flags for max line and indentation
Nov 21, 2024
ffc88c9
remove max line length check for comments
Nov 21, 2024
b863acf
switch to use PSE and refactor the matchings
Nov 26, 2024
3c3dcbd
push settings into display_pse so we can attempt formatting Tuple
Dec 2, 2024
af8c35b
fix map/tuple formatting
Dec 2, 2024
b8e4451
add nested indentation
Dec 2, 2024
9017ce5
fix format_map
Dec 2, 2024
86016fd
fix match and let formatting
Dec 3, 2024
c6254c1
handle and/or
Dec 3, 2024
25005a2
golden test prettified
Dec 3, 2024
e2bb030
special casing on comments and previous checking
Dec 16, 2024
8bc8c15
update match formatting
Dec 17, 2024
057af87
cleanup spacing cruft
Dec 17, 2024
8b3ff87
fix boolean comments and a bit of pre/post newline logic
Dec 18, 2024
5736d15
add a couple golden files
Dec 18, 2024
dcc2f2c
fix traits spacing
Dec 18, 2024
feafe83
comments golden and fix simple lists
Dec 18, 2024
4d6d022
use manifest-path properly
Dec 19, 2024
972c49c
fix key value sugar
Dec 19, 2024
93b64a8
Merge branch 'main' into clarinet-format
Dec 19, 2024
3850034
use some peekable for inlining comments
Dec 20, 2024
76cf5e7
cleanup previous_expr unused code
Dec 20, 2024
83afc00
index-of case
Dec 20, 2024
22a8337
add metadata to golden test files
Dec 20, 2024
3596282
simplify tuple handling and add if-tests
Dec 23, 2024
ae2de4d
add clarity bitcoin example contract
Dec 29, 2024
fe4b18d
module out the helpers and ignored source code
tippenein Jan 1, 2025
3583f58
remove unused previous_expr
tippenein Jan 1, 2025
6ccbe92
use the ignored and helper mods
tippenein Jan 1, 2025
0f94882
fix the indentation nesting and if statements
Jan 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 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 @@ -40,6 +40,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
79 changes: 78 additions & 1 deletion components/clarinet-cli/src/frontend/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use clarinet_files::{
get_manifest_location, FileLocation, NetworkManifest, ProjectManifest, ProjectManifestFile,
RequirementConfig,
};
use clarinet_format::formatter::{ClarityFormatter, Settings};
use clarity_repl::analysis::call_checker::ContractAnalysis;
use clarity_repl::clarity::vm::analysis::AnalysisDatabase;
use clarity_repl::clarity::vm::costs::LimitedCostTracker;
Expand All @@ -39,7 +40,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,26 @@ 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")]
Format(Format),
/// 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 Format {
/// Path to clarity files
#[clap(long = "path", short = 'p')]
pub code_path: Option<String>,
/// If specified, format only this file
#[clap(long = "file", short = 'f')]
pub file: Option<String>,
#[clap(long = "dry-run")]
pub dry_run: bool,
}

#[derive(Subcommand, PartialEq, Clone, Debug)]
enum Devnet {
/// Generate package of all required devnet artifacts
Expand Down Expand Up @@ -1180,6 +1196,18 @@ pub fn main() {
process::exit(1);
}
},
Command::Format(cmd) => {
let sources = get_source_with_path(cmd.code_path, cmd.file);
tippenein marked this conversation as resolved.
Show resolved Hide resolved
let settings = Settings::default();
let mut formatter = ClarityFormatter::new(settings);

for (file_path, source) in &sources {
let output = formatter.format(source);
if !cmd.dry_run {
let _ = overwrite_formatted(file_path, output);
}
}
}
Command::Devnet(subcommand) => match subcommand {
Devnet::Package(cmd) => {
let manifest = load_manifest_or_exit(cmd.manifest_path);
Expand All @@ -1193,6 +1221,47 @@ pub fn main() {
};
}

fn overwrite_formatted(file_path: &String, output: String) -> io::Result<()> {
// Open the file in write mode, overwriting existing content
let mut file = fs::File::create(file_path)?;

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

// flush the contents to ensure it's written immediately
file.flush()
}

fn get_source_with_path(code_path: Option<String>, file: Option<String>) -> Vec<(String, String)> {
// look for files at the default code path (./contracts/) if
// cmd.code_path is not specified OR if cmd.file is not specified
let path = code_path.unwrap_or_else(|| "./contracts/".to_string());

// Collect file paths and load source code
let files: Vec<String> = match file {
Some(file_name) => vec![format!("{}/{}", path, file_name)],
None => match fs::read_dir(&path) {
Ok(entries) => entries
.filter_map(Result::ok)
.filter(|entry| entry.path().is_file())
.map(|entry| entry.path().to_string_lossy().into_owned())
.collect(),
Err(message) => {
eprintln!("{}", format_err!(message));
std::process::exit(1)
}
},
};
// 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 Expand Up @@ -1250,6 +1319,14 @@ fn load_manifest_or_warn(path: Option<String>) -> Option<ProjectManifest> {
}
}

fn load_clarity_code(
code_path: &Option<String>,
file: &Option<String>,
dry_run: bool,
) -> (Option<String>) {
Some("".to_string())
}

fn load_deployment_and_artifacts_or_exit(
manifest: &ProjectManifest,
deployment_plan_path: &Option<String>,
Expand Down
13 changes: 13 additions & 0 deletions components/clarinet-format/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "clarinet-format"
version = "0.1.0"
edition = "2021"

[dependencies]
clarinet-files = { path = "../clarinet-files", default-features = false, optional = true }
clarity-repl = { path = "../clarity-repl" }
tippenein marked this conversation as resolved.
Show resolved Hide resolved

[lib]
name = "clarinet_format"
path = "src/lib.rs"
crate-type = ["lib"]
220 changes: 220 additions & 0 deletions components/clarinet-format/src/formatter/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
use clarity_repl::clarity::ast::build_ast_with_rules;
use clarity_repl::clarity::vm::functions::{define::DefineFunctions, NativeFunctions};
use clarity_repl::clarity::vm::types::QualifiedContractIdentifier;
use clarity_repl::clarity::ClarityVersion;
use clarity_repl::clarity::StacksEpochId;
use clarity_repl::clarity::SymbolicExpression;

pub enum Indentation {
Space(u8),
Tab,
}

pub struct Settings {
pub indentation: Indentation,
pub max_line_length: u8,
}
impl Settings {
pub fn default() -> Settings {
Settings {
indentation: Indentation::Space(2),
max_line_length: 80,
}
}
}
//
pub struct ClarityFormatter {
settings: Settings,
}
impl ClarityFormatter {
pub fn new(settings: Settings) -> Self {
Self { settings }
}
pub fn format(&mut self, source: &str) -> String {
let ast = build_ast_with_rules(
&QualifiedContractIdentifier::transient(),
source,
&mut (),
ClarityVersion::Clarity3,
StacksEpochId::Epoch30,
clarity_repl::clarity::ast::ASTRules::Typical,
)
.unwrap();
let output = format_source_exprs(&self.settings, &ast.expressions, "");
println!("output: {}", output);
output
}
}

// * functions

// Top level define-<function> should have a line break above and after (except on first line)
// options always on new lines
// Functions Always on multiple lines, even if short
// *begin* never on one line
// *let* never on one line

// * match *
// One line if less than max length (unless the original source has line breaks?)
// Multiple lines if more than max length (should the first arg be on the first line if it fits?)
pub fn format_source_exprs(
settings: &Settings,
expressions: &[SymbolicExpression],
acc: &str,
) -> String {
if let Some((expr, remaining)) = expressions.split_first() {
if let Some(list) = expr.match_list() {
let atom = list.split_first().and_then(|(f, _)| f.match_atom());
use NativeFunctions::*;
let formatted = if let Some(
DefineFunctions::PublicFunction
| DefineFunctions::ReadOnlyFunction
| DefineFunctions::PrivateFunction,
) = atom.and_then(|a| DefineFunctions::lookup_by_name(a))
{
format_function(settings, list)
} else if let Some(Begin) = atom.and_then(|a| NativeFunctions::lookup_by_name(a)) {
format_begin(settings, list)
} else if let Some(Let) = atom.and_then(|a| NativeFunctions::lookup_by_name(a)) {
format_let(settings, list)
} else if let Some(TupleCons) = atom.and_then(|a| NativeFunctions::lookup_by_name(a)) {
format_tuple(settings, list)
} else {
format!("({})", format_source_exprs(settings, list, acc))
};
return format!(
"{formatted} {}",
format_source_exprs(settings, remaining, acc)
)
.trim()
.to_owned();
}
return format!("{} {}", expr, format_source_exprs(settings, remaining, acc))
.trim()
.to_owned();
};
acc.to_owned()
}

fn format_begin(settings: &Settings, exprs: &[SymbolicExpression]) -> String {
let mut begin_acc = "(begin\n".to_string();
for arg in exprs.get(1..).unwrap_or_default() {
if let Some(list) = arg.match_list() {
begin_acc.push_str(&format!(
"\n ({})",
format_source_exprs(settings, list, "")
))
}
}
begin_acc.push_str("\n)\n");
begin_acc.to_owned()
}

fn format_let(settings: &Settings, exprs: &[SymbolicExpression]) -> String {
let mut begin_acc = "(let (\n".to_string();
for arg in exprs.get(1..).unwrap_or_default() {
if let Some(list) = arg.match_list() {
begin_acc.push_str(&format!(
"\n ({})",
format_source_exprs(settings, list, "")
))
}
}
begin_acc.push_str("\n) \n");
begin_acc.to_owned()
}

fn format_tuple(settings: &Settings, exprs: &[SymbolicExpression]) -> String {
let mut tuple_acc = "{ ".to_string();
for (i, expr) in exprs[1..].iter().enumerate() {
let (key, value) = expr
.match_list()
.and_then(|list| list.split_first())
.unwrap();
if i < exprs.len() - 2 {
tuple_acc.push_str(&format!(
"{key}: {}, ",
format_source_exprs(settings, value, "")
));
} else {
tuple_acc.push_str(&format!(
"{key}: {}",
format_source_exprs(settings, value, "")
));
}
}
tuple_acc.push_str(" }");
tuple_acc.to_string()
}

fn format_function(settings: &Settings, exprs: &[SymbolicExpression]) -> String {
let func_type = exprs.first().unwrap();
let name_and_args = exprs.get(1).and_then(|f| f.match_list()).unwrap();
let mut func_acc = format!(
"({func_type} ({})",
format_source_exprs(settings, name_and_args, "")
);
for arg in exprs.get(2..).unwrap_or_default() {
if let Some(list) = arg.match_list() {
func_acc.push_str(&format!(
"\n ({})",
format_source_exprs(settings, list, "")
))
}
}
func_acc.push_str("\n)");
func_acc.to_owned()
}
#[cfg(test)]
mod tests_formatter {
use super::{ClarityFormatter, Settings};
fn format_with_default(source: &str) -> String {
let mut formatter = ClarityFormatter::new(Settings::default());
formatter.format(source)
}
#[test]
fn test_simplest_formatter() {
let result = format_with_default(&String::from("( ok true )"));
assert_eq!(result, "(ok true)");
}
#[test]
fn test_two_expr_formatter() {
let result = format_with_default(&String::from("(ok true)(ok true)"));
assert_eq!(result, "(ok true)\n(ok true)");
}
#[test]
fn test_function_formatter() {
let result = format_with_default(&String::from("(define-private (my-func) (ok true))"));
assert_eq!(result, "(define-private (my-func)\n (ok true)\n)");
}
#[test]
fn test_tuple_formatter() {
let result = format_with_default(&String::from("{n1:1,n2:2,n3:3}"));
assert_eq!(result, "{ n1: 1, n2: 2, n3: 3 }");
}
#[test]
fn test_function_and_tuple_formatter() {
let src = "(define-private (my-func) (ok { n1: 1, n2: 2, n3: 3 }))";
let result = format_with_default(&String::from(src));
assert_eq!(
result,
"(define-private (my-func)\n (ok { n1: 1, n2: 2, n3: 3 })\n)"
);
}

#[test]
fn test_function_args_multiline() {
let src = "(define-public (my-func (amount uint) (sender principal)) (ok true))";
let result = format_with_default(&String::from(src));
assert_eq!(
result,
"(define-public (my-func\n (amount uint)\n (sender principal)\n )\n (ok true)\n)"
);
}
#[test]
fn test_begin_never_one_line() {
let src = "(begin (ok true))";
let result = format_with_default(&String::from(src));
assert_eq!(result, "(begin\n (ok true)\n)");
}
}
Loading
Loading