2 unstable releases

0.2.0 Sep 21, 2023
0.0.0 Sep 20, 2023

#32 in #parse-tree

Download history 6/week @ 2024-06-25 53/week @ 2024-07-02 25/week @ 2024-07-09 49/week @ 2024-07-16 81/week @ 2024-07-23 14/week @ 2024-07-30 24/week @ 2024-08-06 9/week @ 2024-08-13 17/week @ 2024-08-20 58/week @ 2024-08-27 10/week @ 2024-09-03 7/week @ 2024-09-10 53/week @ 2024-09-17 53/week @ 2024-09-24 8/week @ 2024-10-01 7/week @ 2024-10-08

124 downloads per month
Used in 2 crates

MIT/Apache

760KB
16K SLoC

Rust 12K SLoC // 0.0% comments Solidity 4K SLoC // 0.5% comments

Formatter (fmt)

Solidity formatter that respects (some parts of) the Style Guide and is tested on the Prettier Solidity Plugin cases.

Architecture

The formatter works in two steps:

  1. Parse Solidity source code with solang into the PT (Parse Tree) (not the same as Abstract Syntax Tree, see difference).
  2. Walk the PT and output new source code that's compliant with provided config and rule set.

The technique for walking the tree is based on Visitor Pattern and works as following:

  1. Implement Formatter callback functions for each PT node type. Every callback function should write formatted output for the current node and call Visitable::visit function for child nodes delegating the output writing.
  2. Implement Visitable trait and its visit function for each PT node type. Every visit function should call corresponding Formatter's callback function.

Output

The formatted output is written into the output buffer in chunks. The Chunk struct holds the content to be written & metadata for it. This includes the comments surrounding the content as well as the needs_space flag specifying whether this chunk needs a space. The flag overrides the default behavior of Formatter::next_char_needs_space method.

The content gets written into the FormatBuffer which contains the information about the current indentation level, indentation length, current state as well as the other data determining the rules for writing the content. FormatBuffer implements the std::fmt::Write trait where it evaluates the current information and decides how the content should be written to the destination.

Comments

The solang parser does not output comments as a type of parse tree node, but rather in a list alongside the parse tree with location information. It is therefore necessary to infer where to insert the comments and how to format them while traversing the parse tree.

To handle this, the formatter pre-parses the comments and puts them into two categories: Prefix and Postfix comments. Prefix comments refer to the node directly after them, and postfix comments refer to the node before them. As an illustration:

// This is a prefix comment
/* This is also a prefix comment */
uint variable = 1 + 2; /* this is postfix */ // this is postfix too
    // and this is a postfix comment on the next line

To insert the comments into the appropriate areas, strings get converted to chunks before being written to the buffer. A chunk is any string that cannot be split by whitespace. A chunk also carries with it the surrounding comment information. Thereby when writing the chunk the comments can be added before and after the chunk as well as any any whitespace surrounding.

To construct a chunk, the string and the location of the string is given to the Formatter and the pre-parsed comments before the start and end of the string are associated with that string. The source code can then further be chunked before the chunks are written to the buffer.

To write the chunk, first the comments associated with the start of the chunk get written to the buffer. Then the Formatter checks if any whitespace is needed between what's been written to the buffer and what's in the chunk and inserts it where appropriate. If the chunk content fits on the same line, it will be written directly to the buffer, otherwise it will be written on the next line. Finally, any associated postfix comments also get written.

Example

Source code

pragma   solidity ^0.8.10 ;
contract  HelloWorld {
    string   public message;
    constructor(  string memory initMessage) { message = initMessage;}
}


event    Greet( string  indexed  name) ;

Parse Tree (simplified)

SourceUnit
 | PragmaDirective("solidity", "^0.8.10")
 | ContractDefinition("HelloWorld")
    | VariableDefinition("string", "message", null, ["public"])
    | FunctionDefinition("constructor")
       | Parameter("string", "initMessage", ["memory"])
 | EventDefinition("string", "Greet", ["indexed"], ["name"])

Formatted source code that was reconstructed from the Parse Tree

pragma solidity ^0.8.10;

contract HelloWorld {
    string public message;

    constructor(string memory initMessage) {
        message = initMessage;
    }
}

event Greet(string indexed name);

Configuration

The formatter supports multiple configuration options defined in FormatterConfig.

Option Default Description
line_length 120 Maximum line length where formatter will try to wrap the line
tab_width 4 Number of spaces per indentation level
bracket_spacing false Print spaces between brackets
int_types long Style of uint/int256 types. Available options: long, short, preserve
func_attrs_with_params_multiline true If function parameters are multiline then always put the function attributes on separate lines
quote_style double Style of quotation marks. Available options: double, single, preserve
number_underscore preserve Style of underscores in number literals. Available options: remove, thousands, preserve

TODO: update ^

Disable Line

The formatter can be disabled on specific lines by adding a comment // forgefmt: disable-next-line, like this:

// forgefmt: disable-next-line
uint x = 100;

Alternatively, the comment can also be placed at the end of the line. In this case, you'd have to use disable-line instead:

uint x = 100; // forgefmt: disable-line

Testing

Tests reside under the fmt/testdata folder and specify the malformatted & expected Solidity code. The source code file is named original.sol and expected file(s) are named in a format ({prefix}.)?fmt.sol. Multiple expected files are needed for tests covering available configuration options.

The default configuration values can be overridden from within the expected file by adding a comment in the format // config: {config_entry} = {config_value}. For example:

// config: line_length = 160

The test_directory macro is used to specify a new folder with source files for the test suite. Each test suite has the following process:

  1. Preparse comments with config values
  2. Parse and compare the AST for source & expected files.
    • The AstEq trait defines the comparison rules for the AST nodes
  3. Format the source file and assert the equality of the output with the expected file.
  4. Format the expected files and assert the idempotency of the formatting operation.

Contributing

Check out the foundry contribution guide.

Guidelines for contributing to forge fmt:

Opening an issue

  1. Create a short concise title describing an issue.
    • Bad Title Examples
      Forge fmt does not work
      Forge fmt breaks
      Forge fmt unexpected behavior
      
    • Good Title Examples
      Forge fmt postfix comment misplaced
      Forge fmt does not inline short yul blocks
      
  2. Fill in the issue template fields that include foundry version, platform & component info.
  3. Provide the code snippets showing the current & expected behaviors.
  4. If it's a feature request, specify why this feature is needed.
  5. Besides the default label (T-Bug for bugs or T-feature for features), add C-forge and Cmd-forge-fmt labels.

Fixing A Bug

  1. Specify an issue that is being addressed in the PR description.
  2. Add a note on the solution in the PR description.
  3. Make sure the PR includes the acceptance test(s).

Developing A Feature

  1. Specify an issue that is being addressed in the PR description.
  2. Add a note on the solution in the PR description.
  3. Provide the test coverage for the new feature. These should include:
    • Adding malformatted & expected solidity code under fmt/testdata/$dir/
    • Testing the behavior of pre and postfix comments
    • If it's a new config value, tests covering all available options

Dependencies

~26–44MB
~725K SLoC