aboutsummaryrefslogtreecommitdiffhomepage
path: root/docs/development/best-practices.md
blob: d5d1f4c113ebe269ca070770d701aae793641747 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# Best practices

This document explains our best practices.
Follow these best practices when you're working on our code.

## Git branch names

Branch names should start with a [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) scope like `feat/` or `fix/`.
If you're closing an issue with your PR then put the issue number after the scope.
Finally, describe the changes in the branch in a few words.

Some good branch names:

- `feat/13732-cacache-cleanup`
- `fix/15431-gitea-automerge-strategy`
- `refactor/jest-reset-mocks`
- `docs/rewrite-packageRules-section`

Avoid branch names like `patch-1`.

If you don't know the correct Conventional Commit scope: give your branch a descriptive name like `issue-1-feature-foo`.

If you forgot to pick a good branch name when you started work, then rename the branch before creating the pull request.
Read the [GitHub Docs, renaming a branch](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-branches-in-your-repository/renaming-a-branch) to learn how to rename your branch on GitHub.

## General

- Prefer full function declarations for readability and better stack traces, so avoid `const func = ():void => {}`
- Prefer `interface` over `type` for TypeScript type declarations
- Avoid [Enums](https://github.com/renovatebot/renovate/issues/13743), use unions or [immutable objects](https://github.com/renovatebot/renovate/blob/5043379847818ac1fa71ff69c098451975e95710/lib/modules/versioning/pep440/range.ts#L8-L20) instead
- Always add unit tests for full code coverage
  - Only use `istanbul` comments for unreachable code coverage that is needed for `codecov` completion
  - Use descriptive `istanbul` comments
- Avoid cast or prefer `x as T` instead of `<T>x` cast
- Prefer `satisfies` operator over `as`, read the [TypeScript release notes for `satisfies` operator](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html#the-satisfies-operator) to learn more
- Avoid `Boolean` instead use `is` functions from `@sindresorhus/is` package, for example: `is.string`

```ts
// istanbul ignore next: can never happen
```

### Functions

- Use `function foo(){...}` to declare named functions
- Use function declaration instead of assigning function expression into local variables (`const f = function(){...}`) (TypeScript already prevents rebinding functions)
  - Exception: if the function accesses the outer scope's `this` then use arrow functions assigned to variables instead of function declarations
- Regular functions _should not_ access `this`, but arrow functions and methods may access `this`
- Only use nested functions when the [lexical scope](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) is used

#### Use arrow functions in expressions

Avoid:

```ts
bar(function(){...})
```

Use:

```ts
bar(() => {
  this.doSomething();
});
```

Generally the `this` pointer _should not_ be rebound.
Only use function expressions if you need to dynamically rebind `this`.

Source: [Google TypeScript Style Guide, function declarations](https://google.github.io/styleguide/tsguide.html#function-declarations).

## Code style

### Write understandable code

Write code that is easier to read, review and maintain.
Avoid "clever" code that's hard to understand.

Prefer verbose code which is easier for others to read and maintain than concise code which may be hard or slower for others to understand.
For example, Array `reduce()` functions are often hard to understand first time, and can be replaced with simpler `for` loops.
A `for` loop is just as fast but is simpler to understand and maintain.

#### Write single purpose functions

Single purpose functions are easier to understand, test and debug.

```ts
function caller() {
  // ..code..
  calculateUpdateAndPrint(data)
  // ..code..
}

function calculateUpdateAndPrint(...) { /* code */ }
```

Simplified code:

```ts
function caller() {
    // code..
    const res = calculate(data);
    update(res);
    print(res);
    // code..
}

function calculate(...) { /* code */ }
function update(...)    { /* code */ }
function print(...)     { /* code */ }
```

#### Keep indentation level low

Fail quickly.
Nested code logic is hard to read and prone to logic mistakes.

```ts
function foo(str: string): boolean {
  let result = false;
  if (condition(str)) {
    const x = extractData(str);
    if (x) {
      // do something
      result = true;
    }
  }
  return result;
}
```

Simplified code:

```ts
function foo(str: string): boolean {
  if (!condition(str)) {
    return false;
  }

  const x = extractData(str);
  if (!x) {
    return false;
  }

  // do something
  return true;
}
```

### Refactor one thing at a time

Refactor the code _or_ refactor the tests.

Avoid refactoring the code and tests at the same time, this can mask regression errors.

## Logging

For `WARN`, `ERROR` and `FATAL` log messages use logger metadata.
Also use logger metadata the result is a complex metadata object needing a multiple-line pretty stringification.

For `INFO` and `DEBUG` log messages inline the metadata into the log message where feasible.
It is OK to not inline metadata if it's complex, but in that case first think whether that much information really needs to be logged.

`WARN`, `ERROR` and `FATAL` messages are often used in metrics or error catching services.
These log messages should have a static `msg` component, so they can be automatically grouped or associated.

Good:

```ts
logger.debug({ config }, 'Full config');
logger.debug(`Generated branchName: ${branchName}`);
logger.warn({ presetName }, 'Failed to look up preset');
```

Avoid:

```ts
logger.debug({ branchName }, 'Generated branchName');
logger.warn(`Failed to look up preset ${presetName}`);
```

## Array constructor

Avoid the `Array()` constructor, with or without `new`, in your TypeScript code.
It has confusing and contradictory usage.
So you should avoid:

```ts
const a = new Array(2); // [undefined, undefined]
const b = new Array(2, 3); // [2, 3];
```

Instead, always use bracket notation to initialize arrays, or `from` to initialize an Array with a certain size.
For example:

```ts
// [0, 0, 0, 0, 0]
Array.from<number>({ length: 5 }).fill(0);
```

[Source](https://google.github.io/styleguide/tsguide.html#array-constructor)

## Iterating objects & containers

Use `for ( ... of ...)` loops instead of `[Array|Set|Map].prototype.forEach` and `for ( ... in ...)`.

- Using `for ( ... in ...)` for objects is error-prone. It will include enumerable properties from the prototype chain
- Using `for ( ... in ...)` to iterate over arrays, will counterintuitively return the array's indices
- Avoid `[Array|Set|Map].prototype.forEach`. It makes code harder to debug and defeats some useful compiler checks like reachability

Only use `Array.prototype.map()` when the return value is used, otherwise use `for ( ... of ...)`.

Source: [Google TypeScript Style Guide, iterating objects](https://google.github.io/styleguide/tsguide.html#iterating-objects)

## Exports

Use named exports in all code.
Avoid default `exports`.
This way all `imports` follow the same pattern.

Source: [Google TypeScript Style Guide, exports](https://google.github.io/styleguide/tsguide.html#exports)

## Imports

Use [ES6 module](https://exploringjs.com/es6/ch_modules.html#sec_basics-of-es6-modules) syntax.
For example:

```ts
import { square, diag } from 'lib';

// You may also use:

import * as lib from 'lib';
```

And avoid `require`:

```ts
import x = require('...');
```

## HTTP & RESTful API request handling

Prefer using `Http` from `util/http` to simplify HTTP request handling and to enable authentication and caching, as our `Http` class will transparently handle host rules.
For example:

```ts
import { Http } from '../../../util/http';

const http = new Http('some-host-type');

try {
    const body = (await http.getJson<Response>(url)).body;
} catch (err) {
  ...
}
```

## Async functions

Never use `Promise.resolve` in async functions.
Never use `Promise.reject` in async functions, instead throw an `Error` class type.

## Dates and times

Use the [`Luxon` package](https://www.npmjs.com/package/luxon) to handle dates and times.
Use `UTC` to be time zone independent.

[Example from our code](https://github.com/renovatebot/renovate/blob/5043379847818ac1fa71ff69c098451975e95710/lib/modules/versioning/distro.ts#L133-L134)
:

```ts
if (end) {
  const now = DateTime.now().toUTC();
  const eol = DateTime.fromISO(end, { zone: 'utc' });
  return eol < now;
}
```

## Unit testing

- Separate the _Arrange_, _Act_ and _Assert_ phases with newlines
- Use `it.each` rather than `test.each`
- Prefer [Tagged Template Literal](https://jestjs.io/docs/api#2-testeachtablename-fn-timeout) style for `it.each`, Prettier will help with formatting
  - See [Example](https://github.com/renovatebot/renovate/blob/768e178419437a98f5ce4996bafd23f169e530b4/lib/modules/platform/util.spec.ts#L8-L18)
- Mock Date/Time when testing a Date/Time dependent module
  - For `Luxon` mocking see [Example](https://github.com/renovatebot/renovate/blob/5043379847818ac1fa71ff69c098451975e95710/lib/modules/versioning/distro.spec.ts#L7-L10)
- Prefer `jest.spyOn` for mocking single functions, or mock entire modules
  - Avoid overwriting functions, for example: (`func = jest.fn();`)
- Prefer `toEqual`
- Use `toMatchObject` for huge objects when only parts need to be tested
- Avoid `toMatchSnapshot`, only use it for:
  - huge strings like the Renovate PR body text
  - huge complex objects where you only need to test parts
- Avoid exporting functions purely for the purpose of testing unless you really need to
- Avoid cast or prefer `x as T` instead of `<T>x` cast
  - Use `partial<T>()` from `test/util` if only a partial object is required

## Fixtures

Where possible, reduce the test fixture to a size where an inline `codeBlock` is possible to use instead of a separate fixture file.
Inline `codeBlock`s improve performance plus are more readable.

Use the `Fixture` class if loading fixtures from files.
For example:

```ts
Fixture.get('./file.json'); // for loading string data
Fixture.getJson('./file.json'); // for loading and parsing objects
Fixture.getBinary('./file.json'); // for retrieving a buffer
```

## Working with vanilla JS files (renovate/tools only)

Declare types and function prototypes with [JSDoc](https://jsdoc.app/index.html).

[Example](https://github.com/renovatebot/renovate/blob/5043379847818ac1fa71ff69c098451975e95710/tools/distro-json-generate.mjs#L7-L17)

## Classes

- Use [Typescript getter setters (Accessors) when needed](https://google.github.io/styleguide/tsguide.html#properties-used-outside-of-class-lexical-scope).
  The getter must be a `pure function` i.e.
  - The function return values are identical for identical arguments
  - The function has no side effects

[Source](https://en.wikipedia.org/wiki/Pure_function)

- Omit constructors when defining Static classes
- [No `#private` fields](https://google.github.io/styleguide/tsguide.html#private-fields), use TypeScript's visibility annotations instead
- Avoid underscore suffixes or prefixes like `_prop`, instead use [whole words](https://google.github.io/styleguide/tsguide.html#properties-used-outside-of-class-lexical-scope) as the suffix or prefix like `internalProp`

## regex

Use [Named Capturing Groups](https://www.regular-expressions.info/named.html) when capturing multiple groups, for example: `(?<groupName>CapturedGroup)`.

## Windows

We recommend you set [`core.autocrlf = input`](https://git-scm.com/docs/gitattributes#_text) in your Git config.
You can do this by running this Git command:

```bash
git config --global core.autocrlf input
```

This prevents the carriage return `\r\n` which may confuse Renovate.
You can also set the line endings in your repository by adding `* text=auto eol=lf` to your `.gitattributes` file.