2 unstable releases
0.2.0 | Sep 21, 2023 |
---|---|
0.0.0 | Sep 20, 2023 |
#32 in #parse-tree
124 downloads per month
Used in 2 crates
760KB
16K
SLoC
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:
- Parse Solidity source code with solang into the PT (Parse Tree) (not the same as Abstract Syntax Tree, see difference).
- 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:
- Implement
Formatter
callback functions for each PT node type. Every callback function should write formatted output for the current node and callVisitable::visit
function for child nodes delegating the output writing. - Implement
Visitable
trait and itsvisit
function for each PT node type. Everyvisit
function should call correspondingFormatter
'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:
- Preparse comments with config values
- Parse and compare the AST for source & expected files.
- The
AstEq
trait defines the comparison rules for the AST nodes
- The
- Format the source file and assert the equality of the output with the expected file.
- 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
- 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
- Bad Title Examples
- Fill in the issue template fields that include foundry version, platform & component info.
- Provide the code snippets showing the current & expected behaviors.
- If it's a feature request, specify why this feature is needed.
- Besides the default label (
T-Bug
for bugs orT-feature
for features), addC-forge
andCmd-forge-fmt
labels.
Fixing A Bug
- Specify an issue that is being addressed in the PR description.
- Add a note on the solution in the PR description.
- Make sure the PR includes the acceptance test(s).
Developing A Feature
- Specify an issue that is being addressed in the PR description.
- Add a note on the solution in the PR description.
- 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
- Adding malformatted & expected solidity code under
Dependencies
~26–44MB
~725K SLoC