Skip to content

Style Guide

Brandon Ingli edited this page Apr 21, 2022 · 19 revisions

0 Preface

0.1 Code Formatter

We use prettier for formatting code. Our settings are present in package.json: the defaults. Please also format your code using this tool before submitting a PR. Note that some of our files may not be formatted with prettier currently, but will be formatted when they’re next edited.

To install in VS Code, open the Command Pallette (CTRL + P on Windows/Linux, SHIFT + CMD + P on Mac), then enter

ext install esbenp.prettier-vscode

The workspace settings present in our repository will automatically set prettier as the code formatter and enable formatting on save.

1 Formatting

1.1 Braces

Braces are required for all control structures (i.e. if, else, for, do, while, as well as any others), unless the body contains only a single statement. The first statement of a non-empty block must begin on its own line.

Disallowed:

if (someVeryLongCondition()) { doSomething();
  doSomethingElse();
}

for (let i = 0; i < foo.length; i++) bar(foo[i]);

Exception: A simple if statement that can fit entirely on a single line with no wrapping may be kept on a single line with no braces when it improves readability. However, it is still encouraged to include braces for maintainability.

if (shortCondition()) foo();

1.2 Nonempty blocks: K&R style

Braces follow the Kernighan and Ritchie style ("Egyptian brackets") for nonempty blocks and block-like

  • No line break before the opening brace.
  • Line break after the opening brace.
  • Line break before the closing brace.
  • Line break after the closing brace if that brace terminates a statement or the body of a function or class statement, or a class method.

Specifically, there is no line break after the brace if it is followed by else, catch, while, or a comma, semicolon, or right-parenthesis.

Example:

class InnerClass {
  constructor() {}

  /** @param {number} foo */
  method(foo) {
    if (condition(foo)) {
      try {
        // Note: this might fail.
        something();
      } catch (err) {
        recover();
      }
    }
  }
}

1.3 Empty blocks: may be concise

An empty block or block-like construct may be closed immediately after it is opened, with no characters, space, or line break in between (i.e. {}), unless it is a part of a multi-block statement (one that directly contains multiple blocks: if/else or try/catch/finally).

Example:

const doNothing = () => {};

Disallowed:

if (condition) {
  // …
} else if (otherCondition) {} else {
  // …
}

try {
  // …
} catch (e) {}

1.4 Block indentation: +2 spaces

Each time a new block or block-like construct is opened, the indent increases by two spaces. When the block ends, the indent returns to the previous indent level. The indent level applies to both code and comments throughout the block. The tab character, for the purposes of indentation, is expressly forbidden

1.5 Array literals: optionally "block-like"

Any array literal may optionally be formatted as if it were a “block-like construct.” For example, the following are all valid (not an exhaustive list):

const a = [
  0,
  1,
  2,
];

const b =
    [0, 1, 2];
const c = [0, 1, 2];

someMethod(foo, [
  0, 1, 2,
], bar);

Other combinations are allowed, particularly when emphasizing semantic groupings between elements, but should not be used only to reduce the vertical size of larger arrays.

1.5 Object literals: optionally "block-like"

Any object literal may optionally be formatted as if it were a “block-like construct.”

const a = {
  a: 0,
  b: 1,
};

const b =
    {a: 0, b: 1};
const c = {a: 0, b: 1};

someMethod(foo, {
  a: 0, b: 1,
}, bar);

1.6 Class literals

Class literals (whether declarations or expressions) are indented as blocks. Do not add semicolons after methods, or after the closing brace of a class declaration (statements—such as assignments—that contain class expressions are still terminated with a semicolon). Use the extends keyword, but not the @extends JSDoc annotation unless the class extends a templatized type.

Example:

class Foo {
  constructor() {
    /** @type {number} */
    this.x = 42;
  }

  /** @return {number} */
  method() {
    return this.x;
  }
}
Foo.Empty = class {};
/** @extends {Foo<string>} */
foo.Bar = class extends Foo {
  /** @override */
  method() {
    return super.method() / 2;
  }
};

/** @interface */
class Frobnicator {
  /** @param {string} message */
  frobnicate(message) {}
}

1.7 Function expressions

When declaring an anonymous function in the list of arguments for a function call, the body of the function is indented two spaces more than the preceding indentation depth.

Example:

prefix.something.reallyLongFunctionName('whatever', (a1, a2) => {
  // Indent the function body +2 relative to indentation depth
  // of the 'prefix' statement one line above.
  if (a1.equals(a2)) {
    someOtherLongFunctionName(a1);
  } else {
    andNowForSomethingCompletelyDifferent(a2.parrot);
  }
});

1.8 Switch statements

As with any other block, the contents of a switch block are indented +2.

After a switch label, a newline appears, and the indentation level is increased +2, exactly as if a block were being opened. An explicit block may be used if required by lexical scoping. The following switch label returns to the previous indentation level, as if a block had been closed.

A blank line is required between a break and the following case.

Example:

switch (animal) {
  case Animal.BANDERSNATCH:
    handleBandersnatch();
    break;

  case Animal.JABBERWOCK:
    handleJabberwock();
    break;

  default:
    throw new Error('Unknown animal');
}

1.9 Semicolons are required

Every statement must be terminated with a semicolon. Relying on automatic semicolon insertion is forbidden.

1.10 Column limit: 80

JavaScript code has a column limit of 80 characters. Except lines where obeying the column limit is not possible or would hinder discoverability.

1.11 Where to break

The prime directive of line-wrapping is: prefer to break at a higher syntactic level.

Preferred:

currentEstimate =
    calc(currentEstimate + x * currentEstimate) /
        2.0;

Discouraged:

currentEstimate = calc(currentEstimate + x *
    currentEstimate) / 2.0;

In the preceding example, the syntactic levels from highest to lowest are as follows: assignment, division, function call, parameters, number constant.

Operators are wrapped as follows:

  1. When a line is broken at an operator the break comes after the symbol. (Note that this does not apply to the "dot" (.), which is not actually an operator.)
  2. A method or constructor name stays attached to the open parenthesis (() that follows it.
  3. A comma (,) stays attached to the token that precedes it.

1.12 Comments

All comments must have a space between the commented text and the comment operator. Additionally, for any comments which occur at the end of a line of code, there must be a space present between the last character of the line and the comment.

Example:

// this is allowed

/*
 * This is
 * also allowed
 */

const apples = "Are yummy"; // This is a given

Disallowed:

//Don't do this

/*
 *This is
 *not okay
 */

const apples = "Are yummy";//This is evil

1.13 Control structures

A space must be inserted between a control structure term and its following parenthesis

Example:

if (0 === true)

Disallowed:

if(0 === true)

1.14 Readability vs Terseness

When given an option between a short one-liner and a more readable multi-line statement, opt for the more readable statement.

Bad:

a != b && doThis();

Good:

if (a != b) {
  doThis();
}

2 Language Features

2.1 Use const and let

Declare all local variables with either const or let. Use const by default, unless a variable needs to be reassigned. The var keyword must not be used.

2.2 One variable per declaration

Every local variable declaration declares only one variable.

Example:

let a = 1;
let b = 2;

Disallowed:

let a = 1, b = 2;

2.3 Declared when needed, initialized as soon as possible

Local variables are not habitually declared at the start of their containing block or block-like construct. Instead, local variables are declared close to the point they are first used (within reason), to minimize their scope.

2.4 Functions

Both standard function syntax and arrow function syntax are permissible in any situation. However, it is recommended to use standard function syntax when declaring standalone functions, and arrow function syntax when declaring anonymous functions.

Example:

function myfunc(foo) {
  const bar = 4;
  return foo + bar;
};

someOtherFunction('a', (b) => {
  return b + 5;
});

Also permitted:

const myfunc = (foo) => {
  const bar = 4;
  return foo + bar;
}

someOtherFunction('a', function(b) {
  return b + 5;
});

2.5 Template literals

Use template literals (delimited with `) over complex string concatenation, particularly if multiple string literals are involved. Template literals may span multiple lines.

If a template literal spans multiple lines, it does not need to follow the indentation of the enclosing block, though it may if the added whitespace does not matter.

Example:

const arithmetic = (a, b) => {
  return `Here is a table of arithmetic operations:
${a} + ${b} = ${a + b}
${a} - ${b} = ${a - b}
${a} * ${b} = ${a * b}
${a} / ${b} = ${a / b}`;
}

2.6 Loops

When possible use, map or for..of instead of while loops and index-based for loops. The latter loops are permissible when map or for..of cannot accomplish the necessary task.

3 Documentation

3.1 Classes

Every class must have a JSDoc explaining what the class is for. All methods within the class also require a JSDoc explaining what the method does, the types of each argument, as well as what each argument is.

3.2 Functions

Every function must have a JSDoc explaining what it does, the types of each argument, and what each argument is.

3.3 Architectural Constructs

Architectural constructs, the major parts of the overlying program (i.e. tokenizer, parser, file manager) must be documented in the appropriate repo's wiki. This documentation should include: what the construct does, what parts make up the construct, and any other information that may prove valuable to future developers.

3.4 End User Documentation

Any documentation for the end user should be documented via the devcodeabode.github.io website. Repository wikis are reserved for documentation only relevant to developers working on the codebase.

4 Logging

4.1 Never Use console.log

console.log (and related functions) should not appear in committed code ready for production.

4.2 Use winston for Logging

The winston library should be used for logging purposes. Ideally, only one winston logger will exist for a given project.

4.3 Logging Levels

Level Function Call Meaning
Error logger.error(message) Something definitely went wrong with the codebase.
Warning logger.warn(message) Something went wrong. It might be user error, might be something wrong in the code, or might not be a real issue.
Info logger.info(message) Something went wrong, but it was user error and didn't actually break anything.
Verbose logger.log("verbose", message) Info, but longer.
Debug logger.log("debug", message) This is only really useful if you're actively developing.
Silly logger.log("silly", message) You wanted someone to maybe eventually see this, but they probably won't ever.