1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00
joplin/readme/coding_style.md
2023-03-13 19:31:22 +00:00

9.4 KiB

Coding style

Coding style is mostly enforced by a pre-commit hook that runs eslint. This hook is installed whenever running yarn install on any of the application directory. If for some reason the pre-commit hook didn't get installed, you can manually install it by running yarn install at the root of the repository.

Enforcing rules using eslint

Whenever possible, coding style should be enforced using an eslint rule. To do so, add the relevant rule or plugin to eslintrc.js. To manually run the linter, run yarn run linter ./ from the root of the project.

When adding a rule, you will often find that many files will no longer pass the linter. In that case, you have two options:

  • Fix the files one by one. If there aren't too many files, and the changes are simple (they are unlikely to introduce regressions), this is the preferred solution.

  • Or use yarn run linter-interactive ./ to disable existing errors. The interactive tool will process all the files and you can then choose to disable any existing error that it finds (by adding a eslint-disable-next-line comment above it). This allows keeping the existing, working codebase as it is, and enforcing that new code follows the rule. When using this method, add the comment "Old code before rule was applied" so that we can easily find back all the lines that have been automatically disabled.

Use TypeScript for new files

Creating a new .ts file

Because the TypeScript compiler generates .js files, be sure to add these new .js files to .eslintignore and .gitignore.

To do this,

  1. If the TypeScript compiler has already generated a .js file for the new .ts file, delete it.
  2. Run yarn run updateIgnored in the root directory of the project (or yarn run postinstall)

Convert existing .js files to TypeScript before modifying

Even if you are modifying a file that was originally in JavaScript you should ideally convert it first to TypeScript before modifying it.

If this is a large file however please ask first if it needs to be converted. Some very old and large JS files are tricky to convert properly due to poorly defined types, so in some cases it's better to leave that for another day (or another PR).

Prefer import to require

In TypeScript files prefer import to require so that we can benefit from type-checking. If it does not work, you may have to add the type using yarn add @types/NAME_OF_PACKAGE. If you are trying to import an old package, it may not have TypeScript types and in this case using require() is acceptable.

Avoid inline types

In general please define types separately as it improves readability and it means the type can be re-used.

BAD:

const config: { [key: string]: Knex.Config } = {
	// ...
}	

Good:

type Config = Record<string, Knex.Config>;

const config: Config = {
	// ...
}	

Filenames

Use the same case for imported and exported members

If you create a file that exports a single function called processData(), the file should be named processData.ts. When importing, it should be imported as processData, too. Basically, be consistent with naming, even though JS allows things to be named differently.

BAD:

// ProcessDATA.ts
export default const processData = () => {
	// ...
};

// foo.ts
import doDataProcessing from './ProcessDATA';

doDataProcessing();
...

Good:

// processData.ts
export default const processData = () => {
	// ...
};

// foo.ts
import processData from './processData';

processData();
...

Only import what you need

Only import what you need so that we can potentially benefit from tree shaking if we ever implement it.

BAD:

import * as fs from 'fs-extra';
// ...
fs.writeFile('example.md', 'example');

Good:

import { writeFile } from 'fs-extra';
// ...
writeFile('example.md', 'example');

Use camelCase for constants in new code

BAD:

// Bad! Don't use in new code!
const GRAVITY_ACCEL = 9.8;

Good:

const gravityAccel = 9.8;

Declare variables just before their usage

BAD:

// Bad!
let foo, bar;

const doThings = () => {
	// do things unrelated to foo, bar
};

// Do things involving foo and bar
foo = Math.random();
bar = foo + Math.random() / 100;
foo += Math.sin(bar + Math.tan(foo));
...

Good:

...
const doThings = () => {
	// do things unrelated to foo, bar
};

// Do things involving foo and bar
let foo = Math.random();
let bar = foo + Math.random() / 100;
foo += Math.sin(bar + Math.tan(foo));
...

Don't allow this to lead to duplicate code, however. If constants are used multiple times, it's okay to declare them at the top of a file or in a separate, imported file.

Prefer const to let (where possible)

Prefer () => {} to function() { ... }

Doing this avoids having to deal with the this keyword. Not having it makes it easier to refactor class components into React Hooks, because any use of this (used in classes) will be correctly detected as invalid by TypeScript.

BAD:

// Bad!
function foo() {
	...
}

Good:

const foo = () => {
	...
};

See also

Avoid default and optional parameters

As much as possible, avoid default parameters in function definitions and optional fields in interface definitions. When all parameters are required, it is much easier to refactor the code because the compiler will automatically catch any missing parameters.

React

Use function components for new code

New code should use React Hooks and function components, rather than objects that extend Component.

Bad:

// Don't do this in new code!
class Example extends React.Component {
	public constructor(props: { text: string }) {
		super(props);
	}

	public render() {
		return (
			<div>${text}</div>
		);
	}
}

Good:

const Example = (props: { text: string }) => {
	return (
		<div>${text}</div>
	);
};

Use react custom hooks to simplify long code

If eslint gives an error about useFoo being called outside of a component, be sure the custom hook is titled appropriately.

Database

Use snake_case

We use snake_case for table names and column names.

Everything is NOT NULL

All columns should be defined as NOT NULL, possibly with a default value (but see below). This helps keeping queries more simple as we don't have to do check for both NULL and 0 or empty string.

Use defaults sparingly

Don't automatically give a default valuet to a column - in many cases it's better to require the user to explicitly set the value, otherwise it will be set to a default they might not know about or want. Exceptions can be less important columns, things like timestamp, or columns that are going to be set by the system.

Use an integer for enum-like values

If a column can be set to a fixed number of values, please set the type to integer. In code, you would then have a TypeScript enum that defines what each values is for. For example:

export enum Action {
	Create = 1,
	Update = 2,
	Delete = 3,
}

We don't use built-in database enums because they make migrations difficult. They provide added readability when accessing the database directly, but it is not worth the extra trouble.

Prefer using tinyint(1) to bool

Booleans are not a distinct types in many common DBMS, including SQLite (which we use) and MySQL, so prefer using a tinyint(1) instead.

Web requests and API

Use snake_case

We use snake_case for end points and query parameters.

See also

Other projects' style guides

We aren't using these guides, but they may still be helpful!