Proposed JavaScript Coding Standards Revisions for Prettier Compatibility

(The following standards revision proposal was drafted by @jsnajdr. Jarda maintains the wp-prettier Prettier fork and has been working to introduce it to the Gutenberg repository. You’re invited to provide feedback to this proposal in the comments below, or to join an upcoming JavaScript meeting in #core-js, where this will be discussed)


Across the JavaScriptJavaScript JavaScript or JS is an object-oriented computer programming language commonly used to create interactive effects within web browsers. WordPress makes extensive use of JS for a better user experience. While PHP is executed on the server, JS executes within a user’s browser. https://www.javascript.com/. community, the Prettier code formatter has become immensely popular over that last three years since it was originally released. It automatically performs high-quality formatting of your JavaScript code: when you press Save, your code is instantly formatted. This removes a distraction for contributors who write or review code, and allows them to focus on the more valuable aspects of their work without having to discuss the JavaScript WordPress Coding Standards so often. That’s why we’d like to adopt it in the WordPress JavaScript code bases, too.

The official Prettier formatter is very opinionated and has very few options. The reasons are both technical and cultural. The complexity of the formatting algorithm would explode exponentially with too many options and their combinations, and a part of the project vision is to establish an unified JavaScript formatting standard.

The WordPress formatting standard has one major incompatibility with the Prettier convention: it requires spaces inside all kinds of parens — (){}[], inside template strings, JSX attributes and expressions, everywhere:

function Label( { text, icon } ) {
	if ( ! [ 'left', 'right' ].includes( icon ) ) {
		return null;
	}

	return (
		<label className={ `icon-${ icon }` }>
			{ text }
		</label>
	);
}

To teach this convention to Prettier, we had to create a fork that adds an extra option and modified the paren-formatting code, and publish it on NPM under the name wp-prettier (@wordpress/prettier would arguably be even better name!). At this moment, we’ve been using that fork for 2.5 years in the Calypso and Jetpack projects, have maintained and updated it over countless upstream releases, and are confident that we can recommend it to anyone who wants to format their JavaScript code the WordPress way.

In a Gutenberg pull request, we are proposing adopting the WordPress Prettier tool in the project.

After adding support for the WordPress style paren spacing, there remain several very minor cases where Prettier formats JavaScript code slightly differently from what the current WordPress JavaScript Coding Standards recommend. They don’t diverge from the essence and spirit of the WordPress coding standards. Further patching of Prettier would be, on our opinion, not worth the coding and maintenance effort. And in some cases is outright impossible, because the recommendation asks for human judgment that an algorithm cannot implement.

In this post, we propose several small changes of the coding standards that align them fully with the Prettier convention.

Formatting ternaries and binary ops

The standard says that when breaking a long statement, the “operator” should be at the end of line, and doesn’t distinguish between binary and ternary operators. But Prettier does. When breaking a binary operator, it indeed puts the operator at the end of line:

const result =
	partOne +
	PartTwo;

But the parts of a ternary operator are put on the start of the new line (after the indentation):

const result = isRtl
	? 'right'
	: 'left';

Also, the standard recommends that long “lines should be broken into logical groups if it improves readability”. That doesn’t happen with Prettier — it wraps the lines if and only if the line would be longer than maximum line length otherwise. We propose to remove that ambiguous and subjective formulation from the standard.

Wrapping chained n-ary operators

Another difference related to binary operators and the Multi-Line Statements section is that Prettier puts each operand on separate line, even the left side of an assignment. It doesn’t do the “fluid text wrap” style like this:

const result = a + b +
	c + d;

but this:

const result =
	a +
	b +
	c +
	d;

To address all these differences, we propose to reformulate the Multi-Line Statements section of the standards document as follows (the last paragraph about conditionals is unchanged):

Before:

When a statement is too long to fit on one line, line breaks must occur after an operator.

// Bad
var html = '<p>The sum of ' + a + ' and ' + b + ' plus ' + c
	+ ' is ' + ( a + b + c ) + '</p>';
 
// Good
var html = '<p>The sum of ' + a + ' and ' + b + ' plus ' + c +
	' is ' + ( a + b + c ) + '</p>';

Lines should be broken into logical groups if it improves readability, such as splitting each expression of a ternary operator onto its own line, even if both will fit on a single line.

// Acceptable
var baz = ( true === conditionalStatement() ) ? 'thing 1' : 'thing 2';
 
// Better
var baz = firstCondition( foo ) && secondCondition( bar ) ?
	qux( foo, bar ) :
	foo;

When a conditional is too long to fit on one line, each operand of a logical operator in the boolean expression must appear on its own line, indented one extra level from the opening and closing parentheses.

if (
	firstCondition() &&
	secondCondition() &&
	thirdCondition()
) {
	doStuff();
}

After:

When a statement with operators is too long to fit on one line and is broken into multiple lines, line breaks must occur after a binary operator. Each operand with the operator that follows it must be on a separate line.

// Bad
const html = '<p>The sum of ' + a + ' and ' + b + " plus " + c
	+ ' is ' + ( a + b + c ) + '</p>';
 
// Good
const html =
	'<p>The sum of ' +
	a +
	' and ' +
	b +
	' plus ' +
	c +
	' is ' +
	( a + b + c ) +
	'</p>';

On the other hand, when a long statement with a ternary operator is broken into multiple lines, the parts of the ternary operator should be after the line break:

// Bad
const baz = true === conditionalStatement() ?
	'thing 1' : 
	'thing 2';

// Good
const baz =
	true === conditionalStatement()
		? 'thing 1'
		: 'thing 2';

When a conditional is too long to fit on one line, each operand of a logical operator in the boolean expression must appear on its own line, indented one extra level from the opening and closing parentheses.

if (
	firstCondition() &&
	secondCondition() &&
	thirdCondition()
) {
	doStuff();
}

Chained method calls and context

The standard recommends that when one method in a chain “changes the context”, it should add an extra indentation:

elements
	.addClass( 'foo' )
	.children()
		.html( 'hello' )
	.end()
	.appendTo( 'body' );

But Prettier doesn’t know what the context is — it’s an ambiguous concept even for a human. So it indents everything equally:

elements
	.addClass( 'foo' )
	.children()
	.html( 'hello' )
	.end()
	.appendTo( 'body' );

It also adds one extra touch: if the initial identifier is just one or two characters long, it will keep the first method call on the same line:

el.addClass( 'foo' )
	.children()
	.html( 'hello' )
	.end()
	.appendTo( 'body' );

To address these differences, we propose to reformulate the Chained Method Calls section of the standards document as follows. The new formulation removes the requirements that can’t be reasonably implemented, focuses on the main points, i.e., breaking into multiple lines and consistent indentation, and modernizes the jQuery-based example to show more contemporary, functional JavaScript code.

Before:

When a chain of method calls is too long to fit on one line, there must be one call per line, with the first call on a separate line from the object the methods are called on. If the method changes the context, an extra level of indentation must be used.

elements
	.addClass( 'foo' )
	.children()
		.html( 'hello' )
	.end()
	.appendTo( 'body' );

After:

When a chain of method calls is too long to fit on one line, it must be broken into multiple lines where each line contains one call from the chain. All lines after the first must be indented by one tab.

findFocusable( context )
	.filter( isTabbableIndex )
	.map( mapElementToObjectTabbable )
	.sort( compareObjectTabbables )
	.map( mapObjectTabbableToElement );

#javascript, #standards