@mui/internal-code-infra 0.0.4-canary.64 → 0.0.4-canary.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/eslint/mui/rules/no-floating-cleanup.d.mts +5 -0
- package/package.json +2 -6
- package/src/eslint/mui/index.mjs +2 -0
- package/src/eslint/mui/rules/no-floating-cleanup.mjs +187 -0
- package/src/eslint/mui/rules/no-floating-cleanup.test.mjs +141 -0
- package/build/markdownlint/duplicate-h1.d.mts +0 -56
- package/build/markdownlint/git-diff.d.mts +0 -11
- package/build/markdownlint/index.d.mts +0 -52
- package/build/markdownlint/straight-quotes.d.mts +0 -11
- package/build/markdownlint/table-alignment.d.mts +0 -11
- package/build/markdownlint/terminal-language.d.mts +0 -11
- package/src/markdownlint/duplicate-h1.mjs +0 -69
- package/src/markdownlint/git-diff.mjs +0 -31
- package/src/markdownlint/index.mjs +0 -62
- package/src/markdownlint/straight-quotes.mjs +0 -26
- package/src/markdownlint/table-alignment.mjs +0 -42
- package/src/markdownlint/terminal-language.mjs +0 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mui/internal-code-infra",
|
|
3
|
-
"version": "0.0.4-canary.
|
|
3
|
+
"version": "0.0.4-canary.66",
|
|
4
4
|
"author": "MUI Team",
|
|
5
5
|
"description": "Infra scripts and configs to be used across MUI repos.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,10 +29,6 @@
|
|
|
29
29
|
"types": "./build/utils/github.d.mts",
|
|
30
30
|
"default": "./src/utils/github.mjs"
|
|
31
31
|
},
|
|
32
|
-
"./markdownlint": {
|
|
33
|
-
"types": "./build/markdownlint/index.d.mts",
|
|
34
|
-
"default": "./src/markdownlint/index.mjs"
|
|
35
|
-
},
|
|
36
32
|
"./remark": {
|
|
37
33
|
"types": "./build/remark/config.d.mts",
|
|
38
34
|
"default": "./src/remark/config.mjs"
|
|
@@ -192,7 +188,7 @@
|
|
|
192
188
|
"publishConfig": {
|
|
193
189
|
"access": "public"
|
|
194
190
|
},
|
|
195
|
-
"gitSha": "
|
|
191
|
+
"gitSha": "db0c201b6c697cff1aab50b9b33270720c93f424",
|
|
196
192
|
"scripts": {
|
|
197
193
|
"build": "tsgo -p tsconfig.build.json",
|
|
198
194
|
"typescript": "tsgo -noEmit",
|
package/src/eslint/mui/index.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import straightQuotes from './rules/straight-quotes.mjs';
|
|
|
13
13
|
import addUndefToOptional from './rules/add-undef-to-optional.mjs';
|
|
14
14
|
import flattenParentheses from './rules/flatten-parentheses.mjs';
|
|
15
15
|
import noPresentationRole from './rules/no-presentation-role.mjs';
|
|
16
|
+
import noFloatingCleanup from './rules/no-floating-cleanup.mjs';
|
|
16
17
|
|
|
17
18
|
/** @type {import('eslint').ESLint.Plugin} */
|
|
18
19
|
const muiPlugin = {
|
|
@@ -37,6 +38,7 @@ const muiPlugin = {
|
|
|
37
38
|
'add-undef-to-optional': /** @type {any} */ (addUndefToOptional),
|
|
38
39
|
'flatten-parentheses': /** @type {any} */ (flattenParentheses),
|
|
39
40
|
'no-presentation-role': noPresentationRole,
|
|
41
|
+
'no-floating-cleanup': /** @type {any} */ (noFloatingCleanup),
|
|
40
42
|
},
|
|
41
43
|
};
|
|
42
44
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* no-floating-cleanup
|
|
6
|
+
*
|
|
7
|
+
* Like `@typescript-eslint/no-floating-promises`, but for functions: reports
|
|
8
|
+
* call expressions used as statements whose return value is a function
|
|
9
|
+
* (e.g. an `unsubscribe` / cleanup callback). Ignoring such a value usually
|
|
10
|
+
* means the subscription can never be torn down — a leak.
|
|
11
|
+
*
|
|
12
|
+
* Requires type information (parserOptions.project / projectService).
|
|
13
|
+
*
|
|
14
|
+
* Calls whose resolved signature declares a `this` return type are never
|
|
15
|
+
* reported: fluent / builder APIs (e.g. d3 scales) chain by returning `this`,
|
|
16
|
+
* so the discarded result is callable but not a leaked cleanup function.
|
|
17
|
+
*
|
|
18
|
+
* Opt-out at a call site, same convention as no-floating-promises:
|
|
19
|
+
* void subscribe(cb);
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const RULE_NAME = 'no-floating-cleanup';
|
|
23
|
+
|
|
24
|
+
const createRule = ESLintUtils.RuleCreator(
|
|
25
|
+
(name) =>
|
|
26
|
+
`https://github.com/mui/mui-public/blob/master/packages/code-infra/src/eslint/mui/rules/${name}.mjs`,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export default createRule({
|
|
30
|
+
meta: {
|
|
31
|
+
type: 'problem',
|
|
32
|
+
docs: {
|
|
33
|
+
description:
|
|
34
|
+
'Disallow ignoring returned cleanup/unsubscribe functions, which leaks subscriptions.',
|
|
35
|
+
},
|
|
36
|
+
messages: {
|
|
37
|
+
floatingCleanup:
|
|
38
|
+
"Return value of type '{{typeName}}' is ignored. This likely leaks a subscription — " +
|
|
39
|
+
'store the cleanup function, or discard it explicitly with the `void` operator.',
|
|
40
|
+
},
|
|
41
|
+
schema: [],
|
|
42
|
+
},
|
|
43
|
+
name: RULE_NAME,
|
|
44
|
+
defaultOptions: [],
|
|
45
|
+
create(context) {
|
|
46
|
+
const services = ESLintUtils.getParserServices(context);
|
|
47
|
+
const checker = services.program.getTypeChecker();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Does this type (or any union member) look like a cleanup function?
|
|
51
|
+
* @param {import('typescript').Type} type
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
function isCleanupType(type) {
|
|
55
|
+
if (type.isUnion()) {
|
|
56
|
+
return type.types.some(isCleanupType);
|
|
57
|
+
}
|
|
58
|
+
// Never fire on any/unknown/never — too noisy and not provable.
|
|
59
|
+
// eslint-disable-next-line no-bitwise -- TypeScript type flags are a bitmask
|
|
60
|
+
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {import('typescript').Type} type
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
function describeType(type) {
|
|
72
|
+
return (type.aliasSymbol && type.aliasSymbol.getName()) || checker.typeToString(type);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Does this call resolve to a signature that returns `this`? Fluent /
|
|
77
|
+
* builder APIs (e.g. d3 scales) chain by returning `this`, so the
|
|
78
|
+
* discarded result is callable but never a leaked cleanup function —
|
|
79
|
+
* those calls should not be reported. Covers both an explicit `: this`
|
|
80
|
+
* annotation and an inferred `this` return (no annotation).
|
|
81
|
+
* @param {import('@typescript-eslint/utils').TSESTree.Node} estreeCall
|
|
82
|
+
* @returns {boolean}
|
|
83
|
+
*/
|
|
84
|
+
function returnsThis(estreeCall) {
|
|
85
|
+
/** @type {import('typescript').Node | undefined} */
|
|
86
|
+
let tsNode = services.esTreeNodeToTSNodeMap.get(estreeCall);
|
|
87
|
+
// Unwrap `await (...)` / parens down to the underlying call.
|
|
88
|
+
while (tsNode && (ts.isAwaitExpression(tsNode) || ts.isParenthesizedExpression(tsNode))) {
|
|
89
|
+
tsNode = tsNode.expression;
|
|
90
|
+
}
|
|
91
|
+
if (!tsNode || !(ts.isCallExpression(tsNode) || ts.isNewExpression(tsNode))) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const declaration = checker.getResolvedSignature(tsNode)?.getDeclaration();
|
|
95
|
+
if (!declaration) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
// Explicit `: this` annotation.
|
|
99
|
+
if (declaration.type?.kind === ts.SyntaxKind.ThisType) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
// Inferred `this` return: the *declared* (uninstantiated) signature's
|
|
103
|
+
// return type is the polymorphic `this` type, which prints as `this`.
|
|
104
|
+
// (The resolved signature would have it bound to the receiver type.)
|
|
105
|
+
const declaredSignature = checker.getSignatureFromDeclaration(declaration);
|
|
106
|
+
return (
|
|
107
|
+
!!declaredSignature &&
|
|
108
|
+
checker.typeToString(checker.getReturnTypeOfSignature(declaredSignature)) === 'this'
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Unwrap an expression-statement expression down to the calls whose
|
|
114
|
+
* results are being discarded. Mirrors the cases no-floating-promises
|
|
115
|
+
* handles: plain calls, optional chains, `await`, comma sequences,
|
|
116
|
+
* and the operand(s) of unary / logical / conditional expressions.
|
|
117
|
+
* A top-level `void` operator is the explicit opt-out, so we stop there.
|
|
118
|
+
* @param {import('@typescript-eslint/utils').TSESTree.Node} expr
|
|
119
|
+
* @param {import('@typescript-eslint/utils').TSESTree.Node[]} [out]
|
|
120
|
+
* @returns {import('@typescript-eslint/utils').TSESTree.Node[]}
|
|
121
|
+
*/
|
|
122
|
+
function findDiscardedCalls(expr, out = []) {
|
|
123
|
+
switch (expr.type) {
|
|
124
|
+
case 'CallExpression':
|
|
125
|
+
case 'NewExpression':
|
|
126
|
+
out.push(expr);
|
|
127
|
+
break;
|
|
128
|
+
case 'ChainExpression':
|
|
129
|
+
findDiscardedCalls(expr.expression, out);
|
|
130
|
+
break;
|
|
131
|
+
case 'AwaitExpression':
|
|
132
|
+
// type of the AwaitExpression node is already the awaited type
|
|
133
|
+
if (expr.argument.type === 'CallExpression' || expr.argument.type === 'ChainExpression') {
|
|
134
|
+
out.push(expr);
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
case 'UnaryExpression':
|
|
138
|
+
// `void expr` is the explicit opt-out; any other unary operator
|
|
139
|
+
// (e.g. `!subscribe()`) still discards the operand's result.
|
|
140
|
+
if (expr.operator !== 'void') {
|
|
141
|
+
findDiscardedCalls(expr.argument, out);
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
case 'LogicalExpression':
|
|
145
|
+
// `a && subscribe()`, `a || subscribe()`, `a ?? subscribe()`:
|
|
146
|
+
// either side may be the discarded value.
|
|
147
|
+
findDiscardedCalls(expr.left, out);
|
|
148
|
+
findDiscardedCalls(expr.right, out);
|
|
149
|
+
break;
|
|
150
|
+
case 'ConditionalExpression':
|
|
151
|
+
// `cond ? subscribe() : other()`: the branches are discarded,
|
|
152
|
+
// not the test.
|
|
153
|
+
findDiscardedCalls(expr.consequent, out);
|
|
154
|
+
findDiscardedCalls(expr.alternate, out);
|
|
155
|
+
break;
|
|
156
|
+
case 'SequenceExpression':
|
|
157
|
+
// every value except the last is discarded; the last one is the
|
|
158
|
+
// statement's value, which is also discarded here
|
|
159
|
+
expr.expressions.forEach((subExpr) => findDiscardedCalls(subExpr, out));
|
|
160
|
+
break;
|
|
161
|
+
default:
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
/** @param {import('@typescript-eslint/utils').TSESTree.ExpressionStatement} node */
|
|
169
|
+
ExpressionStatement(node) {
|
|
170
|
+
findDiscardedCalls(node.expression).forEach((call) => {
|
|
171
|
+
// Fluent/builder calls return `this` for chaining — not a leak.
|
|
172
|
+
if (returnsThis(call)) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const type = services.getTypeAtLocation(call);
|
|
176
|
+
if (isCleanupType(type)) {
|
|
177
|
+
context.report({
|
|
178
|
+
node: call,
|
|
179
|
+
messageId: 'floatingCleanup',
|
|
180
|
+
data: { typeName: describeType(type) },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
import TSESlintParser from '@typescript-eslint/parser';
|
|
3
|
+
import rule from './no-floating-cleanup.mjs';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester({
|
|
6
|
+
languageOptions: {
|
|
7
|
+
parser: TSESlintParser,
|
|
8
|
+
parserOptions: {
|
|
9
|
+
tsconfigRootDir: import.meta.dirname,
|
|
10
|
+
projectService: {
|
|
11
|
+
allowDefaultProject: ['*.ts'],
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
ruleTester.run('no-floating-cleanup', rule, {
|
|
18
|
+
valid: [
|
|
19
|
+
{
|
|
20
|
+
name: 'stored cleanup function',
|
|
21
|
+
code: `
|
|
22
|
+
type Unsubscribe = () => void;
|
|
23
|
+
declare function subscribe(cb: () => void): Unsubscribe;
|
|
24
|
+
function f() {
|
|
25
|
+
const un = subscribe(() => {});
|
|
26
|
+
un();
|
|
27
|
+
}
|
|
28
|
+
`,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'explicit void opt-out',
|
|
32
|
+
code: `
|
|
33
|
+
type Unsubscribe = () => void;
|
|
34
|
+
declare function subscribe(cb: () => void): Unsubscribe;
|
|
35
|
+
function f() {
|
|
36
|
+
void subscribe(() => {});
|
|
37
|
+
}
|
|
38
|
+
`,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'call returning void',
|
|
42
|
+
code: `
|
|
43
|
+
declare function noop(): void;
|
|
44
|
+
function f() {
|
|
45
|
+
noop();
|
|
46
|
+
}
|
|
47
|
+
`,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'fluent builder methods returning `this`',
|
|
51
|
+
code: `
|
|
52
|
+
interface Scale {
|
|
53
|
+
(value: number): number;
|
|
54
|
+
domain(d: number[]): this;
|
|
55
|
+
range(r: number[]): this;
|
|
56
|
+
}
|
|
57
|
+
declare const scale: Scale;
|
|
58
|
+
function f() {
|
|
59
|
+
scale.domain([0, 1]);
|
|
60
|
+
scale.range([0, 100]);
|
|
61
|
+
}
|
|
62
|
+
`,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'fluent builder method with an inferred `this` return',
|
|
66
|
+
code: `
|
|
67
|
+
class Builder {
|
|
68
|
+
configure() {
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
declare const builder: Builder;
|
|
73
|
+
function f() {
|
|
74
|
+
builder.configure();
|
|
75
|
+
}
|
|
76
|
+
`,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
invalid: [
|
|
80
|
+
{
|
|
81
|
+
name: 'discarded unsubscribe function',
|
|
82
|
+
code: `
|
|
83
|
+
type Unsubscribe = () => void;
|
|
84
|
+
declare function subscribe(cb: () => void): Unsubscribe;
|
|
85
|
+
function f() {
|
|
86
|
+
subscribe(() => {});
|
|
87
|
+
}
|
|
88
|
+
`,
|
|
89
|
+
errors: [{ messageId: 'floatingCleanup', line: 5 }],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'call returning a fresh callable (not `this`)',
|
|
93
|
+
code: `
|
|
94
|
+
interface Scale {
|
|
95
|
+
(value: number): number;
|
|
96
|
+
copy(): Scale;
|
|
97
|
+
}
|
|
98
|
+
declare const scale: Scale;
|
|
99
|
+
function f() {
|
|
100
|
+
scale.copy();
|
|
101
|
+
}
|
|
102
|
+
`,
|
|
103
|
+
errors: [{ messageId: 'floatingCleanup', line: 8 }],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'discarded cleanup behind a unary operator',
|
|
107
|
+
code: `
|
|
108
|
+
type Unsubscribe = () => void;
|
|
109
|
+
declare function subscribe(cb: () => void): Unsubscribe;
|
|
110
|
+
function f() {
|
|
111
|
+
!subscribe(() => {});
|
|
112
|
+
}
|
|
113
|
+
`,
|
|
114
|
+
errors: [{ messageId: 'floatingCleanup', line: 5 }],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'discarded cleanup in a logical expression',
|
|
118
|
+
code: `
|
|
119
|
+
type Unsubscribe = () => void;
|
|
120
|
+
declare function subscribe(cb: () => void): Unsubscribe;
|
|
121
|
+
declare const enabled: boolean;
|
|
122
|
+
function f() {
|
|
123
|
+
enabled && subscribe(() => {});
|
|
124
|
+
}
|
|
125
|
+
`,
|
|
126
|
+
errors: [{ messageId: 'floatingCleanup', line: 6 }],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'discarded cleanup in a conditional expression',
|
|
130
|
+
code: `
|
|
131
|
+
type Unsubscribe = () => void;
|
|
132
|
+
declare function subscribe(cb: () => void): Unsubscribe;
|
|
133
|
+
declare const enabled: boolean;
|
|
134
|
+
function f() {
|
|
135
|
+
enabled ? subscribe(() => {}) : undefined;
|
|
136
|
+
}
|
|
137
|
+
`,
|
|
138
|
+
errors: [{ messageId: 'floatingCleanup', line: 6 }],
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
});
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @typedef {[string, string]} Attr
|
|
3
|
-
*/
|
|
4
|
-
export type Attr = [string, string];
|
|
5
|
-
export type Token = {
|
|
6
|
-
type: string;
|
|
7
|
-
info: string;
|
|
8
|
-
tag: string;
|
|
9
|
-
content: string;
|
|
10
|
-
lineNumber: number;
|
|
11
|
-
attrs: Attr[];
|
|
12
|
-
};
|
|
13
|
-
export type OnErrorObj = {
|
|
14
|
-
lineNumber: number;
|
|
15
|
-
detail?: string;
|
|
16
|
-
};
|
|
17
|
-
export type OnError = (err: OnErrorObj) => void;
|
|
18
|
-
export type MdParams = {
|
|
19
|
-
name: string;
|
|
20
|
-
lines: string[];
|
|
21
|
-
tokens: Token[];
|
|
22
|
-
};
|
|
23
|
-
/**
|
|
24
|
-
* @typedef {Object} Token
|
|
25
|
-
* @property {string} type
|
|
26
|
-
* @property {string} info
|
|
27
|
-
* @property {string} tag
|
|
28
|
-
* @property {string} content
|
|
29
|
-
* @property {number} lineNumber
|
|
30
|
-
* @property {Attr[]} attrs
|
|
31
|
-
*/
|
|
32
|
-
/**
|
|
33
|
-
* @typedef {Object} OnErrorObj
|
|
34
|
-
* @property {number} lineNumber
|
|
35
|
-
* @property {string} [detail]
|
|
36
|
-
*/
|
|
37
|
-
/**
|
|
38
|
-
* @typedef {(err: OnErrorObj) => void} OnError
|
|
39
|
-
*/
|
|
40
|
-
/**
|
|
41
|
-
* @typedef {Object} MdParams
|
|
42
|
-
* @property {string} name
|
|
43
|
-
* @property {string[]} lines
|
|
44
|
-
* @property {Token[]} tokens
|
|
45
|
-
*/
|
|
46
|
-
declare const _default: {
|
|
47
|
-
names: string[];
|
|
48
|
-
description: string;
|
|
49
|
-
tags: string[];
|
|
50
|
-
/**
|
|
51
|
-
* @param {import('./duplicate-h1.mjs').MdParams} params
|
|
52
|
-
* @param {import('./duplicate-h1.mjs').OnError} onError
|
|
53
|
-
*/
|
|
54
|
-
function: (params: import('./duplicate-h1.mjs').MdParams, onError: import('./duplicate-h1.mjs').OnError) => void;
|
|
55
|
-
};
|
|
56
|
-
export default _default;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
declare const _default: {
|
|
2
|
-
names: string[];
|
|
3
|
-
description: string;
|
|
4
|
-
tags: string[];
|
|
5
|
-
/**
|
|
6
|
-
* @param {import('./duplicate-h1.mjs').MdParams} params
|
|
7
|
-
* @param {import('./duplicate-h1.mjs').OnError} onError
|
|
8
|
-
*/
|
|
9
|
-
function: (params: import('./duplicate-h1.mjs').MdParams, onError: import('./duplicate-h1.mjs').OnError) => void;
|
|
10
|
-
};
|
|
11
|
-
export default _default;
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import straightQuotes from './straight-quotes.mjs';
|
|
2
|
-
/**
|
|
3
|
-
* Create a base configuration for markdownlint.
|
|
4
|
-
* @param {Object} options
|
|
5
|
-
* @param {(typeof straightQuotes)[]} [options.rules] - An array of custom rules to include.
|
|
6
|
-
* @param {string[]} [options.ignores] - An array of glob patterns to ignore.
|
|
7
|
-
* @returns
|
|
8
|
-
*/
|
|
9
|
-
export declare function createBaseConfig(options?: {
|
|
10
|
-
rules?: (typeof straightQuotes)[];
|
|
11
|
-
ignores?: string[];
|
|
12
|
-
}): {
|
|
13
|
-
config: {
|
|
14
|
-
default: boolean;
|
|
15
|
-
MD004: boolean;
|
|
16
|
-
MD009: {
|
|
17
|
-
br_spaces: number;
|
|
18
|
-
strict: boolean;
|
|
19
|
-
list_item_empty_lines: boolean;
|
|
20
|
-
};
|
|
21
|
-
MD013: boolean;
|
|
22
|
-
MD014: boolean;
|
|
23
|
-
MD024: {
|
|
24
|
-
siblings_only: boolean;
|
|
25
|
-
};
|
|
26
|
-
MD025: {
|
|
27
|
-
level: number;
|
|
28
|
-
front_matter_title: string;
|
|
29
|
-
};
|
|
30
|
-
MD033: boolean;
|
|
31
|
-
MD034: boolean;
|
|
32
|
-
MD028: boolean;
|
|
33
|
-
MD029: boolean;
|
|
34
|
-
MD031: boolean;
|
|
35
|
-
MD036: boolean;
|
|
36
|
-
MD051: boolean;
|
|
37
|
-
MD052: boolean;
|
|
38
|
-
MD059: boolean;
|
|
39
|
-
straightQuotes: boolean;
|
|
40
|
-
gitDiff: boolean;
|
|
41
|
-
tableAlignment: boolean;
|
|
42
|
-
terminalLanguage: boolean;
|
|
43
|
-
duplicateH1: boolean;
|
|
44
|
-
};
|
|
45
|
-
customRules: {
|
|
46
|
-
names: string[];
|
|
47
|
-
description: string;
|
|
48
|
-
tags: string[];
|
|
49
|
-
function: (params: import('./duplicate-h1.mjs').MdParams, onError: import('./duplicate-h1.mjs').OnError) => void;
|
|
50
|
-
}[];
|
|
51
|
-
ignores: string[];
|
|
52
|
-
};
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
declare const _default: {
|
|
2
|
-
names: string[];
|
|
3
|
-
description: string;
|
|
4
|
-
tags: string[];
|
|
5
|
-
/**
|
|
6
|
-
* @param {import('./duplicate-h1.mjs').MdParams} params
|
|
7
|
-
* @param {import('./duplicate-h1.mjs').OnError} onError
|
|
8
|
-
*/
|
|
9
|
-
function: (params: import('./duplicate-h1.mjs').MdParams, onError: import('./duplicate-h1.mjs').OnError) => void;
|
|
10
|
-
};
|
|
11
|
-
export default _default;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
declare const _default: {
|
|
2
|
-
names: string[];
|
|
3
|
-
description: string;
|
|
4
|
-
tags: string[];
|
|
5
|
-
/**
|
|
6
|
-
* @param {import('./duplicate-h1.mjs').MdParams} params
|
|
7
|
-
* @param {import('./duplicate-h1.mjs').OnError} onError
|
|
8
|
-
*/
|
|
9
|
-
function: (params: import('./duplicate-h1.mjs').MdParams, onError: import('./duplicate-h1.mjs').OnError) => void;
|
|
10
|
-
};
|
|
11
|
-
export default _default;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
declare const _default: {
|
|
2
|
-
names: string[];
|
|
3
|
-
description: string;
|
|
4
|
-
tags: string[];
|
|
5
|
-
/**
|
|
6
|
-
* @param {import('./duplicate-h1.mjs').MdParams} params
|
|
7
|
-
* @param {import('./duplicate-h1.mjs').OnError} onError
|
|
8
|
-
*/
|
|
9
|
-
function: (params: import('./duplicate-h1.mjs').MdParams, onError: import('./duplicate-h1.mjs').OnError) => void;
|
|
10
|
-
};
|
|
11
|
-
export default _default;
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @typedef {[string, string]} Attr
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @typedef {Object} Token
|
|
7
|
-
* @property {string} type
|
|
8
|
-
* @property {string} info
|
|
9
|
-
* @property {string} tag
|
|
10
|
-
* @property {string} content
|
|
11
|
-
* @property {number} lineNumber
|
|
12
|
-
* @property {Attr[]} attrs
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @typedef {Object} OnErrorObj
|
|
17
|
-
* @property {number} lineNumber
|
|
18
|
-
* @property {string} [detail]
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @typedef {(err: OnErrorObj) => void} OnError
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @typedef {Object} MdParams
|
|
27
|
-
* @property {string} name
|
|
28
|
-
* @property {string[]} lines
|
|
29
|
-
* @property {Token[]} tokens
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
// This rule is an extension of MD025/no-multiple-top-level-headings.
|
|
33
|
-
// The rule is buggy https://github.com/DavidAnson/markdownlint/pull/1109
|
|
34
|
-
// but also blog headers don't tell you that h1 is already injected.
|
|
35
|
-
export default {
|
|
36
|
-
names: ['duplicateH1'],
|
|
37
|
-
description: 'Multiple top-level headings in the same document.',
|
|
38
|
-
tags: ['headings'],
|
|
39
|
-
/**
|
|
40
|
-
* @param {import('./duplicate-h1.mjs').MdParams} params
|
|
41
|
-
* @param {import('./duplicate-h1.mjs').OnError} onError
|
|
42
|
-
*/
|
|
43
|
-
function: (params, onError) => {
|
|
44
|
-
/**
|
|
45
|
-
* @type {number|boolean}
|
|
46
|
-
*/
|
|
47
|
-
let hasTopLevelHeading = false;
|
|
48
|
-
params.tokens.forEach((token) => {
|
|
49
|
-
if (token.type === 'heading_open' && token.tag === 'h1') {
|
|
50
|
-
// Avoid duplicate errors with MD025.
|
|
51
|
-
if (hasTopLevelHeading !== false && hasTopLevelHeading !== 1) {
|
|
52
|
-
onError({
|
|
53
|
-
lineNumber: token.lineNumber,
|
|
54
|
-
});
|
|
55
|
-
} else if (params.name.includes('/docs/pages/blog/')) {
|
|
56
|
-
onError({
|
|
57
|
-
lineNumber: token.lineNumber,
|
|
58
|
-
detail: 'In the blog, the h1 is already added using the markdown header.title value.',
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Store the first h1 of the page.
|
|
63
|
-
if (hasTopLevelHeading === false) {
|
|
64
|
-
hasTopLevelHeading = token.lineNumber;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
},
|
|
69
|
-
};
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
names: ['gitDiff'],
|
|
3
|
-
description: 'Respect the format output of git diff.',
|
|
4
|
-
tags: ['spaces'],
|
|
5
|
-
/**
|
|
6
|
-
* @param {import('./duplicate-h1.mjs').MdParams} params
|
|
7
|
-
* @param {import('./duplicate-h1.mjs').OnError} onError
|
|
8
|
-
*/
|
|
9
|
-
function: (params, onError) => {
|
|
10
|
-
params.tokens.forEach((token) => {
|
|
11
|
-
if (token.type === 'fence' && token.info === 'diff') {
|
|
12
|
-
token.content.split('\n').forEach((line, index) => {
|
|
13
|
-
if (
|
|
14
|
-
line[0] !== ' ' &&
|
|
15
|
-
line[0] !== '-' &&
|
|
16
|
-
line[0] !== '+' &&
|
|
17
|
-
line !== '' &&
|
|
18
|
-
line.indexOf('@@ ') !== 0 &&
|
|
19
|
-
line.indexOf('diff --git ') !== 0 &&
|
|
20
|
-
line.indexOf('index ') !== 0
|
|
21
|
-
) {
|
|
22
|
-
onError({
|
|
23
|
-
lineNumber: token.lineNumber + index + 1,
|
|
24
|
-
detail: `The line start with "+" or "-" or " ": ${line}`,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
},
|
|
31
|
-
};
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import straightQuotes from './straight-quotes.mjs';
|
|
2
|
-
import gitDiff from './git-diff.mjs';
|
|
3
|
-
import tableAlignment from './table-alignment.mjs';
|
|
4
|
-
import terminalLanguage from './terminal-language.mjs';
|
|
5
|
-
import duplicateH1 from './duplicate-h1.mjs';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Create a base configuration for markdownlint.
|
|
9
|
-
* @param {Object} options
|
|
10
|
-
* @param {(typeof straightQuotes)[]} [options.rules] - An array of custom rules to include.
|
|
11
|
-
* @param {string[]} [options.ignores] - An array of glob patterns to ignore.
|
|
12
|
-
* @returns
|
|
13
|
-
*/
|
|
14
|
-
export function createBaseConfig(options = {}) {
|
|
15
|
-
const { rules = [], ignores = [] } = options;
|
|
16
|
-
// https://github.com/DavidAnson/markdownlint#rules--aliases
|
|
17
|
-
return {
|
|
18
|
-
config: {
|
|
19
|
-
default: true,
|
|
20
|
-
MD004: false, // MD004/ul-style. Buggy
|
|
21
|
-
MD009: {
|
|
22
|
-
// MD009/no-trailing-spaces
|
|
23
|
-
br_spaces: 0,
|
|
24
|
-
strict: true,
|
|
25
|
-
list_item_empty_lines: false,
|
|
26
|
-
},
|
|
27
|
-
MD013: false, // MD013/line-length. Already handled by Prettier.
|
|
28
|
-
MD014: false, // MD014/commands-show-output. It's OK.
|
|
29
|
-
MD024: { siblings_only: true }, // MD024/no-duplicate-heading/no-duplicate-header
|
|
30
|
-
MD025: {
|
|
31
|
-
// Heading level
|
|
32
|
-
level: 1,
|
|
33
|
-
// RegExp for matching title in front matter
|
|
34
|
-
front_matter_title: '',
|
|
35
|
-
},
|
|
36
|
-
MD033: false, // MD033/no-inline-html. We use it from time to time, it's fine.
|
|
37
|
-
MD034: false, // MD034/no-bare-urls. Not a concern for us, our Markdown interpreter supports it.
|
|
38
|
-
MD028: false, // MD028/no-blanks-blockquote prevent double blockquote
|
|
39
|
-
MD029: false, // MD029/ol-prefix. Buggy
|
|
40
|
-
MD031: false, // MD031/blanks-around-fences Some code blocks inside li
|
|
41
|
-
MD036: false, // MD036/no-emphasis-as-heading/no-emphasis-as-header. It's OK.
|
|
42
|
-
MD051: false, // MD051/link-fragments. Many false positives in the changelog.
|
|
43
|
-
MD052: false, // MD052/reference-links-images. Many false positives in the changelog.
|
|
44
|
-
MD059: false, // MD059/descriptive-link-text. Does not allow links on text like "link", whereas we redirect to "Link" component.
|
|
45
|
-
straightQuotes: true,
|
|
46
|
-
gitDiff: true,
|
|
47
|
-
tableAlignment: true,
|
|
48
|
-
terminalLanguage: true,
|
|
49
|
-
duplicateH1: true,
|
|
50
|
-
},
|
|
51
|
-
customRules: [straightQuotes, gitDiff, tableAlignment, terminalLanguage, duplicateH1, ...rules],
|
|
52
|
-
ignores: [
|
|
53
|
-
'CHANGELOG.old.md',
|
|
54
|
-
'**/node_modules/**',
|
|
55
|
-
'**/build/**',
|
|
56
|
-
'.github/PULL_REQUEST_TEMPLATE.md',
|
|
57
|
-
'docs/public/**',
|
|
58
|
-
'docs/export/**',
|
|
59
|
-
...ignores,
|
|
60
|
-
],
|
|
61
|
-
};
|
|
62
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
const nonStraightQuotes = /[‘’“”]/;
|
|
2
|
-
|
|
3
|
-
export default {
|
|
4
|
-
names: ['straightQuotes'],
|
|
5
|
-
description: 'Only allow straight quotes.',
|
|
6
|
-
tags: ['spelling'],
|
|
7
|
-
/**
|
|
8
|
-
* @param {import('./duplicate-h1.mjs').MdParams} params
|
|
9
|
-
* @param {import('./duplicate-h1.mjs').OnError} onError
|
|
10
|
-
*/
|
|
11
|
-
function: (params, onError) => {
|
|
12
|
-
params.lines.forEach((line, lineNumber) => {
|
|
13
|
-
// It will match
|
|
14
|
-
// opening single quote: \xE2\x80\x98
|
|
15
|
-
// closing single quote: \xE2\x80\x99
|
|
16
|
-
// opening double quote: \xE2\x80\x9C
|
|
17
|
-
// closing double quote: \xE2\x80\x9D
|
|
18
|
-
if (nonStraightQuotes.test(line)) {
|
|
19
|
-
onError({
|
|
20
|
-
lineNumber: lineNumber + 1,
|
|
21
|
-
detail: `For line: ${line}`,
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
},
|
|
26
|
-
};
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @param {import('./duplicate-h1.mjs').Attr[]} attrs
|
|
3
|
-
* @returns {Record<string, string>}
|
|
4
|
-
*/
|
|
5
|
-
function attr(attrs) {
|
|
6
|
-
return (attrs || []).reduce((acc, item) => ({ ...acc, [item[0]]: item[1] }), {});
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export default {
|
|
10
|
-
names: ['tableAlignment'],
|
|
11
|
-
description: 'Set table alignment.',
|
|
12
|
-
tags: ['table'],
|
|
13
|
-
/**
|
|
14
|
-
* @param {import('./duplicate-h1.mjs').MdParams} params
|
|
15
|
-
* @param {import('./duplicate-h1.mjs').OnError} onError
|
|
16
|
-
*/
|
|
17
|
-
function: (params, onError) => {
|
|
18
|
-
params.tokens.forEach((token) => {
|
|
19
|
-
// This is wrong:
|
|
20
|
-
// | Version | Supported |
|
|
21
|
-
// | ------- | ------------------ |
|
|
22
|
-
//
|
|
23
|
-
// The second column should be left aligned because it contains text:
|
|
24
|
-
// | Version | Supported |
|
|
25
|
-
// | ------- | :----------------- |
|
|
26
|
-
//
|
|
27
|
-
// However, columns that includes numbers should be right aligned:
|
|
28
|
-
// | Version | Supported |
|
|
29
|
-
// | ------: | :----------------- |
|
|
30
|
-
//
|
|
31
|
-
// More details: https://ux.stackexchange.com/questions/24066/what-is-the-best-practice-for-data-table-cell-content-alignment
|
|
32
|
-
//
|
|
33
|
-
// In this check we expect the style to be 'text-align:right' or equivalent.
|
|
34
|
-
if (token.type === 'th_open' && attr(token.attrs).style == null) {
|
|
35
|
-
onError({
|
|
36
|
-
lineNumber: token.lineNumber,
|
|
37
|
-
detail: `${params.lines[token.lineNumber - 1]}`,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
},
|
|
42
|
-
};
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
names: ['terminalLanguage'],
|
|
3
|
-
description: 'Set the right language for terminal code.',
|
|
4
|
-
tags: ['code'],
|
|
5
|
-
/**
|
|
6
|
-
* @param {import('./duplicate-h1.mjs').MdParams} params
|
|
7
|
-
* @param {import('./duplicate-h1.mjs').OnError} onError
|
|
8
|
-
*/
|
|
9
|
-
function: (params, onError) => {
|
|
10
|
-
params.tokens.forEach((token) => {
|
|
11
|
-
if (token.type === 'fence' && token.info === 'sh') {
|
|
12
|
-
onError({
|
|
13
|
-
lineNumber: token.lineNumber,
|
|
14
|
-
detail: `Use "bash" instead of "sh".`,
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
},
|
|
19
|
-
};
|